mirror of
https://github.com/zalando/postgres-operator.git
synced 2026-09-30 09:21:33 +02:00
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:
@@ -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
|
||||
|
||||
+16
-2
@@ -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)
|
||||
|
||||
+3
-5
@@ -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"`
|
||||
|
||||
+48
-1
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user