reverse membership for additional owner roles (#1862)

* reverse membership for additional owner roles
* remove type RoleOriginSpilo
* use e2e images with cron_admin inside
* let operator resolve reversed membership
* make additional owner roles part of the sync user strategy
* add more context in the docs about additional_owner_roles
This commit is contained in:
Felix Kunde
2022-04-28 11:15:40 +02:00
committed by GitHub
parent 9eb7517218
commit a77d5df158
7 changed files with 97 additions and 72 deletions
+12 -23
View File
@@ -133,8 +133,10 @@ func New(cfg Config, kubeClient k8sutil.KubernetesClient, pgSpec acidv1.Postgres
Services: make(map[PostgresRole]*v1.Service),
Endpoints: make(map[PostgresRole]*v1.Endpoints)},
userSyncStrategy: users.DefaultUserSyncStrategy{
PasswordEncryption: passwordEncryption,
RoleDeletionSuffix: cfg.OpConfig.RoleDeletionSuffix},
PasswordEncryption: passwordEncryption,
RoleDeletionSuffix: cfg.OpConfig.RoleDeletionSuffix,
AdditionalOwnerRoles: cfg.OpConfig.AdditionalOwnerRoles,
},
deleteOptions: metav1.DeleteOptions{PropagationPolicy: &deletePropagationPolicy},
podEventsQueue: podEventsQueue,
KubeClient: kubeClient,
@@ -1308,28 +1310,15 @@ func (c *Cluster) initRobotUsers() error {
}
func (c *Cluster) initAdditionalOwnerRoles() {
for _, additionalOwner := range c.OpConfig.AdditionalOwnerRoles {
// fetch all database owners the additional should become a member of
memberOf := make([]string, 0)
for username, pgUser := range c.pgUsers {
if pgUser.IsDbOwner {
memberOf = append(memberOf, username)
}
}
if len(c.OpConfig.AdditionalOwnerRoles) == 0 {
return
}
if len(memberOf) > 0 {
namespace := c.Namespace
additionalOwnerPgUser := spec.PgUser{
Origin: spec.RoleOriginSpilo,
MemberOf: memberOf,
Name: additionalOwner,
Namespace: namespace,
}
if currentRole, present := c.pgUsers[additionalOwner]; present {
c.pgUsers[additionalOwner] = c.resolveNameConflict(&currentRole, &additionalOwnerPgUser)
} else {
c.pgUsers[additionalOwner] = additionalOwnerPgUser
}
// fetch database owners and assign additional owner roles
for username, pgUser := range c.pgUsers {
if pgUser.IsDbOwner {
pgUser.MemberOf = append(pgUser.MemberOf, c.OpConfig.AdditionalOwnerRoles...)
c.pgUsers[username] = pgUser
}
}
}
+9 -20
View File
@@ -148,11 +148,9 @@ func TestInitAdditionalOwnerRoles(t *testing.T) {
manifestUsers := map[string]acidv1.UserFlags{"foo_owner": {}, "bar_owner": {}, "app_user": {}}
expectedUsers := map[string]spec.PgUser{
"foo_owner": {Origin: spec.RoleOriginManifest, Name: "foo_owner", Namespace: cl.Namespace, Password: "f123", Flags: []string{"LOGIN"}, IsDbOwner: true},
"bar_owner": {Origin: spec.RoleOriginManifest, Name: "bar_owner", Namespace: cl.Namespace, Password: "b123", Flags: []string{"LOGIN"}, IsDbOwner: true},
"app_user": {Origin: spec.RoleOriginManifest, Name: "app_user", Namespace: cl.Namespace, Password: "a123", Flags: []string{"LOGIN"}, IsDbOwner: false},
"cron_admin": {Origin: spec.RoleOriginSpilo, Name: "cron_admin", Namespace: cl.Namespace, MemberOf: []string{"foo_owner", "bar_owner"}},
"part_man": {Origin: spec.RoleOriginSpilo, Name: "part_man", Namespace: cl.Namespace, MemberOf: []string{"foo_owner", "bar_owner"}},
"foo_owner": {Origin: spec.RoleOriginManifest, Name: "foo_owner", Namespace: cl.Namespace, Password: "f123", Flags: []string{"LOGIN"}, IsDbOwner: true, MemberOf: []string{"cron_admin", "part_man"}},
"bar_owner": {Origin: spec.RoleOriginManifest, Name: "bar_owner", Namespace: cl.Namespace, Password: "b123", Flags: []string{"LOGIN"}, IsDbOwner: true, MemberOf: []string{"cron_admin", "part_man"}},
"app_user": {Origin: spec.RoleOriginManifest, Name: "app_user", Namespace: cl.Namespace, Password: "a123", Flags: []string{"LOGIN"}, IsDbOwner: false},
}
cl.Spec.Databases = map[string]string{"foo_db": "foo_owner", "bar_db": "bar_owner"}
@@ -163,24 +161,15 @@ func TestInitAdditionalOwnerRoles(t *testing.T) {
t.Errorf("%s could not init manifest users", testName)
}
// update passwords to compare with result
for manifestUser := range manifestUsers {
pgUser := cl.pgUsers[manifestUser]
pgUser.Password = manifestUser[0:1] + "123"
cl.pgUsers[manifestUser] = pgUser
}
// now assign additional roles to owners
cl.initAdditionalOwnerRoles()
for _, additionalOwnerRole := range cl.Config.OpConfig.AdditionalOwnerRoles {
expectedPgUser := expectedUsers[additionalOwnerRole]
existingPgUser, exists := cl.pgUsers[additionalOwnerRole]
if !exists {
t.Errorf("%s additional owner role %q not initilaized", testName, additionalOwnerRole)
}
// update passwords to compare with result
for username, existingPgUser := range cl.pgUsers {
expectedPgUser := expectedUsers[username]
if !util.IsEqualIgnoreOrder(expectedPgUser.MemberOf, existingPgUser.MemberOf) {
t.Errorf("%s unexpected membership of additional owner role %q: expected member of %#v, got member of %#v",
testName, additionalOwnerRole, expectedPgUser.MemberOf, existingPgUser.MemberOf)
t.Errorf("%s unexpected membership of user %q: expected member of %#v, got member of %#v",
testName, username, expectedPgUser.MemberOf, existingPgUser.MemberOf)
}
}
}
+1 -1
View File
@@ -1650,7 +1650,7 @@ func (c *Cluster) generateUserSecrets() map[string]*v1.Secret {
func (c *Cluster) generateSingleUserSecret(namespace string, pgUser spec.PgUser) *v1.Secret {
//Skip users with no password i.e. human users (they'll be authenticated using pam)
if pgUser.Password == "" {
if pgUser.Origin != spec.RoleOriginTeamsAPI && pgUser.Origin != spec.RoleOriginSpilo {
if pgUser.Origin != spec.RoleOriginTeamsAPI {
c.logger.Warningf("could not generate secret for a non-teamsAPI role %q: role has no password",
pgUser.Name)
}
-1
View File
@@ -30,7 +30,6 @@ const (
RoleOriginManifest
RoleOriginInfrastructure
RoleOriginTeamsAPI
RoleOriginSpilo
RoleOriginSystem
RoleOriginBootstrap
RoleConnectionPooler
+47 -14
View File
@@ -20,6 +20,7 @@ const (
alterRoleSetSQL = `ALTER ROLE "%s" SET %s TO %s`
dropUserSQL = `SET LOCAL synchronous_commit = 'local'; DROP ROLE "%s";`
grantToUserSQL = `GRANT %s TO "%s"`
revokeFromUserSQL = `REVOKE %s FROM %s`
doBlockStmt = `SET LOCAL synchronous_commit = 'local'; DO $$ BEGIN %s; END;$$;`
passwordTemplate = "ENCRYPTED PASSWORD '%s'"
inRoleTemplate = `IN ROLE %s`
@@ -31,8 +32,9 @@ const (
// an existing roles of another role membership, nor it removes the already assigned flag
// (except for the NOLOGIN). TODO: process other NOflags, i.e. NOSUPERUSER correctly.
type DefaultUserSyncStrategy struct {
PasswordEncryption string
RoleDeletionSuffix string
PasswordEncryption string
RoleDeletionSuffix string
AdditionalOwnerRoles []string
}
// ProduceSyncRequests figures out the types of changes that need to happen with the given users.
@@ -53,30 +55,27 @@ func (strategy DefaultUserSyncStrategy) ProduceSyncRequests(dbUsers spec.PgUserM
}
} else {
r := spec.PgSyncUserRequest{}
r.User = dbUser
newMD5Password := util.NewEncryptor(strategy.PasswordEncryption).PGUserPassword(newUser)
// do not compare for roles coming from docker image
if newUser.Origin != spec.RoleOriginSpilo {
if dbUser.Password != newMD5Password {
r.User.Password = newMD5Password
r.Kind = spec.PGsyncUserAlter
}
if addNewFlags, equal := util.SubstractStringSlices(newUser.Flags, dbUser.Flags); !equal {
r.User.Flags = addNewFlags
r.Kind = spec.PGsyncUserAlter
}
if dbUser.Password != newMD5Password {
r.User.Password = newMD5Password
r.Kind = spec.PGsyncUserAlter
}
if addNewRoles, equal := util.SubstractStringSlices(newUser.MemberOf, dbUser.MemberOf); !equal {
r.User.MemberOf = addNewRoles
r.User.IsDbOwner = newUser.IsDbOwner
r.Kind = spec.PGsyncUserAlter
}
if addNewFlags, equal := util.SubstractStringSlices(newUser.Flags, dbUser.Flags); !equal {
r.User.Flags = addNewFlags
r.Kind = spec.PGsyncUserAlter
}
if r.Kind == spec.PGsyncUserAlter {
r.User.Name = newUser.Name
reqs = append(reqs, r)
}
if newUser.Origin != spec.RoleOriginSpilo &&
len(newUser.Parameters) > 0 &&
if len(newUser.Parameters) > 0 &&
!reflect.DeepEqual(dbUser.Parameters, newUser.Parameters) {
reqs = append(reqs, spec.PgSyncUserRequest{Kind: spec.PGSyncAlterSet, User: newUser})
}
@@ -120,6 +119,15 @@ func (strategy DefaultUserSyncStrategy) ExecuteSyncRequests(requests []spec.PgSy
if err := strategy.alterPgUser(request.User, db); err != nil {
reqretries = append(reqretries, request)
errors = append(errors, fmt.Sprintf("could not alter user %q: %v", request.User.Name, err))
// XXX: we do not allow additional owner roles to be members of database owners
// if ALTER fails it could be because of the wrong memberhip (check #1862 for details)
// so in any case try to revoke the database owner from the additional owner roles
// the initial ALTER statement will be retried once and should work then
if request.User.IsDbOwner && len(strategy.AdditionalOwnerRoles) > 0 {
if err := resolveOwnerMembership(request.User, strategy.AdditionalOwnerRoles, db); err != nil {
errors = append(errors, fmt.Sprintf("could not resolve owner membership for %q: %v", request.User.Name, err))
}
}
}
case spec.PGSyncAlterSet:
if err := strategy.alterPgUserSet(request.User, db); err != nil {
@@ -152,6 +160,21 @@ func (strategy DefaultUserSyncStrategy) ExecuteSyncRequests(requests []spec.PgSy
return nil
}
func resolveOwnerMembership(dbOwner spec.PgUser, additionalOwners []string, db *sql.DB) error {
errors := make([]string, 0)
for _, additionalOwner := range additionalOwners {
if err := revokeRole(dbOwner.Name, additionalOwner, db); err != nil {
errors = append(errors, fmt.Sprintf("could not revoke %q from %q: %v", dbOwner.Name, additionalOwner, err))
}
}
if len(errors) > 0 {
return fmt.Errorf("could not resolve membership between %q and additional owner roles: %v", dbOwner.Name, strings.Join(errors, `', '`))
}
return nil
}
func (strategy DefaultUserSyncStrategy) alterPgUserSet(user spec.PgUser, db *sql.DB) error {
queries := produceAlterRoleSetStmts(user)
query := fmt.Sprintf(doBlockStmt, strings.Join(queries, ";"))
@@ -272,6 +295,16 @@ func quoteMemberList(user spec.PgUser) string {
return strings.Join(memberof, ",")
}
func revokeRole(groupRole, role string, db *sql.DB) error {
revokeStmt := fmt.Sprintf(revokeFromUserSQL, groupRole, role)
if _, err := db.Exec(fmt.Sprintf(doBlockStmt, revokeStmt)); err != nil {
return err
}
return nil
}
// quoteVal quotes values to be used at ALTER ROLE SET param = value if necessary
func quoteParameterValue(name, val string) string {
start := val[0]