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:
Felix Kunde 2021-05-21 15:49:39 +02:00 committed by GitHub
parent 7a8dc6084d
commit eeb59c5bfd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 262 additions and 49 deletions

View File

@ -443,6 +443,9 @@ spec:
enable_postgres_team_crd_superusers: enable_postgres_team_crd_superusers:
type: boolean type: boolean
default: false default: false
enable_team_member_deprecation:
type: boolean
default: false
enable_team_superuser: enable_team_superuser:
type: boolean type: boolean
default: false default: false
@ -465,6 +468,9 @@ spec:
type: string type: string
default: default:
- admin - admin
role_deletion_suffix:
type: string
default: "_deleted"
team_admin_role: team_admin_role:
type: string type: string
default: "admin" default: "admin"

View File

@ -289,13 +289,13 @@ configLogicalBackup:
# automate creation of human users with teams API service # automate creation of human users with teams API service
configTeamsApi: configTeamsApi:
# team_admin_role will have the rights to grant roles coming from PG manifests # 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 # operator watches for PostgresTeam CRs to assign additional teams and members to clusters
enable_postgres_team_crd: false enable_postgres_team_crd: false
# toogle to create additional superuser teams from PostgresTeam CRs # 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 # 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 # 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 # 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 # List of teams which members need the superuser role in each Postgres cluster
# postgres_superuser_teams: postgres_superuser_teams:
# - postgres_superusers - postgres_superusers
# List of roles that cannot be overwritten by an application, team or infrastructure role # List of roles that cannot be overwritten by an application, team or infrastructure role
protected_role_names: protected_role_names:
- admin - 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 # 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 # postgres config parameters to apply to each team member role

View File

@ -280,36 +280,32 @@ configLogicalBackup:
# automate creation of human users with teams API service # automate creation of human users with teams API service
configTeamsApi: configTeamsApi:
# team_admin_role will have the rights to grant roles coming from PG manifests # 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 # operator watches for PostgresTeam CRs to assign additional teams and members to clusters
enable_postgres_team_crd: "false" enable_postgres_team_crd: "false"
# toogle to create additional superuser teams from PostgresTeam CRs # 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 # 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 # toggles usage of the Teams API by the operator
enable_teams_api: "false" enable_teams_api: "false"
# should contain a URL to use for authentication (username and token) # should contain a URL to use for authentication (username and token)
# pam_configuration: https://info.example.com/oauth2/tokeninfo?access_token= uid realm=/employees # 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 # 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 # 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 # 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 # 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 # 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 # URL of the Teams API service
# teams_api_url: http://fake-teams-api.default.svc.cluster.local # teams_api_url: http://fake-teams-api.default.svc.cluster.local

View File

@ -704,6 +704,19 @@ key.
cluster to administer Postgres and maintain infrastructure built around it. cluster to administer Postgres and maintain infrastructure built around it.
The default is empty. 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** * **enable_postgres_team_crd**
toggle to make the operator watch for created or updated `PostgresTeam` CRDs toggle to make the operator watch for created or updated `PostgresTeam` CRDs
and create roles for specified additional teams and members. and create roles for specified additional teams and members.

View File

@ -407,6 +407,23 @@ spec:
- "briggs" - "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 ## Prepared databases with roles and default privileges
The `users` section in the manifests only allows for creating database roles The `users` section in the manifests only allows for creating database roles

View File

