diff --git a/charts/postgres-operator/crds/operatorconfigurations.yaml b/charts/postgres-operator/crds/operatorconfigurations.yaml index 0ac5651c3..0f036c299 100644 --- a/charts/postgres-operator/crds/operatorconfigurations.yaml +++ b/charts/postgres-operator/crds/operatorconfigurations.yaml @@ -443,6 +443,9 @@ spec: enable_postgres_team_crd_superusers: type: boolean default: false + enable_team_member_deprecation: + type: boolean + default: false enable_team_superuser: type: boolean default: false @@ -465,6 +468,9 @@ spec: type: string default: - admin + role_deletion_suffix: + type: string + default: "_deleted" team_admin_role: type: string default: "admin" diff --git a/charts/postgres-operator/values-crd.yaml b/charts/postgres-operator/values-crd.yaml index 6110fe8bc..bd563a636 100644 --- a/charts/postgres-operator/values-crd.yaml +++ b/charts/postgres-operator/values-crd.yaml @@ -289,13 +289,13 @@ configLogicalBackup: # automate creation of human users with teams API service configTeamsApi: # team_admin_role will have the rights to grant roles coming from PG manifests - # enable_admin_role_for_users: true - + enable_admin_role_for_users: true # operator watches for PostgresTeam CRs to assign additional teams and members to clusters enable_postgres_team_crd: false # toogle to create additional superuser teams from PostgresTeam CRs - # enable_postgres_team_crd_superusers: false - + enable_postgres_team_crd_superusers: false + # toggle to automatically rename roles of former team members and deny LOGIN + enable_team_member_deprecation: false # toggle to grant superuser to team members created from the Teams API enable_team_superuser: false # toggles usage of the Teams API by the operator @@ -306,12 +306,13 @@ configTeamsApi: # operator will add all team member roles to this group and add a pg_hba line pam_role_name: zalandos # List of teams which members need the superuser role in each Postgres cluster - # postgres_superuser_teams: - # - postgres_superusers - + postgres_superuser_teams: + - postgres_superusers # List of roles that cannot be overwritten by an application, team or infrastructure role protected_role_names: - admin + # Suffix to add if members are removed from TeamsAPI or PostgresTeam CRD + role_deletion_suffix: "_deleted" # role name to grant to team members created from the Teams API team_admin_role: admin # postgres config parameters to apply to each team member role diff --git a/charts/postgres-operator/values.yaml b/charts/postgres-operator/values.yaml index 1cf84ca87..15a53a00d 100644 --- a/charts/postgres-operator/values.yaml +++ b/charts/postgres-operator/values.yaml @@ -280,36 +280,32 @@ configLogicalBackup: # automate creation of human users with teams API service configTeamsApi: # team_admin_role will have the rights to grant roles coming from PG manifests - # enable_admin_role_for_users: "true" - + enable_admin_role_for_users: "true" # operator watches for PostgresTeam CRs to assign additional teams and members to clusters enable_postgres_team_crd: "false" # toogle to create additional superuser teams from PostgresTeam CRs - # enable_postgres_team_crd_superusers: "false" - + enable_postgres_team_crd_superusers: "false" + # toggle to automatically rename roles of former team members and deny LOGIN + enable_team_member_deprecation: "false" # toggle to grant superuser to team members created from the Teams API - # enable_team_superuser: "false" - + enable_team_superuser: "false" # toggles usage of the Teams API by the operator enable_teams_api: "false" # should contain a URL to use for authentication (username and token) # pam_configuration: https://info.example.com/oauth2/tokeninfo?access_token= uid realm=/employees # operator will add all team member roles to this group and add a pg_hba line - # pam_role_name: zalandos - + pam_role_name: "zalandos" # List of teams which members need the superuser role in each Postgres cluster - # postgres_superuser_teams: "postgres_superusers" - + postgres_superuser_teams: "postgres_superusers" # List of roles that cannot be overwritten by an application, team or infrastructure role - # protected_role_names: "admin" - + protected_role_names: "admin" + # Suffix to add if members are removed from TeamsAPI or PostgresTeam CRD + role_deletion_suffix: "_deleted" # role name to grant to team members created from the Teams API - # team_admin_role: "admin" - + team_admin_role: "admin" # postgres config parameters to apply to each team member role - # team_api_role_configuration: "log_statement:all" - + team_api_role_configuration: "log_statement:all" # URL of the Teams API service # teams_api_url: http://fake-teams-api.default.svc.cluster.local diff --git a/docs/reference/operator_parameters.md b/docs/reference/operator_parameters.md index 80d0e5b8c..b0d982943 100644 --- a/docs/reference/operator_parameters.md +++ b/docs/reference/operator_parameters.md @@ -704,6 +704,19 @@ key. cluster to administer Postgres and maintain infrastructure built around it. The default is empty. +* **role_deletion_suffix** + defines a suffix that - when `enable_team_member_deprecation` is set to + `true` - will be appended to database role names of team members that were + removed from either the team in the Teams API or a `PostgresTeam` custom + resource (additionalMembers). When re-added, the operator will rename roles + with the defined suffix back to the original role name. + The default is `_deleted`. + +* **enable_team_member_deprecation** + if `true` database roles of former team members will be renamed by appending + the configured `role_deletion_suffix` and `LOGIN` privilege will be revoked. + The default is `false`. + * **enable_postgres_team_crd** toggle to make the operator watch for created or updated `PostgresTeam` CRDs and create roles for specified additional teams and members. diff --git a/docs/user.md b/docs/user.md index 3342913c8..8e406ec00 100644 --- a/docs/user.md +++ b/docs/user.md @@ -407,6 +407,23 @@ spec: - "briggs" ``` +#### Removed members + +The Postgres Operator does not delete database roles when users are removed +from manifests. But, using the `PostgresTeam` custom resource or Teams API it +is very easy to add roles to many clusters. Manually reverting such a change +is cumbersome. Therefore, if members are removed from a `PostgresTeam` or the +Teams API the operator can rename roles appending a configured suffix to the +name (see `role_deletion_suffix` option) and revoke the `LOGIN` privilege. +The suffix makes it easy then for a cleanup script to remove those deprecated +roles completely. Switch `enable_team_member_deprecation` to `true` to enable +this behavior. + +When a role is re-added to a `PostgresTeam` manifest (or to the source behind +the Teams API) the operator will check for roles with the configured suffix +and if found, rename the role back to the original name and grant `LOGIN` +again. + ## Prepared databases with roles and default privileges The `users` section in the manifests only allows for creating database roles diff --git a/e2e/tests/test_e2e.py b/e2e/tests/test_e2e.py index 114f881c4..bffb12386 100644 --- a/e2e/tests/test_e2e.py +++ b/e2e/tests/test_e2e.py @@ -197,13 +197,15 @@ def test_additional_teams_and_members(self): enable_postgres_team_crd = { "data": { "enable_postgres_team_crd": "true", - "resync_period": "15s", + "enable_team_member_deprecation": "true", + "role_deletion_suffix": "_delete_me", + "resync_period": "15s" }, } self.k8s.update_config(enable_postgres_team_crd) self.eventuallyEqual(lambda: self.k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync") - + self.k8s.api.custom_objects_api.patch_namespaced_custom_object( 'acid.zalan.do', 'v1', 'default', 'postgresteams', 'custom-team-membership', @@ -222,18 +224,60 @@ def test_additional_teams_and_members(self): } }) - # make sure we let one sync pass and the new user being added - time.sleep(15) - leader = self.k8s.get_cluster_leader_pod() user_query = """ - SELECT usename - FROM pg_catalog.pg_user - WHERE usename IN ('elephant', 'kind'); + SELECT rolname + FROM pg_catalog.pg_roles + WHERE rolname IN ('elephant', 'kind'); + """ + self.eventuallyEqual(lambda: len(self.query_database(leader.metadata.name, "postgres", user_query)), 2, + "Not all additional users found in database", 10, 5) + + # replace additional member and check if the removed member's role is renamed + self.k8s.api.custom_objects_api.patch_namespaced_custom_object( + 'acid.zalan.do', 'v1', 'default', + 'postgresteams', 'custom-team-membership', + { + 'spec': { + 'additionalMembers': { + 'e2e': [ + 'tester' + ] + }, + } + }) + + user_query = """ + SELECT rolname + FROM pg_catalog.pg_roles + WHERE (rolname = 'tester' AND rolcanlogin) + OR (rolname = 'kind_delete_me' AND NOT rolcanlogin); + """ + self.eventuallyEqual(lambda: len(self.query_database(leader.metadata.name, "postgres", user_query)), 2, + "Database role of replaced member in PostgresTeam not renamed", 10, 5) + + # re-add additional member and check if the role is renamed back + self.k8s.api.custom_objects_api.patch_namespaced_custom_object( + 'acid.zalan.do', 'v1', 'default', + 'postgresteams', 'custom-team-membership', + { + 'spec': { + 'additionalMembers': { + 'e2e': [ + 'kind' + ] + }, + } + }) + + user_query = """ + SELECT rolname + FROM pg_catalog.pg_roles + WHERE (rolname = 'kind' AND rolcanlogin) + OR (rolname = 'tester_delete_me' AND NOT rolcanlogin); """ - users = self.query_database(leader.metadata.name, "postgres", user_query) - self.eventuallyEqual(lambda: len(users), 2, - "Not all additional users found in database: {}".format(users)) + self.eventuallyEqual(lambda: len(self.query_database(leader.metadata.name, "postgres", user_query)), 2, + "Database role of recreated member in PostgresTeam not renamed back to original name", 10, 5) # revert config change revert_resync = { @@ -407,9 +451,9 @@ def test_enable_disable_connection_pooler(self): leader = k8s.get_cluster_leader_pod() schemas_query = """ - select schema_name - from information_schema.schemata - where schema_name = 'pooler' + SELECT schema_name + FROM information_schema.schemata + WHERE schema_name = 'pooler' """ db_list = self.list_databases(leader.metadata.name) @@ -529,6 +573,7 @@ def verify_role(): "Parameters": None, "AdminRole": "", "Origin": 2, + "Deleted": False }) return True except: @@ -1417,7 +1462,7 @@ def list_databases(self, pod_name): k8s = self.k8s result_set = [] db_list = [] - db_list_query = "select datname from pg_database" + db_list_query = "SELECT datname FROM pg_database" exec_query = r"psql -tAq -c \"{}\" -d {}" try: diff --git a/manifests/configmap.yaml b/manifests/configmap.yaml index 29390a1f0..b379975eb 100644 --- a/manifests/configmap.yaml +++ b/manifests/configmap.yaml @@ -51,6 +51,7 @@ data: # enable_shm_volume: "true" # enable_sidecars: "true" enable_spilo_wal_path_compat: "true" + enable_team_member_deprecation: "false" # enable_team_superuser: "false" enable_teams_api: "false" # etcd_host: "" @@ -111,6 +112,7 @@ data: resource_check_timeout: 10m resync_period: 30m ring_log_lines: "100" + role_deletion_suffix: "_deleted" secret_name_template: "{username}.{cluster}.credentials" # sidecar_docker_images: "" # set_memory_request_to_limit: "false" diff --git a/manifests/operatorconfiguration.crd.yaml b/manifests/operatorconfiguration.crd.yaml index 47244e5d7..fbed0bea1 100644 --- a/manifests/operatorconfiguration.crd.yaml +++ b/manifests/operatorconfiguration.crd.yaml @@ -439,6 +439,9 @@ spec: enable_postgres_team_crd_superusers: type: boolean default: false + enable_team_member_deprecation: + type: boolean + default: false enable_team_superuser: type: boolean default: false @@ -461,6 +464,9 @@ spec: type: string default: - admin + role_deletion_suffix: + type: string + default: "_deleted" team_admin_role: type: string default: "admin" diff --git a/manifests/postgresql-operator-default-configuration.yaml b/manifests/postgresql-operator-default-configuration.yaml index 3a8a79c8d..65dfd6ce4 100644 --- a/manifests/postgresql-operator-default-configuration.yaml +++ b/manifests/postgresql-operator-default-configuration.yaml @@ -141,6 +141,7 @@ configuration: # enable_admin_role_for_users: true # enable_postgres_team_crd: false # enable_postgres_team_crd_superusers: false + enable_team_member_deprecation: false enable_team_superuser: false enable_teams_api: false # pam_configuration: "" @@ -149,6 +150,7 @@ configuration: # - postgres_superusers protected_role_names: - admin + role_deletion_suffix: "_deleted" team_admin_role: admin team_api_role_configuration: log_statement: all diff --git a/pkg/apis/acid.zalan.do/v1/crds.go b/pkg/apis/acid.zalan.do/v1/crds.go index 89d71eef5..83e7273e4 100644 --- a/pkg/apis/acid.zalan.do/v1/crds.go +++ b/pkg/apis/acid.zalan.do/v1/crds.go @@ -1377,6 +1377,9 @@ var OperatorConfigCRDResourceValidation = apiextv1.CustomResourceValidation{ "enable_postgres_team_crd_superusers": { Type: "boolean", }, + "enable_team_member_deprecation": { + Type: "boolean", + }, "enable_team_superuser": { Type: "boolean", }, @@ -1405,6 +1408,9 @@ var OperatorConfigCRDResourceValidation = apiextv1.CustomResourceValidation{ }, }, }, + "role_deletion_suffix": { + Type: "string", + }, "team_admin_role": { Type: "string", }, diff --git a/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go b/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go index 78b618b78..cf581431b 100644 --- a/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go +++ b/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go @@ -159,6 +159,8 @@ type TeamsAPIConfiguration struct { PostgresSuperuserTeams []string `json:"postgres_superuser_teams,omitempty"` EnablePostgresTeamCRD bool `json:"enable_postgres_team_crd,omitempty"` EnablePostgresTeamCRDSuperusers bool `json:"enable_postgres_team_crd_superusers,omitempty"` + EnableTeamMemberDeprecation bool `json:"enable_team_member_deprecation,omitempty"` + RoleDeletionSuffix string `json:"role_deletion_suffix,omitempty"` } // LoggingRESTAPIConfiguration defines Logging API conf diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go index ff3a33af9..5b4d15ba5 100644 --- a/pkg/cluster/cluster.go +++ b/pkg/cluster/cluster.go @@ -74,6 +74,7 @@ type Cluster struct { eventRecorder record.EventRecorder patroni patroni.Interface pgUsers map[string]spec.PgUser + pgUsersCache map[string]spec.PgUser systemUsers map[string]spec.PgUser podSubscribers map[spec.NamespacedName]chan PodEvent podSubscribersMu sync.RWMutex @@ -129,7 +130,9 @@ func New(cfg Config, kubeClient k8sutil.KubernetesClient, pgSpec acidv1.Postgres Secrets: make(map[types.UID]*v1.Secret), Services: make(map[PostgresRole]*v1.Service), Endpoints: make(map[PostgresRole]*v1.Endpoints)}, - userSyncStrategy: users.DefaultUserSyncStrategy{PasswordEncryption: passwordEncryption}, + userSyncStrategy: users.DefaultUserSyncStrategy{ + PasswordEncryption: passwordEncryption, + RoleDeletionSuffix: cfg.OpConfig.RoleDeletionSuffix}, deleteOptions: metav1.DeleteOptions{PropagationPolicy: &deletePropagationPolicy}, podEventsQueue: podEventsQueue, KubeClient: kubeClient, @@ -190,6 +193,17 @@ func (c *Cluster) isNewCluster() bool { func (c *Cluster) initUsers() error { c.setProcessName("initializing users") + // if team member deprecation is enabled save current state of pgUsers + // to check for deleted roles + c.pgUsersCache = map[string]spec.PgUser{} + if c.OpConfig.EnableTeamMemberDeprecation { + for k, v := range c.pgUsers { + if v.Origin == spec.RoleOriginTeamsAPI { + c.pgUsersCache[k] = v + } + } + } + // clear our the previous state of the cluster users (in case we are // running a sync). c.systemUsers = map[string]spec.PgUser{} @@ -650,7 +664,7 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error { needConnectionPooler := needMasterConnectionPoolerWorker(&newSpec.Spec) || needReplicaConnectionPoolerWorker(&newSpec.Spec) if !sameUsers || needConnectionPooler { - c.logger.Debugf("syncing secrets") + c.logger.Debugf("initialize users") if err := c.initUsers(); err != nil { c.logger.Errorf("could not init users: %v", err) updateFailed = true diff --git a/pkg/cluster/database.go b/pkg/cluster/database.go index 760b68d72..829c2e5c7 100644 --- a/pkg/cluster/database.go +++ b/pkg/cluster/database.go @@ -198,6 +198,7 @@ func (c *Cluster) readPgUsersFromDatabase(userNames []string) (users spec.PgUser rolname, rolpassword string rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin bool roloptions, memberof []string + roldeleted bool ) err := rows.Scan(&rolname, &rolpassword, &rolsuper, &rolinherit, &rolcreaterole, &rolcreatedb, &rolcanlogin, pq.Array(&roloptions), pq.Array(&memberof)) @@ -216,7 +217,11 @@ func (c *Cluster) readPgUsersFromDatabase(userNames []string) (users spec.PgUser parameters[fields[0]] = fields[1] } - users[rolname] = spec.PgUser{Name: rolname, Password: rolpassword, Flags: flags, MemberOf: memberof, Parameters: parameters} + if strings.HasSuffix(rolname, c.OpConfig.RoleDeletionSuffix) { + roldeleted = true + } + + users[rolname] = spec.PgUser{Name: rolname, Password: rolpassword, Flags: flags, MemberOf: memberof, Parameters: parameters, Deleted: roldeleted} } return users, nil diff --git a/pkg/cluster/sync.go b/pkg/cluster/sync.go index 3036a9942..e987e744b 100644 --- a/pkg/cluster/sync.go +++ b/pkg/cluster/sync.go @@ -551,10 +551,29 @@ func (c *Cluster) syncRoles() (err error) { } }() + // mapping between original role name and with deletion suffix + deletedUsers := map[string]string{} + + // create list of database roles to query for _, u := range c.pgUsers { userNames = append(userNames, u.Name) + // add team member role name with rename suffix in case we need to rename it back + if u.Origin == spec.RoleOriginTeamsAPI && c.OpConfig.EnableTeamMemberDeprecation { + deletedUsers[u.Name+c.OpConfig.RoleDeletionSuffix] = u.Name + userNames = append(userNames, u.Name+c.OpConfig.RoleDeletionSuffix) + } + } + + // add team members that exist only in cache + // to trigger a rename of the role in ProduceSyncRequests + for _, cachedUser := range c.pgUsersCache { + if _, exists := c.pgUsers[cachedUser.Name]; !exists { + userNames = append(userNames, cachedUser.Name) + } } + // add pooler user to list of pgUsers, too + // to check if the pooler user exists or has to be created if needMasterConnectionPooler(&c.Spec) || needReplicaConnectionPooler(&c.Spec) { connectionPoolerUser := c.systemUsers[constants.ConnectionPoolerUserKeyName] userNames = append(userNames, connectionPoolerUser.Name) @@ -569,6 +588,16 @@ func (c *Cluster) syncRoles() (err error) { return fmt.Errorf("error getting users from the database: %v", err) } + // update pgUsers where a deleted role was found + // so that they are skipped in ProduceSyncRequests + for _, dbUser := range dbUsers { + if originalUser, exists := deletedUsers[dbUser.Name]; exists { + recreatedUser := c.pgUsers[originalUser] + recreatedUser.Deleted = true + c.pgUsers[originalUser] = recreatedUser + } + } + pgSyncRequests := c.userSyncStrategy.ProduceSyncRequests(dbUsers, c.pgUsers) if err = c.userSyncStrategy.ExecuteSyncRequests(pgSyncRequests, c.pgDb); err != nil { return fmt.Errorf("error executing sync statements: %v", err) diff --git a/pkg/cluster/util.go b/pkg/cluster/util.go index fabc6b216..6acecc38d 100644 --- a/pkg/cluster/util.go +++ b/pkg/cluster/util.go @@ -242,7 +242,7 @@ func (c *Cluster) getTeamMembers(teamID string) ([]string, error) { for team, membership := range *c.Config.PgTeamMap { if team == teamID { additionalMembers = membership.AdditionalMembers - c.logger.Debugf("found %d additional members for team %q", len(members), teamID) + c.logger.Debugf("found %d additional members for team %q", len(additionalMembers), teamID) } } @@ -256,14 +256,12 @@ func (c *Cluster) getTeamMembers(teamID string) ([]string, error) { token, err := c.oauthTokenGetter.getOAuthToken() if err != nil { - c.logger.Warnf("could not get oauth token to authenticate to team service API, only returning %d members for team %q: %v", len(members), teamID, err) - return members, nil + return nil, fmt.Errorf("could not get oauth token to authenticate to team service API: %v", err) } teamInfo, err := c.teamsAPIClient.TeamInfo(teamID, token) if err != nil { - c.logger.Warnf("could not get team info for team %q, only returning %d members: %v", teamID, len(members), err) - return members, nil + return nil, fmt.Errorf("could not get team info for team %q: %v", teamID, err) } for _, member := range teamInfo.Members { diff --git a/pkg/controller/operator_config.go b/pkg/controller/operator_config.go index 3c0302cab..fbec7a462 100644 --- a/pkg/controller/operator_config.go +++ b/pkg/controller/operator_config.go @@ -180,6 +180,8 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur result.PostgresSuperuserTeams = fromCRD.TeamsAPI.PostgresSuperuserTeams result.EnablePostgresTeamCRD = fromCRD.TeamsAPI.EnablePostgresTeamCRD result.EnablePostgresTeamCRDSuperusers = fromCRD.TeamsAPI.EnablePostgresTeamCRDSuperusers + result.EnableTeamMemberDeprecation = fromCRD.TeamsAPI.EnableTeamMemberDeprecation + result.RoleDeletionSuffix = util.Coalesce(fromCRD.TeamsAPI.RoleDeletionSuffix, "_deleted") // logging REST API config result.APIPort = util.CoalesceInt(fromCRD.LoggingRESTAPI.APIPort, 8080) diff --git a/pkg/spec/types.go b/pkg/spec/types.go index 78c79e1b3..5d7794b42 100644 --- a/pkg/spec/types.go +++ b/pkg/spec/types.go @@ -42,6 +42,7 @@ const ( PGSyncUserAdd = iota PGsyncUserAlter PGSyncAlterSet // handle ALTER ROLE SET parameter = value + PGSyncUserRename ) // PgUser contains information about a single user. @@ -53,6 +54,7 @@ type PgUser struct { MemberOf []string `yaml:"inrole"` Parameters map[string]string `yaml:"db_parameters"` AdminRole string `yaml:"admin_role"` + Deleted bool `yaml:"deleted"` } func (user *PgUser) Valid() bool { diff --git a/pkg/teams/postgres_team_test.go b/pkg/teams/postgres_team_test.go index f138ec124..dec020c7d 100644 --- a/pkg/teams/postgres_team_test.go +++ b/pkg/teams/postgres_team_test.go @@ -9,8 +9,6 @@ import ( ) var ( - True = true - False = false pgTeamList = acidv1.PostgresTeamList{ TypeMeta: metav1.TypeMeta{ Kind: "List", diff --git a/pkg/util/config/config.go b/pkg/util/config/config.go index a5d144051..c8430e7a6 100644 --- a/pkg/util/config/config.go +++ b/pkg/util/config/config.go @@ -176,6 +176,8 @@ type Config struct { EnableTeamsAPI bool `name:"enable_teams_api" default:"true"` EnableTeamSuperuser bool `name:"enable_team_superuser" default:"false"` TeamAdminRole string `name:"team_admin_role" default:"admin"` + RoleDeletionSuffix string `name:"role_deletion_suffix" default:"_deleted"` + EnableTeamMemberDeprecation bool `name:"enable_team_member_deprecation" default:"false"` EnableAdminRoleForUsers bool `name:"enable_admin_role_for_users" default:"true"` EnablePostgresTeamCRD bool `name:"enable_postgres_team_crd" default:"false"` EnablePostgresTeamCRDSuperusers bool `name:"enable_postgres_team_crd_superusers" default:"false"` diff --git a/pkg/util/users/users.go b/pkg/util/users/users.go index 231cf2a89..3da933644 100644 --- a/pkg/util/users/users.go +++ b/pkg/util/users/users.go @@ -9,11 +9,13 @@ import ( "github.com/zalando/postgres-operator/pkg/spec" "github.com/zalando/postgres-operator/pkg/util" + "github.com/zalando/postgres-operator/pkg/util/constants" ) const ( createUserSQL = `SET LOCAL synchronous_commit = 'local'; CREATE ROLE "%s" %s %s;` alterUserSQL = `ALTER ROLE "%s" %s` + alterUserRenameSQL = `ALTER ROLE "%s" RENAME TO "%s%s"` alterRoleResetAllSQL = `ALTER ROLE "%s" RESET ALL` alterRoleSetSQL = `ALTER ROLE "%s" SET %s TO %s` grantToUserSQL = `GRANT %s TO "%s"` @@ -29,6 +31,7 @@ const ( // (except for the NOLOGIN). TODO: process other NOflags, i.e. NOSUPERUSER correctly. type DefaultUserSyncStrategy struct { PasswordEncryption string + RoleDeletionSuffix string } // ProduceSyncRequests figures out the types of changes that need to happen with the given users. @@ -36,8 +39,11 @@ func (strategy DefaultUserSyncStrategy) ProduceSyncRequests(dbUsers spec.PgUserM newUsers spec.PgUserMap) []spec.PgSyncUserRequest { var reqs []spec.PgSyncUserRequest - // No existing roles are deleted or stripped of role memebership/flags for name, newUser := range newUsers { + // do not create user that exists in DB with deletion suffix + if newUser.Deleted { + continue + } dbUser, exists := dbUsers[name] if !exists { reqs = append(reqs, spec.PgSyncUserRequest{Kind: spec.PGSyncUserAdd, User: newUser}) @@ -70,6 +76,25 @@ func (strategy DefaultUserSyncStrategy) ProduceSyncRequests(dbUsers spec.PgUserM } } + // No existing roles are deleted or stripped of role membership/flags + // but team roles will be renamed and denied from LOGIN + for name, dbUser := range dbUsers { + if _, exists := newUsers[name]; !exists { + // toggle LOGIN flag based on role deletion + userFlags := make([]string, len(dbUser.Flags)) + userFlags = append(userFlags, dbUser.Flags...) + if dbUser.Deleted { + dbUser.Flags = util.StringSliceReplaceElement(dbUser.Flags, constants.RoleFlagNoLogin, constants.RoleFlagLogin) + } else { + dbUser.Flags = util.StringSliceReplaceElement(dbUser.Flags, constants.RoleFlagLogin, constants.RoleFlagNoLogin) + } + if !util.IsEqualIgnoreOrder(userFlags, dbUser.Flags) { + reqs = append(reqs, spec.PgSyncUserRequest{Kind: spec.PGsyncUserAlter, User: dbUser}) + } + + reqs = append(reqs, spec.PgSyncUserRequest{Kind: spec.PGSyncUserRename, User: dbUser}) + } + } return reqs } @@ -94,6 +119,11 @@ func (strategy DefaultUserSyncStrategy) ExecuteSyncRequests(requests []spec.PgSy reqretries = append(reqretries, request) errors = append(errors, fmt.Sprintf("could not set custom user %q parameters: %v", request.User.Name, err)) } + case spec.PGSyncUserRename: + if err := strategy.alterPgUserRename(request.User, db); err != nil { + reqretries = append(reqretries, request) + errors = append(errors, fmt.Sprintf("could not rename custom user %q: %v", request.User.Name, err)) + } default: return fmt.Errorf("unrecognized operation: %v", request.Kind) } @@ -124,6 +154,23 @@ func (strategy DefaultUserSyncStrategy) alterPgUserSet(user spec.PgUser, db *sql return nil } +func (strategy DefaultUserSyncStrategy) alterPgUserRename(user spec.PgUser, db *sql.DB) error { + var query string + + // append or trim deletion suffix depending if the user has the suffix or not + if user.Deleted { + newName := strings.TrimSuffix(user.Name, strategy.RoleDeletionSuffix) + query = fmt.Sprintf(alterUserRenameSQL, user.Name, newName, "") + } else { + query = fmt.Sprintf(alterUserRenameSQL, user.Name, user.Name, strategy.RoleDeletionSuffix) + } + + if _, err := db.Exec(query); err != nil { + return err + } + return nil +} + func (strategy DefaultUserSyncStrategy) createPgUser(user spec.PgUser, db *sql.DB) error { var userFlags []string var userPassword string diff --git a/pkg/util/util.go b/pkg/util/util.go index bebb9f8da..a52925583 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -151,6 +151,18 @@ func IsEqualIgnoreOrder(a, b []string) bool { return reflect.DeepEqual(a_copy, b_copy) } +// SliceReplaceElement +func StringSliceReplaceElement(s []string, a, b string) (result []string) { + tmp := make([]string, 0, len(s)) + for _, str := range s { + if str == a { + str = b + } + tmp = append(tmp, str) + } + return tmp +} + // SubstractStringSlices finds elements in a that are not in b and return them as a result slice. func SubstractStringSlices(a []string, b []string) (result []string, equal bool) { // Slices are assumed to contain unique elements only diff --git a/pkg/util/util_test.go b/pkg/util/util_test.go index c02d2c075..75853c3d6 100644 --- a/pkg/util/util_test.go +++ b/pkg/util/util_test.go @@ -166,6 +166,14 @@ func TestIsEqualIgnoreOrder(t *testing.T) { } } +func TestStringSliceReplaceElement(t *testing.T) { + testSlice := []string{"a", "b", "c"} + testSlice = StringSliceReplaceElement(testSlice, "b", "d") + if !SliceContains(testSlice, "d") { + t.Errorf("testSlide item not replaced: %v", testSlice) + } +} + func TestSubstractSlices(t *testing.T) { for _, tt := range substractTest { actualRes, actualEqual := SubstractStringSlices(tt.inA, tt.inB)