From 6983f444ede8ec0e56bd9065b9b807136164d77a Mon Sep 17 00:00:00 2001 From: Oleksii Kliukin Date: Tue, 9 May 2017 13:13:03 +0200 Subject: [PATCH] Periodically sync roles with the running clusters. (#102) The sync adds or alters database roles based on the roles defined in the cluster's TPR, Team API and operator's infrastructure roles. At the moment, roles are not deleted, as it would be dangerous for the robot roles in case TPR is misconfigured. In addition, ALTER ROLE does not remove role options, i.e. SUPERUSER or CREATEROLE, neither it removes role membership: only new options are added and new role membership is granted. So far, options like NOSUPERUSER and NOCREATEROLE won't be handed correctly, when mixed with the non-negative counterparts, also NOLOGIN should be processed correctly. The code assumes that only MD5 passwords are stored in the DB and will likely break with the new SCRAM auth in PostgreSQL 10. On the implementation side, create the new interface to abstract roles merge and creation, move most of the role-based functionality from cluster/pg into the new 'users' module, strip create user code of special cases related to human-based users (moving them to init instead) and fixed the password md5 generator to avoid processing already encrypted passwords. In addition, moved the system roles off the slice containing all other roles in order to avoid extra efforts to avoid creating them. Also, fix a leak in DB connections when the new connection is not considered healthy and discarded without being closed. Initialize the database during the sync phase before syncing users. --- pkg/cluster/cluster.go | 23 ++++- pkg/cluster/k8sres.go | 46 ++++++---- pkg/cluster/pg.go | 93 +++++++++++-------- pkg/cluster/resources.go | 47 +++++----- pkg/cluster/sync.go | 28 ++++++ pkg/cluster/util.go | 15 ++++ pkg/controller/postgresql.go | 2 +- pkg/controller/util.go | 2 +- pkg/spec/types.go | 23 ++++- pkg/util/constants/constants.go | 8 ++ pkg/util/users/users.go | 152 ++++++++++++++++++++++++++++++++ pkg/util/util.go | 26 +++++- 12 files changed, 378 insertions(+), 87 deletions(-) create mode 100644 pkg/util/users/users.go diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go index 56e5a3f7a..083bd08ef 100644 --- a/pkg/cluster/cluster.go +++ b/pkg/cluster/cluster.go @@ -26,6 +26,7 @@ import ( "github.bus.zalan.do/acid/postgres-operator/pkg/util/constants" "github.bus.zalan.do/acid/postgres-operator/pkg/util/k8sutil" "github.bus.zalan.do/acid/postgres-operator/pkg/util/teams" + "github.bus.zalan.do/acid/postgres-operator/pkg/util/users" ) var ( @@ -58,6 +59,7 @@ type Cluster struct { Config logger *logrus.Entry pgUsers map[string]spec.PgUser + systemUsers map[string]spec.PgUser podEvents chan spec.PodEvent podSubscribers map[spec.NamespacedName]chan spec.PodEvent podSubscribersMu sync.RWMutex @@ -65,6 +67,7 @@ type Cluster struct { mu sync.Mutex masterLess bool podDispatcherRunning bool + userSyncStrategy spec.UserSyncer deleteOptions *v1.DeleteOptions } @@ -78,11 +81,13 @@ func New(cfg Config, pgSpec spec.Postgresql, logger *logrus.Entry) *Cluster { Postgresql: pgSpec, logger: lg, pgUsers: make(map[string]spec.PgUser), + systemUsers: make(map[string]spec.PgUser), podEvents: make(chan spec.PodEvent), podSubscribers: make(map[spec.NamespacedName]chan spec.PodEvent), kubeResources: kubeResources, masterLess: false, podDispatcherRunning: false, + userSyncStrategy: users.DefaultUserSyncStrategy{}, deleteOptions: &v1.DeleteOptions{OrphanDependents: &orphanDependents}, } @@ -426,12 +431,15 @@ func (c *Cluster) ReceivePodEvent(event spec.PodEvent) { } func (c *Cluster) initSystemUsers() { - c.pgUsers[c.OpConfig.SuperUsername] = spec.PgUser{ + // We don't actually use that to create users, delegating this + // task to Patroni. Those definitions are only used to create + // secrets, therefore, setting flags like SUPERUSER or REPLICATION + // is not necessary here + c.systemUsers[constants.SuperuserKeyName] = spec.PgUser{ Name: c.OpConfig.SuperUsername, Password: util.RandomPassword(constants.PasswordLength), } - - c.pgUsers[c.OpConfig.ReplicationUsername] = spec.PgUser{ + c.systemUsers[constants.ReplicationUserKeyName] = spec.PgUser{ Name: c.OpConfig.ReplicationUsername, Password: util.RandomPassword(constants.PasswordLength), } @@ -464,7 +472,9 @@ func (c *Cluster) initHumanUsers() error { return fmt.Errorf("Can't get list of team members: %s", err) } else { for _, username := range teamMembers { - c.pgUsers[username] = spec.PgUser{Name: username} + flags := []string{constants.RoleFlagLogin, constants.RoleFlagSuperuser} + memberOf := []string{c.OpConfig.PamRoleName} + c.pgUsers[username] = spec.PgUser{Name: username, Flags: flags, MemberOf: memberOf} } } @@ -477,6 +487,11 @@ func (c *Cluster) initInfrastructureRoles() error { if !isValidUsername(username) { return fmt.Errorf("Invalid username: '%s'", username) } + if flags, err := normalizeUserFlags(data.Flags); err != nil { + return fmt.Errorf("Invalid flags for user '%s': %s", username, err) + } else { + data.Flags = flags + } c.pgUsers[username] = data } return nil diff --git a/pkg/cluster/k8sres.go b/pkg/cluster/k8sres.go index 43ddbe61d..14d0e1721 100644 --- a/pkg/cluster/k8sres.go +++ b/pkg/cluster/k8sres.go @@ -228,32 +228,48 @@ func persistentVolumeClaimTemplate(volumeSize, volumeStorageClass string) *v1.Pe return volumeClaim } -func (c *Cluster) genUserSecrets() (secrets map[string]*v1.Secret, err error) { +func (c *Cluster) genUserSecrets() (secrets map[string]*v1.Secret) { secrets = make(map[string]*v1.Secret, len(c.pgUsers)) namespace := c.Metadata.Namespace for username, pgUser := range c.pgUsers { //Skip users with no password i.e. human users (they'll be authenticated using pam) - if pgUser.Password == "" { - continue + secret := c.genSingleUserSecret(namespace, pgUser) + if secret != nil { + secrets[username] = secret } - secret := v1.Secret{ - ObjectMeta: v1.ObjectMeta{ - Name: c.credentialSecretName(username), - Namespace: namespace, - Labels: c.labelsSet(), - }, - Type: v1.SecretTypeOpaque, - Data: map[string][]byte{ - "username": []byte(pgUser.Name), - "password": []byte(pgUser.Password), - }, + } + /* special case for the system user */ + for _, systemUser := range c.systemUsers { + secret := c.genSingleUserSecret(namespace, systemUser) + if secret != nil { + secrets[systemUser.Name] = secret } - secrets[username] = &secret } return } +func (c *Cluster) genSingleUserSecret(namespace string, pgUser spec.PgUser) *v1.Secret { + //Skip users with no password i.e. human users (they'll be authenticated using pam) + if pgUser.Password == "" { + return nil + } + username := pgUser.Name + secret := v1.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: c.credentialSecretName(username), + Namespace: namespace, + Labels: c.labelsSet(), + }, + Type: v1.SecretTypeOpaque, + Data: map[string][]byte{ + "username": []byte(pgUser.Name), + "password": []byte(pgUser.Password), + }, + } + return &secret +} + func (c *Cluster) genService(allowedSourceRanges []string) *v1.Service { service := &v1.Service{ ObjectMeta: v1.ObjectMeta{ diff --git a/pkg/cluster/pg.go b/pkg/cluster/pg.go index 090479ad2..eb8882a5d 100644 --- a/pkg/cluster/pg.go +++ b/pkg/cluster/pg.go @@ -8,18 +8,28 @@ import ( _ "github.com/lib/pq" "github.bus.zalan.do/acid/postgres-operator/pkg/spec" - "github.bus.zalan.do/acid/postgres-operator/pkg/util" + "github.com/lib/pq" + "github.bus.zalan.do/acid/postgres-operator/pkg/util/constants" ) -var createUserSQL = `SET LOCAL synchronous_commit = 'local'; CREATE ROLE "%s" %s %s;` +var getUserSQL = `SELECT a.rolname, COALESCE(a.rolpassword, ''), a.rolsuper, a.rolinherit, + a.rolcreaterole, a.rolcreatedb, a.rolcanlogin, + ARRAY(SELECT b.rolname + FROM pg_catalog.pg_auth_members m + JOIN pg_catalog.pg_authid b ON (m.roleid = b.oid) + WHERE m.member = a.oid) as memberof + FROM pg_catalog.pg_authid a + WHERE a.rolname = ANY($1) + ORDER BY 1;` func (c *Cluster) pgConnectionString() string { hostname := fmt.Sprintf("%s.%s.svc.cluster.local", c.Metadata.Name, c.Metadata.Namespace) - password := c.pgUsers[c.OpConfig.SuperUsername].Password + username := c.systemUsers[constants.SuperuserKeyName].Name + password := c.systemUsers[constants.SuperuserKeyName].Password return fmt.Sprintf("host='%s' dbname=postgres sslmode=require user='%s' password='%s'", hostname, - c.OpConfig.SuperUsername, + username, strings.Replace(password, "$", "\\$", -1)) } @@ -33,6 +43,7 @@ func (c *Cluster) initDbConn() error { } err = conn.Ping() if err != nil { + conn.Close() return err } @@ -43,42 +54,48 @@ func (c *Cluster) initDbConn() error { return nil } -func (c *Cluster) createPgUser(user spec.PgUser) (isHuman bool, err error) { - var flags []string = user.Flags - - if user.Password == "" { - isHuman = true - flags = append(flags, "SUPERUSER") - flags = append(flags, fmt.Sprintf("IN ROLE \"%s\"", c.OpConfig.PamRoleName)) - } else { - isHuman = false +func (c *Cluster) readPgUsersFromDatabase(userNames []string) (users spec.PgUserMap, err error) { + var rows *sql.Rows + users = make(spec.PgUserMap) + if rows, err = c.pgDb.Query(getUserSQL, pq.Array(userNames)); err != nil { + return nil, fmt.Errorf("Error when querying users: %s", err) } - - addLoginFlag := true - for _, v := range flags { - if v == "NOLOGIN" { - addLoginFlag = false - break + defer rows.Close() + for rows.Next() { + var ( + rolname, rolpassword string + rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin bool + memberof []string + ) + err := rows.Scan(&rolname, &rolpassword, &rolsuper, &rolinherit, + &rolcreaterole, &rolcreatedb, &rolcanlogin, pq.Array(&memberof)) + if err != nil { + return nil, fmt.Errorf("Error when processing user rows: %s", err) } - } - if addLoginFlag { - flags = append(flags, "LOGIN") - } - if !isHuman && user.MemberOf != "" { - flags = append(flags, fmt.Sprintf("IN ROLE \"%s\"", user.MemberOf)) - } - userFlags := strings.Join(flags, " ") - userPassword := fmt.Sprintf("ENCRYPTED PASSWORD '%s'", util.PGUserPassword(user)) - if user.Password == "" { - userPassword = "PASSWORD NULL" - } - query := fmt.Sprintf(createUserSQL, user.Name, userFlags, userPassword) - - _, err = c.pgDb.Query(query) // TODO: Try several times - if err != nil { - err = fmt.Errorf("DB error: %s", err) - return + flags := makeUserFlags(rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin) + // XXX: the code assumes the password we get from pg_authid is always MD5 + users[rolname] = spec.PgUser{Name: rolname, Password: rolpassword, Flags: flags, MemberOf: memberof} } - return + return users, nil +} + +func makeUserFlags(rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin bool) (result []string) { + if rolsuper { + result = append(result, constants.RoleFlagSuperuser) + } + if rolinherit { + result = append(result, constants.RoleFlagInherit) + } + if rolcreaterole { + result = append(result, constants.RoleFlagCreateRole) + } + if rolcreatedb { + result = append(result, constants.RoleFlagCreateDB) + } + if rolcanlogin { + result = append(result, constants.RoleFlagLogin) + } + + return result } diff --git a/pkg/cluster/resources.go b/pkg/cluster/resources.go index b6eeef252..8a5cc1991 100644 --- a/pkg/cluster/resources.go +++ b/pkg/cluster/resources.go @@ -6,9 +6,11 @@ import ( "k8s.io/client-go/pkg/api" "k8s.io/client-go/pkg/api/v1" "k8s.io/client-go/pkg/apis/apps/v1beta1" - + "github.bus.zalan.do/acid/postgres-operator/pkg/util" "github.bus.zalan.do/acid/postgres-operator/pkg/util/k8sutil" + "github.bus.zalan.do/acid/postgres-operator/pkg/spec" + "github.bus.zalan.do/acid/postgres-operator/pkg/util/constants" ) func (c *Cluster) loadResources() error { @@ -183,7 +185,7 @@ func (c *Cluster) createService() (*v1.Service, error) { return service, nil } -func (c *Cluster) updateService(newService *v1.Service) error { +func (c *Cluster) updateService(newService *v1.Service) error { if c.Service == nil { return fmt.Errorf("There is no Service in the cluster") } @@ -262,23 +264,29 @@ func (c *Cluster) deleteEndpoint() error { } func (c *Cluster) applySecrets() error { - secrets, err := c.genUserSecrets() - - if err != nil { - return fmt.Errorf("Can't get user Secrets") - } + secrets := c.genUserSecrets() for secretUsername, secretSpec := range secrets { secret, err := c.KubeClient.Secrets(secretSpec.Namespace).Create(secretSpec) if k8sutil.ResourceAlreadyExists(err) { + var userMap map[string]spec.PgUser curSecret, err := c.KubeClient.Secrets(secretSpec.Namespace).Get(secretSpec.Name) if err != nil { return fmt.Errorf("Can't get current Secret: %s", err) } c.logger.Debugf("Secret '%s' already exists, fetching it's password", util.NameFromMeta(curSecret.ObjectMeta)) - pwdUser := c.pgUsers[secretUsername] + if secretUsername == c.systemUsers[constants.SuperuserKeyName].Name { + secretUsername = constants.SuperuserKeyName + userMap = c.systemUsers + } else if secretUsername == c.systemUsers[constants.ReplicationUserKeyName].Name { + secretUsername = constants.ReplicationUserKeyName + userMap = c.systemUsers + } else { + userMap = c.pgUsers + } + pwdUser := userMap[secretUsername] pwdUser.Password = string(curSecret.Data["password"]) - c.pgUsers[secretUsername] = pwdUser + userMap[secretUsername] = pwdUser continue } else { @@ -305,23 +313,12 @@ func (c *Cluster) deleteSecret(secret *v1.Secret) error { return err } -func (c *Cluster) createUsers() error { +func (c *Cluster) createUsers() (err error) { // TODO: figure out what to do with duplicate names (humans and robots) among pgUsers - for username, user := range c.pgUsers { - if username == c.OpConfig.SuperUsername || username == c.OpConfig.ReplicationUsername { - continue - } - - isHuman, err := c.createPgUser(user) - var userType string - if isHuman { - userType = "human" - } else { - userType = "robot" - } - if err != nil { - c.logger.Warnf("Can't create %s user '%s': %s", userType, username, err) - } + reqs := c.userSyncStrategy.ProduceSyncRequests(nil, c.pgUsers) + err = c.userSyncStrategy.ExecuteSyncRequests(reqs, c.pgDb) + if err != nil { + return err } return nil diff --git a/pkg/cluster/sync.go b/pkg/cluster/sync.go index 55b098ea9..69444444e 100644 --- a/pkg/cluster/sync.go +++ b/pkg/cluster/sync.go @@ -36,6 +36,14 @@ func (c *Cluster) SyncCluster(stopCh <-chan struct{}) { if err := c.syncStatefulSet(); err != nil { c.logger.Errorf("Can't sync StatefulSets: %s", err) } + if err := c.initDbConn(); err != nil { + c.logger.Errorf("Can't init db connection: %s", err) + } else { + c.logger.Debugf("Syncing Roles") + if err := c.SyncRoles(); err != nil { + c.logger.Errorf("Can't sync Roles: %s", err) + } + } } func (c *Cluster) syncSecrets() error { @@ -150,3 +158,23 @@ func (c *Cluster) syncStatefulSet() error { return nil } + +func (c *Cluster) SyncRoles() error { + var userNames []string + + if err := c.initUsers(); err != nil { + return err + } + for _, u := range c.pgUsers { + userNames = append(userNames, u.Name) + } + dbUsers, err := c.readPgUsersFromDatabase(userNames) + if err != nil { + return fmt.Errorf("Error getting users from the database: %s", err) + } + pgSyncRequests := c.userSyncStrategy.ProduceSyncRequests(dbUsers, c.pgUsers) + if err := c.userSyncStrategy.ExecuteSyncRequests(pgSyncRequests, c.pgDb); err != nil { + return fmt.Errorf("Error executing sync statements: %s", err) + } + return nil +} diff --git a/pkg/cluster/util.go b/pkg/cluster/util.go index 56596cc8b..d3190a56d 100644 --- a/pkg/cluster/util.go +++ b/pkg/cluster/util.go @@ -24,6 +24,7 @@ func isValidUsername(username string) bool { func normalizeUserFlags(userFlags []string) (flags []string, err error) { uniqueFlags := make(map[string]bool) + addLogin := true for _, flag := range userFlags { if !alphaNumericRegexp.MatchString(flag) { @@ -36,11 +37,25 @@ func normalizeUserFlags(userFlags []string) (flags []string, err error) { } } } + if uniqueFlags[constants.RoleFlagLogin] && uniqueFlags[constants.RoleFlagNoLogin] { + return nil, fmt.Errorf("Conflicting or redundant flags: LOGIN and NOLOGIN") + } flags = []string{} for k := range uniqueFlags { + if k == constants.RoleFlagNoLogin || k == constants.RoleFlagLogin { + addLogin = false + if k == constants.RoleFlagNoLogin { + // we don't add NOLOGIN to the list of flags to be consistent with what we get + // from the readPgUsersFromDatabase in SyncUsers + continue + } + } flags = append(flags, k) } + if addLogin { + flags = append(flags, constants.RoleFlagLogin) + } return } diff --git a/pkg/controller/postgresql.go b/pkg/controller/postgresql.go index 23956e6d3..e9be4c2be 100644 --- a/pkg/controller/postgresql.go +++ b/pkg/controller/postgresql.go @@ -159,7 +159,7 @@ func (c *Controller) processClusterEventsQueue(idx int) { func (c *Controller) queueClusterEvent(old, new *spec.Postgresql, eventType spec.EventType) { var ( - uid types.UID + uid types.UID clusterName spec.NamespacedName ) diff --git a/pkg/controller/util.go b/pkg/controller/util.go index 123e230cd..719d85e59 100644 --- a/pkg/controller/util.go +++ b/pkg/controller/util.go @@ -122,7 +122,7 @@ Users: case "password": t.Password = s case "inrole": - t.MemberOf = s + t.MemberOf = append(t.MemberOf, s) default: c.logger.Warnf("Unknown key %s", p) } diff --git a/pkg/spec/types.go b/pkg/spec/types.go index 3fbf7623b..e8a687f5f 100644 --- a/pkg/spec/types.go +++ b/pkg/spec/types.go @@ -1,6 +1,8 @@ package spec import ( + "database/sql" + "k8s.io/client-go/pkg/api/v1" "k8s.io/client-go/pkg/types" ) @@ -24,6 +26,13 @@ type ClusterEvent struct { WorkerID uint32 } +type SyncUserOperation int + +const ( + PGSyncUserAdd = iota + PGsyncUserAlter +) + type PodEvent struct { ClusterName NamespacedName PodName NamespacedName @@ -36,7 +45,19 @@ type PgUser struct { Name string Password string Flags []string - MemberOf string + MemberOf []string +} + +type PgUserMap map[string]PgUser + +type PgSyncUserRequest struct { + Kind SyncUserOperation + User PgUser +} + +type UserSyncer interface { + ProduceSyncRequests(dbUsers PgUserMap, newUsers PgUserMap) (req []PgSyncUserRequest) + ExecuteSyncRequests(req []PgSyncUserRequest, db *sql.DB) error } func (p NamespacedName) String() string { diff --git a/pkg/util/constants/constants.go b/pkg/util/constants/constants.go index 2edde34d3..c4bc30ac3 100644 --- a/pkg/util/constants/constants.go +++ b/pkg/util/constants/constants.go @@ -16,4 +16,12 @@ const ( ResourceName = TPRName + "s" PodRoleMaster = "master" PodRoleReplica = "replica" + SuperuserKeyName = "superuser" + ReplicationUserKeyName = "replication" + RoleFlagSuperuser = "SUPERUSER" + RoleFlagInherit = "INHERIT" + RoleFlagLogin = "LOGIN" + RoleFlagNoLogin = "NOLOGIN" + RoleFlagCreateRole = "CREATEROLE" + RoleFlagCreateDB = "CREATEDB" ) diff --git a/pkg/util/users/users.go b/pkg/util/users/users.go new file mode 100644 index 000000000..bd7a54e8f --- /dev/null +++ b/pkg/util/users/users.go @@ -0,0 +1,152 @@ +package users + +import ( + "database/sql" + "fmt" + "strings" + + "github.bus.zalan.do/acid/postgres-operator/pkg/spec" + "github.bus.zalan.do/acid/postgres-operator/pkg/util" +) + +const ( + createUserSQL = `SET LOCAL synchronous_commit = 'local'; CREATE ROLE "%s" %s %s;` + alterUserSQL = `ALTER ROLE "%s" %s` + grantToUserSQL = `GRANT %s TO "%s"` + doBlockStmt = `SET LOCAL synchronous_commit = 'local'; DO $$ BEGIN %s; END;$$;` + passwordTemplate = "ENCRYPTED PASSWORD '%s'" + inRoleTemplate = `IN ROLE %s` +) + +type DefaultUserSyncStrategy struct { +} + +func (s DefaultUserSyncStrategy) ProduceSyncRequests(dbUsers spec.PgUserMap, + newUsers spec.PgUserMap) (reqs []spec.PgSyncUserRequest) { + + // No existing roles are deleted or stripped of role memebership/flags + for name, newUser := range newUsers { + dbUser, exists := dbUsers[name] + if !exists { + reqs = append(reqs, spec.PgSyncUserRequest{spec.PGSyncUserAdd, newUser}) + } else { + r := spec.PgSyncUserRequest{} + newMD5Password := util.PGUserPassword(newUser) + + if dbUser.Password != newMD5Password { + r.User.Password = newMD5Password + r.Kind = spec.PGsyncUserAlter + } + if addNewRoles, equal := util.SubstractStringSlices(newUser.MemberOf, dbUser.MemberOf); !equal { + r.User.MemberOf = addNewRoles + r.Kind = spec.PGsyncUserAlter + } + if addNewFlags, equal := util.SubstractStringSlices(newUser.Flags, dbUser.Flags); !equal { + r.User.Flags = addNewFlags + r.Kind = spec.PGsyncUserAlter + } + if r.Kind == spec.PGsyncUserAlter { + r.User.Name = newUser.Name + reqs = append(reqs, r) + } + } + } + + return +} + +func (s DefaultUserSyncStrategy) ExecuteSyncRequests(reqs []spec.PgSyncUserRequest, db *sql.DB) error { + for _, r := range reqs { + switch r.Kind { + case spec.PGSyncUserAdd: + if err := s.createPgUser(r.User, db); err != nil { + return fmt.Errorf("Can't create user '%s': %s", r.User.Name, err) + } + case spec.PGsyncUserAlter: + if err := s.alterPgUser(r.User, db); err != nil { + return fmt.Errorf("Can't alter user '%s': %s", r.User.Name, err) + } + default: + return fmt.Errorf("Unrecognized operation: %s", r.Kind) + } + + } + return nil +} + +func (s DefaultUserSyncStrategy) createPgUser(user spec.PgUser, db *sql.DB) (err error) { + var userFlags []string + var userPassword string + + if len(user.Flags) > 0 { + userFlags = append(userFlags, user.Flags...) + } + if len(user.MemberOf) > 0 { + userFlags = append(userFlags, fmt.Sprintf(inRoleTemplate, quoteMemberList(user))) + } + + if user.Password == "" { + userPassword = "PASSWORD NULL" + } else { + userPassword = fmt.Sprintf(passwordTemplate, util.PGUserPassword(user)) + } + query := fmt.Sprintf(createUserSQL, user.Name, strings.Join(userFlags, " "), userPassword) + + _, err = db.Query(query) // TODO: Try several times + if err != nil { + err = fmt.Errorf("DB error: %s, query: %s", err, query) + return + } + + return +} + +func (s DefaultUserSyncStrategy) alterPgUser(user spec.PgUser, db *sql.DB) (err error) { + var resultStmt []string + + if user.Password != "" || len(user.Flags) > 0 { + alterStmt := produceAlterStmt(user) + resultStmt = append(resultStmt, alterStmt) + } + if len(user.MemberOf) > 0 { + grantStmt := produceGrantStmt(user) + resultStmt = append(resultStmt, grantStmt) + } + query := fmt.Sprintf(doBlockStmt, strings.Join(resultStmt, ";")) + + _, err = db.Query(query) // TODO: Try several times + if err != nil { + err = fmt.Errorf("DB error: %s query %s", err, query) + return + } + + return +} + +func produceAlterStmt(user spec.PgUser) string { + // ALTER ROLE ... LOGIN ENCRYPTED PASSWORD .. + result := make([]string, 1) + password := user.Password + flags := user.Flags + + if password != "" { + result = append(result, fmt.Sprintf(passwordTemplate, util.PGUserPassword(user))) + } + if len(flags) != 0 { + result = append(result, strings.Join(flags, " ")) + } + return fmt.Sprintf(alterUserSQL, user.Name, strings.Join(result, " ")) +} + +func produceGrantStmt(user spec.PgUser) string { + // GRANT ROLE "foo", "bar" TO baz + return fmt.Sprintf(grantToUserSQL, quoteMemberList(user), user.Name) +} + +func quoteMemberList(user spec.PgUser) string { + var memberof []string + for _, member := range user.MemberOf { + memberof = append(memberof, fmt.Sprintf(`"%s"`, member)) + } + return strings.Join(memberof, ",") +} diff --git a/pkg/util/util.go b/pkg/util/util.go index aaab41ab4..f0e1b045b 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -14,6 +14,10 @@ import ( "github.bus.zalan.do/acid/postgres-operator/pkg/spec" ) +const ( + MD5Prefix = "md5" +) + var passwordChars = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") func init() { @@ -37,9 +41,12 @@ func NameFromMeta(meta v1.ObjectMeta) spec.NamespacedName { } func PGUserPassword(user spec.PgUser) string { + if (len(user.Password) == md5.Size && user.Password[:3] == MD5Prefix) || user.Password == "" { + // Avoid processing already encrypted or empty passwords + return user.Password + } s := md5.Sum([]byte(user.Password + user.Name)) - - return "md5" + hex.EncodeToString(s[:]) + return MD5Prefix + hex.EncodeToString(s[:]) } func Pretty(x interface{}) (f fmt.Formatter) { @@ -50,3 +57,18 @@ func PrettyDiff(a, b interface{}) (result string) { diff := pretty.Diff(a, b) return strings.Join(diff, "\n") } + +func SubstractStringSlices(a []string, b []string) (result []string, equal bool) { + // Find elements in a that are not in b and return them as a result slice + // Slices are assumed to contain unique elements only +OUTER: + for _, vala := range a { + for _, valb := range b { + if vala == valb { + continue OUTER + } + } + result = append(result, vala) + } + return result, len(result) == 0 +}