@ -197,13 +197,15 @@ class EndToEndTestCase(unittest.TestCase):
enable_postgres_team_crd = { enable_postgres_team_crd = {
"data": { "data": {
"enable_postgres_team_crd": "true", "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.k8s.update_config(enable_postgres_team_crd)
self.eventuallyEqual(lambda: self.k8s.get_operator_state(), {"0": "idle"}, self.eventuallyEqual(lambda: self.k8s.get_operator_state(), {"0": "idle"},
"Operator does not get in sync") "Operator does not get in sync")
self.k8s.api.custom_objects_api.patch_namespaced_custom_object( self.k8s.api.custom_objects_api.patch_namespaced_custom_object(
'acid.zalan.do', 'v1', 'default', 'acid.zalan.do', 'v1', 'default',
'postgresteams', 'custom-team-membership', 'postgresteams', 'custom-team-membership',
@ -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() leader = self.k8s.get_cluster_leader_pod()
user_query = """ user_query = """
SELECT usename SELECT rolname
FROM pg_catalog.pg_user FROM pg_catalog.pg_roles
WHERE usename IN ('elephant', 'kind'); WHERE rolname IN ('elephant', 'kind');
""" """
users = self.query_database(leader.metadata.name, "postgres", user_query) self.eventuallyEqual(lambda: len(self.query_database(leader.metadata.name, "postgres", user_query)), 2,
self.eventuallyEqual(lambda: len(users), 2, "Not all additional users found in database", 10, 5)
"Not all additional users found in database: {}".format(users))
# 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 config change
revert_resync = { revert_resync = {
@ -407,9 +451,9 @@ class EndToEndTestCase(unittest.TestCase):
leader = k8s.get_cluster_leader_pod() leader = k8s.get_cluster_leader_pod()
schemas_query = """ schemas_query = """
select schema_name SELECT schema_name
from information_schema.schemata FROM information_schema.schemata
where schema_name = 'pooler' WHERE schema_name = 'pooler'
""" """
db_list = self.list_databases(leader.metadata.name) db_list = self.list_databases(leader.metadata.name)
@ -529,6 +573,7 @@ class EndToEndTestCase(unittest.TestCase):
"Parameters": None, "Parameters": None,
"AdminRole": "", "AdminRole": "",
"Origin": 2, "Origin": 2,
"Deleted": False
}) })
return True return True
except: except:
@ -1417,7 +1462,7 @@ class EndToEndTestCase(unittest.TestCase):
k8s = self.k8s k8s = self.k8s
result_set = [] result_set = []
db_list = [] 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 {}" exec_query = r"psql -tAq -c \"{}\" -d {}"
try: try:

View File

@ -51,6 +51,7 @@ data:
# enable_shm_volume: "true" # enable_shm_volume: "true"
# enable_sidecars: "true" # enable_sidecars: "true"
enable_spilo_wal_path_compat: "true" enable_spilo_wal_path_compat: "true"
enable_team_member_deprecation: "false"
# enable_team_superuser: "false" # enable_team_superuser: "false"
enable_teams_api: "false" enable_teams_api: "false"
# etcd_host: "" # etcd_host: ""
@ -111,6 +112,7 @@ data:
resource_check_timeout: 10m resource_check_timeout: 10m
resync_period: 30m resync_period: 30m
ring_log_lines: "100" ring_log_lines: "100"
role_deletion_suffix: "_deleted"
secret_name_template: "{username}.{cluster}.credentials" secret_name_template: "{username}.{cluster}.credentials"
# sidecar_docker_images: "" # sidecar_docker_images: ""
# set_memory_request_to_limit: "false" # set_memory_request_to_limit: "false"

View File

@ -439,6 +439,9 @@ spec:
enable_postgres_team_crd_superusers: enable_postgres_team_crd_superusers:
type: boolean type: boolean
default: false default: false
enable_team_member_deprecation:
type: boolean
default: false
enable_team_superuser: enable_team_superuser:
type: boolean type: boolean
default: false default: false
@ -461,6 +464,9 @@ spec:
type: string type: string
default: default:
- admin - admin
role_deletion_suffix:
type: string
default: "_deleted"
team_admin_role: team_admin_role:
type: string type: string
default: "admin" default: "admin"

View File

@ -141,6 +141,7 @@ configuration:
# enable_admin_role_for_users: true # enable_admin_role_for_users: true
# enable_postgres_team_crd: false # enable_postgres_team_crd: false
# enable_postgres_team_crd_superusers: false # enable_postgres_team_crd_superusers: false
enable_team_member_deprecation: false
enable_team_superuser: false enable_team_superuser: false
enable_teams_api: false enable_teams_api: false
# pam_configuration: "" # pam_configuration: ""
@ -149,6 +150,7 @@ configuration:
# - postgres_superusers # - postgres_superusers
protected_role_names: protected_role_names:
- admin - admin
role_deletion_suffix: "_deleted"
team_admin_role: admin team_admin_role: admin
team_api_role_configuration: team_api_role_configuration:
log_statement: all log_statement: all

View File

@ -1377,6 +1377,9 @@ var OperatorConfigCRDResourceValidation = apiextv1.CustomResourceValidation{
"enable_postgres_team_crd_superusers": { "enable_postgres_team_crd_superusers": {
Type: "boolean", Type: "boolean",
}, },
"enable_team_member_deprecation": {
Type: "boolean",
},
"enable_team_superuser": { "enable_team_superuser": {
Type: "boolean", Type: "boolean",
}, },
@ -1405,6 +1408,9 @@ var OperatorConfigCRDResourceValidation = apiextv1.CustomResourceValidation{
}, },
}, },
}, },
"role_deletion_suffix": {
Type: "string",
},
"team_admin_role": { "team_admin_role": {
Type: "string", Type: "string",
}, },

View File

@ -159,6 +159,8 @@ type TeamsAPIConfiguration struct {
PostgresSuperuserTeams []string `json:"postgres_superuser_teams,omitempty"` PostgresSuperuserTeams []string `json:"postgres_superuser_teams,omitempty"`
EnablePostgresTeamCRD bool `json:"enable_postgres_team_crd,omitempty"` EnablePostgresTeamCRD bool `json:"enable_postgres_team_crd,omitempty"`
EnablePostgresTeamCRDSuperusers bool `json:"enable_postgres_team_crd_superusers,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 // LoggingRESTAPIConfiguration defines Logging API conf

View File

@ -74,6 +74,7 @@ type Cluster struct {
eventRecorder record.EventRecorder eventRecorder record.EventRecorder
patroni patroni.Interface patroni patroni.Interface
pgUsers map[string]spec.PgUser pgUsers map[string]spec.PgUser
pgUsersCache map[string]spec.PgUser
systemUsers map[string]spec.PgUser systemUsers map[string]spec.PgUser
podSubscribers map[spec.NamespacedName]chan PodEvent podSubscribers map[spec.NamespacedName]chan PodEvent
podSubscribersMu sync.RWMutex podSubscribersMu sync.RWMutex
@ -129,7 +130,9 @@ func New(cfg Config, kubeClient k8sutil.KubernetesClient, pgSpec acidv1.Postgres
Secrets: make(map[types.UID]*v1.Secret), Secrets: make(map[types.UID]*v1.Secret),
Services: make(map[PostgresRole]*v1.Service), Services: make(map[PostgresRole]*v1.Service),
Endpoints: make(map[PostgresRole]*v1.Endpoints)}, 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}, deleteOptions: metav1.DeleteOptions{PropagationPolicy: &deletePropagationPolicy},
podEventsQueue: podEventsQueue, podEventsQueue: podEventsQueue,
KubeClient: kubeClient, KubeClient: kubeClient,
@ -190,6 +193,17 @@ func (c *Cluster) isNewCluster() bool {
func (c *Cluster) initUsers() error { func (c *Cluster) initUsers() error {
c.setProcessName("initializing users") 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 // clear our the previous state of the cluster users (in case we are
// running a sync). // running a sync).
c.systemUsers = map[string]spec.PgUser{} c.systemUsers = map[string]spec.PgUser{}
@ -650,7 +664,7 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error {
needConnectionPooler := needMasterConnectionPoolerWorker(&newSpec.Spec) || needConnectionPooler := needMasterConnectionPoolerWorker(&newSpec.Spec) ||
needReplicaConnectionPoolerWorker(&newSpec.Spec) needReplicaConnectionPoolerWorker(&newSpec.Spec)
if !sameUsers || needConnectionPooler { if !sameUsers || needConnectionPooler {
c.logger.Debugf("syncing secrets") c.logger.Debugf("initialize users")
if err := c.initUsers(); err != nil { if err := c.initUsers(); err != nil {
c.logger.Errorf("could not init users: %v", err) c.logger.Errorf("could not init users: %v", err)
updateFailed = true updateFailed = true

View File

@ -198,6 +198,7 @@ func (c *Cluster) readPgUsersFromDatabase(userNames []string) (users spec.PgUser
rolname, rolpassword string rolname, rolpassword string
rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin bool rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin bool
roloptions, memberof []string roloptions, memberof []string
roldeleted bool
) )
err := rows.Scan(&rolname, &rolpassword, &rolsuper, &rolinherit, err := rows.Scan(&rolname, &rolpassword, &rolsuper, &rolinherit,
&rolcreaterole, &rolcreatedb, &rolcanlogin, pq.Array(&roloptions), pq.Array(&memberof)) &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] 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 return users, nil

View File

@ -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 { for _, u := range c.pgUsers {
userNames = append(userNames, u.Name) 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) { if needMasterConnectionPooler(&c.Spec) || needReplicaConnectionPooler(&c.Spec) {
connectionPoolerUser := c.systemUsers[constants.ConnectionPoolerUserKeyName] connectionPoolerUser := c.systemUsers[constants.ConnectionPoolerUserKeyName]
userNames = append(userNames, connectionPoolerUser.Name) 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) 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) pgSyncRequests := c.userSyncStrategy.ProduceSyncRequests(dbUsers, c.pgUsers)
if err = c.userSyncStrategy.ExecuteSyncRequests(pgSyncRequests, c.pgDb); err != nil { if err = c.userSyncStrategy.ExecuteSyncRequests(pgSyncRequests, c.pgDb); err != nil {
return fmt.Errorf("error executing sync statements: %v", err) return fmt.Errorf("error executing sync statements: %v", err)

View File

@ -242,7 +242,7 @@ func (c *Cluster) getTeamMembers(teamID string) ([]string, error) {
for team, membership := range *c.Config.PgTeamMap { for team, membership := range *c.Config.PgTeamMap {
if team == teamID { if team == teamID {
additionalMembers = membership.AdditionalMembers 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() token, err := c.oauthTokenGetter.getOAuthToken()
if err != nil { 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 nil, fmt.Errorf("could not get oauth token to authenticate to team service API: %v", err)
return members, nil
} }
teamInfo, err := c.teamsAPIClient.TeamInfo(teamID, token) teamInfo, err := c.teamsAPIClient.TeamInfo(teamID, token)
if err != nil { if err != nil {
c.logger.Warnf("could not get team info for team %q, only returning %d members: %v", teamID, len(members), err) return nil, fmt.Errorf("could not get team info for team %q: %v", teamID, err)
return members, nil
} }
for _, member := range teamInfo.Members { for _, member := range teamInfo.Members {

View File

@ -180,6 +180,8 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
result.PostgresSuperuserTeams = fromCRD.TeamsAPI.PostgresSuperuserTeams result.PostgresSuperuserTeams = fromCRD.TeamsAPI.PostgresSuperuserTeams
result.EnablePostgresTeamCRD = fromCRD.TeamsAPI.EnablePostgresTeamCRD result.EnablePostgresTeamCRD = fromCRD.TeamsAPI.EnablePostgresTeamCRD
result.EnablePostgresTeamCRDSuperusers = fromCRD.TeamsAPI.EnablePostgresTeamCRDSuperusers result.EnablePostgresTeamCRDSuperusers = fromCRD.TeamsAPI.EnablePostgresTeamCRDSuperusers
result.EnableTeamMemberDeprecation = fromCRD.TeamsAPI.EnableTeamMemberDeprecation
result.RoleDeletionSuffix = util.Coalesce(fromCRD.TeamsAPI.RoleDeletionSuffix, "_deleted")
// logging REST API config // logging REST API config
result.APIPort = util.CoalesceInt(fromCRD.LoggingRESTAPI.APIPort, 8080) result.APIPort = util.CoalesceInt(fromCRD.LoggingRESTAPI.APIPort, 8080)

View File

@ -42,6 +42,7 @@ const (
PGSyncUserAdd = iota PGSyncUserAdd = iota
PGsyncUserAlter PGsyncUserAlter
PGSyncAlterSet // handle ALTER ROLE SET parameter = value PGSyncAlterSet // handle ALTER ROLE SET parameter = value
PGSyncUserRename
) )
// PgUser contains information about a single user. // PgUser contains information about a single user.
@ -53,6 +54,7 @@ type PgUser struct {
MemberOf []string `yaml:"inrole"` MemberOf []string `yaml:"inrole"`
Parameters map[string]string `yaml:"db_parameters"` Parameters map[string]string `yaml:"db_parameters"`
AdminRole string `yaml:"admin_role"` AdminRole string `yaml:"admin_role"`
Deleted bool `yaml:"deleted"`
} }
func (user *PgUser) Valid() bool { func (user *PgUser) Valid() bool {

View File

@ -9,8 +9,6 @@ import (
) )
var ( var (
True = true
False = false
pgTeamList = acidv1.PostgresTeamList{ pgTeamList = acidv1.PostgresTeamList{
TypeMeta: metav1.TypeMeta{ TypeMeta: metav1.TypeMeta{
Kind: "List", Kind: "List",

View File

@ -176,6 +176,8 @@ type Config struct {
EnableTeamsAPI bool `name:"enable_teams_api" default:"true"` EnableTeamsAPI bool `name:"enable_teams_api" default:"true"`
EnableTeamSuperuser bool `name:"enable_team_superuser" default:"false"` EnableTeamSuperuser bool `name:"enable_team_superuser" default:"false"`
TeamAdminRole string `name:"team_admin_role" default:"admin"` 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"` EnableAdminRoleForUsers bool `name:"enable_admin_role_for_users" default:"true"`
EnablePostgresTeamCRD bool `name:"enable_postgres_team_crd" default:"false"` EnablePostgresTeamCRD bool `name:"enable_postgres_team_crd" default:"false"`
EnablePostgresTeamCRDSuperusers bool `name:"enable_postgres_team_crd_superusers" default:"false"` EnablePostgresTeamCRDSuperusers bool `name:"enable_postgres_team_crd_superusers" default:"false"`

View File

@ -9,11 +9,13 @@ import (
"github.com/zalando/postgres-operator/pkg/spec" "github.com/zalando/postgres-operator/pkg/spec"
"github.com/zalando/postgres-operator/pkg/util" "github.com/zalando/postgres-operator/pkg/util"
"github.com/zalando/postgres-operator/pkg/util/constants"
) )
const ( const (
createUserSQL = `SET LOCAL synchronous_commit = 'local'; CREATE ROLE "%s" %s %s;` createUserSQL = `SET LOCAL synchronous_commit = 'local'; CREATE ROLE "%s" %s %s;`
alterUserSQL = `ALTER ROLE "%s" %s` alterUserSQL = `ALTER ROLE "%s" %s`
alterUserRenameSQL = `ALTER ROLE "%s" RENAME TO "%s%s"`
alterRoleResetAllSQL = `ALTER ROLE "%s" RESET ALL` alterRoleResetAllSQL = `ALTER ROLE "%s" RESET ALL`
alterRoleSetSQL = `ALTER ROLE "%s" SET %s TO %s` alterRoleSetSQL = `ALTER ROLE "%s" SET %s TO %s`
grantToUserSQL = `GRANT %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. // (except for the NOLOGIN). TODO: process other NOflags, i.e. NOSUPERUSER correctly.
type DefaultUserSyncStrategy struct { type DefaultUserSyncStrategy struct {
PasswordEncryption string PasswordEncryption string
RoleDeletionSuffix string
} }
// ProduceSyncRequests figures out the types of changes that need to happen with the given users. // 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 { newUsers spec.PgUserMap) []spec.PgSyncUserRequest {
var reqs []spec.PgSyncUserRequest var reqs []spec.PgSyncUserRequest
// No existing roles are deleted or stripped of role memebership/flags
for name, newUser := range newUsers { for name, newUser := range newUsers {
// do not create user that exists in DB with deletion suffix
if newUser.Deleted {
continue
}
dbUser, exists := dbUsers[name] dbUser, exists := dbUsers[name]
if !exists { if !exists {
reqs = append(reqs, spec.PgSyncUserRequest{Kind: spec.PGSyncUserAdd, User: newUser}) 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 return reqs
} }
@ -94,6 +119,11 @@ func (strategy DefaultUserSyncStrategy) ExecuteSyncRequests(requests []spec.PgSy
reqretries = append(reqretries, request) reqretries = append(reqretries, request)
errors = append(errors, fmt.Sprintf("could not set custom user %q parameters: %v", request.User.Name, err)) 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: default:
return fmt.Errorf("unrecognized operation: %v", request.Kind) return fmt.Errorf("unrecognized operation: %v", request.Kind)
} }
@ -124,6 +154,23 @@ func (strategy DefaultUserSyncStrategy) alterPgUserSet(user spec.PgUser, db *sql
return nil 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 { func (strategy DefaultUserSyncStrategy) createPgUser(user spec.PgUser, db *sql.DB) error {
var userFlags []string var userFlags []string
var userPassword string var userPassword string

View File

@ -151,6 +151,18 @@ func IsEqualIgnoreOrder(a, b []string) bool {
return reflect.DeepEqual(a_copy, b_copy) 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. // 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) { func SubstractStringSlices(a []string, b []string) (result []string, equal bool) {
// Slices are assumed to contain unique elements only // Slices are assumed to contain unique elements only

View File

@ -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) { func TestSubstractSlices(t *testing.T) {
for _, tt := range substractTest { for _, tt := range substractTest {
actualRes, actualEqual := SubstractStringSlices(tt.inA, tt.inB) actualRes, actualEqual := SubstractStringSlices(tt.inA, tt.inB)