Rename roles that are removed from PostgresTeam CRD (#1457)
* rename db roles that are removed from manifests * extend PostgresTeam e2e test * make suffix configurable and add deprecated field to pgUser struct * deny LOGIN from deprecated roles * update feature documentation
This commit is contained in:
		
							parent
							
								
									7a8dc6084d
								
							
						
					
					
						commit
						eeb59c5bfd
					
				|  | @ -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" | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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. | ||||
|  |  | |||
							
								
								
									
										17
									
								
								docs/user.md
								
								
								
								
							
							
						
						
									
										17
									
								
								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 | ||||
|  |  | |||
|  | @ -197,7 +197,9 @@ class EndToEndTestCase(unittest.TestCase): | |||
|         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) | ||||
|  | @ -222,18 +224,60 @@ class EndToEndTestCase(unittest.TestCase): | |||
|             } | ||||
|         }) | ||||
| 
 | ||||
|         # 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'); | ||||
|         """ | ||||
|         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,  | ||||
|             "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); | ||||
|         """ | ||||
|         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 @@ class EndToEndTestCase(unittest.TestCase): | |||
| 
 | ||||
|         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 @@ class EndToEndTestCase(unittest.TestCase): | |||
|                             "Parameters": None, | ||||
|                             "AdminRole": "", | ||||
|                             "Origin": 2, | ||||
|                             "Deleted": False | ||||
|                         }) | ||||
|                         return True | ||||
|                 except: | ||||
|  | @ -1417,7 +1462,7 @@ class EndToEndTestCase(unittest.TestCase): | |||
|         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: | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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", | ||||
| 							}, | ||||
|  |  | |||
|  | @ -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
 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -9,8 +9,6 @@ import ( | |||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	True       = true | ||||
| 	False      = false | ||||
| 	pgTeamList = acidv1.PostgresTeamList{ | ||||
| 		TypeMeta: metav1.TypeMeta{ | ||||
| 			Kind:       "List", | ||||
|  |  | |||
|  | @ -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"` | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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
 | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue