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.
This commit is contained in:
Oleksii Kliukin 2017-05-09 13:13:03 +02:00 committed by Murat Kabilov
parent 411487e66d
commit 6983f444ed
12 changed files with 378 additions and 87 deletions

View File

@ -26,6 +26,7 @@ import (
"github.bus.zalan.do/acid/postgres-operator/pkg/util/constants" "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/k8sutil"
"github.bus.zalan.do/acid/postgres-operator/pkg/util/teams" "github.bus.zalan.do/acid/postgres-operator/pkg/util/teams"
"github.bus.zalan.do/acid/postgres-operator/pkg/util/users"
) )
var ( var (
@ -58,6 +59,7 @@ type Cluster struct {
Config Config
logger *logrus.Entry logger *logrus.Entry
pgUsers map[string]spec.PgUser pgUsers map[string]spec.PgUser
systemUsers map[string]spec.PgUser
podEvents chan spec.PodEvent podEvents chan spec.PodEvent
podSubscribers map[spec.NamespacedName]chan spec.PodEvent podSubscribers map[spec.NamespacedName]chan spec.PodEvent
podSubscribersMu sync.RWMutex podSubscribersMu sync.RWMutex
@ -65,6 +67,7 @@ type Cluster struct {
mu sync.Mutex mu sync.Mutex
masterLess bool masterLess bool
podDispatcherRunning bool podDispatcherRunning bool
userSyncStrategy spec.UserSyncer
deleteOptions *v1.DeleteOptions deleteOptions *v1.DeleteOptions
} }
@ -78,11 +81,13 @@ func New(cfg Config, pgSpec spec.Postgresql, logger *logrus.Entry) *Cluster {
Postgresql: pgSpec, Postgresql: pgSpec,
logger: lg, logger: lg,
pgUsers: make(map[string]spec.PgUser), pgUsers: make(map[string]spec.PgUser),
systemUsers: make(map[string]spec.PgUser),
podEvents: make(chan spec.PodEvent), podEvents: make(chan spec.PodEvent),
podSubscribers: make(map[spec.NamespacedName]chan spec.PodEvent), podSubscribers: make(map[spec.NamespacedName]chan spec.PodEvent),
kubeResources: kubeResources, kubeResources: kubeResources,
masterLess: false, masterLess: false,
podDispatcherRunning: false, podDispatcherRunning: false,
userSyncStrategy: users.DefaultUserSyncStrategy{},
deleteOptions: &v1.DeleteOptions{OrphanDependents: &orphanDependents}, deleteOptions: &v1.DeleteOptions{OrphanDependents: &orphanDependents},
} }
@ -426,12 +431,15 @@ func (c *Cluster) ReceivePodEvent(event spec.PodEvent) {
} }
func (c *Cluster) initSystemUsers() { 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, Name: c.OpConfig.SuperUsername,
Password: util.RandomPassword(constants.PasswordLength), Password: util.RandomPassword(constants.PasswordLength),
} }
c.systemUsers[constants.ReplicationUserKeyName] = spec.PgUser{
c.pgUsers[c.OpConfig.ReplicationUsername] = spec.PgUser{
Name: c.OpConfig.ReplicationUsername, Name: c.OpConfig.ReplicationUsername,
Password: util.RandomPassword(constants.PasswordLength), 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) return fmt.Errorf("Can't get list of team members: %s", err)
} else { } else {
for _, username := range teamMembers { 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) { if !isValidUsername(username) {
return fmt.Errorf("Invalid username: '%s'", 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 c.pgUsers[username] = data
} }
return nil return nil

View File

@ -228,32 +228,48 @@ func persistentVolumeClaimTemplate(volumeSize, volumeStorageClass string) *v1.Pe
return volumeClaim 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)) secrets = make(map[string]*v1.Secret, len(c.pgUsers))
namespace := c.Metadata.Namespace namespace := c.Metadata.Namespace
for username, pgUser := range c.pgUsers { for username, pgUser := range c.pgUsers {
//Skip users with no password i.e. human users (they'll be authenticated using pam) //Skip users with no password i.e. human users (they'll be authenticated using pam)
if pgUser.Password == "" { secret := c.genSingleUserSecret(namespace, pgUser)
continue if secret != nil {
secrets[username] = secret
} }
secret := v1.Secret{ }
ObjectMeta: v1.ObjectMeta{ /* special case for the system user */
Name: c.credentialSecretName(username), for _, systemUser := range c.systemUsers {
Namespace: namespace, secret := c.genSingleUserSecret(namespace, systemUser)
Labels: c.labelsSet(), if secret != nil {
}, secrets[systemUser.Name] = secret
Type: v1.SecretTypeOpaque,
Data: map[string][]byte{
"username": []byte(pgUser.Name),
"password": []byte(pgUser.Password),
},
} }
secrets[username] = &secret
} }
return 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 { func (c *Cluster) genService(allowedSourceRanges []string) *v1.Service {
service := &v1.Service{ service := &v1.Service{
ObjectMeta: v1.ObjectMeta{ ObjectMeta: v1.ObjectMeta{

View File

@ -8,18 +8,28 @@ import (
_ "github.com/lib/pq" _ "github.com/lib/pq"
"github.bus.zalan.do/acid/postgres-operator/pkg/spec" "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 { func (c *Cluster) pgConnectionString() string {
hostname := fmt.Sprintf("%s.%s.svc.cluster.local", c.Metadata.Name, c.Metadata.Namespace) 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'", return fmt.Sprintf("host='%s' dbname=postgres sslmode=require user='%s' password='%s'",
hostname, hostname,
c.OpConfig.SuperUsername, username,
strings.Replace(password, "$", "\\$", -1)) strings.Replace(password, "$", "\\$", -1))
} }
@ -33,6 +43,7 @@ func (c *Cluster) initDbConn() error {
} }
err = conn.Ping() err = conn.Ping()
if err != nil { if err != nil {
conn.Close()
return err return err
} }
@ -43,42 +54,48 @@ func (c *Cluster) initDbConn() error {
return nil return nil
} }
func (c *Cluster) createPgUser(user spec.PgUser) (isHuman bool, err error) { func (c *Cluster) readPgUsersFromDatabase(userNames []string) (users spec.PgUserMap, err error) {
var flags []string = user.Flags var rows *sql.Rows
users = make(spec.PgUserMap)
if user.Password == "" { if rows, err = c.pgDb.Query(getUserSQL, pq.Array(userNames)); err != nil {
isHuman = true return nil, fmt.Errorf("Error when querying users: %s", err)
flags = append(flags, "SUPERUSER")
flags = append(flags, fmt.Sprintf("IN ROLE \"%s\"", c.OpConfig.PamRoleName))
} else {
isHuman = false
} }
defer rows.Close()
addLoginFlag := true for rows.Next() {
for _, v := range flags { var (
if v == "NOLOGIN" { rolname, rolpassword string
addLoginFlag = false rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin bool
break 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)
} }
} flags := makeUserFlags(rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin)
if addLoginFlag { // XXX: the code assumes the password we get from pg_authid is always MD5
flags = append(flags, "LOGIN") users[rolname] = spec.PgUser{Name: rolname, Password: rolpassword, Flags: flags, MemberOf: memberof}
}
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
} }
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
} }

View File

@ -6,9 +6,11 @@ import (
"k8s.io/client-go/pkg/api" "k8s.io/client-go/pkg/api"
"k8s.io/client-go/pkg/api/v1" "k8s.io/client-go/pkg/api/v1"
"k8s.io/client-go/pkg/apis/apps/v1beta1" "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"
"github.bus.zalan.do/acid/postgres-operator/pkg/util/k8sutil" "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 { func (c *Cluster) loadResources() error {
@ -183,7 +185,7 @@ func (c *Cluster) createService() (*v1.Service, error) {
return service, nil return service, nil
} }
func (c *Cluster) updateService(newService *v1.Service) error { func (c *Cluster) updateService(newService *v1.Service) error {
if c.Service == nil { if c.Service == nil {
return fmt.Errorf("There is no Service in the cluster") return fmt.Errorf("There is no Service in the cluster")
} }
@ -262,23 +264,29 @@ func (c *Cluster) deleteEndpoint() error {
} }
func (c *Cluster) applySecrets() error { func (c *Cluster) applySecrets() error {
secrets, err := c.genUserSecrets() secrets := c.genUserSecrets()
if err != nil {
return fmt.Errorf("Can't get user Secrets")
}
for secretUsername, secretSpec := range secrets { for secretUsername, secretSpec := range secrets {
secret, err := c.KubeClient.Secrets(secretSpec.Namespace).Create(secretSpec) secret, err := c.KubeClient.Secrets(secretSpec.Namespace).Create(secretSpec)
if k8sutil.ResourceAlreadyExists(err) { if k8sutil.ResourceAlreadyExists(err) {
var userMap map[string]spec.PgUser
curSecret, err := c.KubeClient.Secrets(secretSpec.Namespace).Get(secretSpec.Name) curSecret, err := c.KubeClient.Secrets(secretSpec.Namespace).Get(secretSpec.Name)
if err != nil { if err != nil {
return fmt.Errorf("Can't get current Secret: %s", err) 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)) 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"]) pwdUser.Password = string(curSecret.Data["password"])
c.pgUsers[secretUsername] = pwdUser userMap[secretUsername] = pwdUser
continue continue
} else { } else {
@ -305,23 +313,12 @@ func (c *Cluster) deleteSecret(secret *v1.Secret) error {
return err 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 // TODO: figure out what to do with duplicate names (humans and robots) among pgUsers
for username, user := range c.pgUsers { reqs := c.userSyncStrategy.ProduceSyncRequests(nil, c.pgUsers)
if username == c.OpConfig.SuperUsername || username == c.OpConfig.ReplicationUsername { err = c.userSyncStrategy.ExecuteSyncRequests(reqs, c.pgDb)
continue if err != nil {
} return err
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)
}
} }
return nil return nil

View File

@ -36,6 +36,14 @@ func (c *Cluster) SyncCluster(stopCh <-chan struct{}) {
if err := c.syncStatefulSet(); err != nil { if err := c.syncStatefulSet(); err != nil {
c.logger.Errorf("Can't sync StatefulSets: %s", err) 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 { func (c *Cluster) syncSecrets() error {
@ -150,3 +158,23 @@ func (c *Cluster) syncStatefulSet() error {
return nil 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
}

View File

@ -24,6 +24,7 @@ func isValidUsername(username string) bool {
func normalizeUserFlags(userFlags []string) (flags []string, err error) { func normalizeUserFlags(userFlags []string) (flags []string, err error) {
uniqueFlags := make(map[string]bool) uniqueFlags := make(map[string]bool)
addLogin := true
for _, flag := range userFlags { for _, flag := range userFlags {
if !alphaNumericRegexp.MatchString(flag) { 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{} flags = []string{}
for k := range uniqueFlags { 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) flags = append(flags, k)
} }
if addLogin {
flags = append(flags, constants.RoleFlagLogin)
}
return return
} }

View File

@ -159,7 +159,7 @@ func (c *Controller) processClusterEventsQueue(idx int) {
func (c *Controller) queueClusterEvent(old, new *spec.Postgresql, eventType spec.EventType) { func (c *Controller) queueClusterEvent(old, new *spec.Postgresql, eventType spec.EventType) {
var ( var (
uid types.UID uid types.UID
clusterName spec.NamespacedName clusterName spec.NamespacedName
) )

View File

@ -122,7 +122,7 @@ Users:
case "password": case "password":
t.Password = s t.Password = s
case "inrole": case "inrole":
t.MemberOf = s t.MemberOf = append(t.MemberOf, s)
default: default:
c.logger.Warnf("Unknown key %s", p) c.logger.Warnf("Unknown key %s", p)
} }

View File

@ -1,6 +1,8 @@
package spec package spec
import ( import (
"database/sql"
"k8s.io/client-go/pkg/api/v1" "k8s.io/client-go/pkg/api/v1"
"k8s.io/client-go/pkg/types" "k8s.io/client-go/pkg/types"
) )
@ -24,6 +26,13 @@ type ClusterEvent struct {
WorkerID uint32 WorkerID uint32
} }
type SyncUserOperation int
const (
PGSyncUserAdd = iota
PGsyncUserAlter
)
type PodEvent struct { type PodEvent struct {
ClusterName NamespacedName ClusterName NamespacedName
PodName NamespacedName PodName NamespacedName
@ -36,7 +45,19 @@ type PgUser struct {
Name string Name string
Password string Password string
Flags []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 { func (p NamespacedName) String() string {

View File

@ -16,4 +16,12 @@ const (
ResourceName = TPRName + "s" ResourceName = TPRName + "s"
PodRoleMaster = "master" PodRoleMaster = "master"
PodRoleReplica = "replica" PodRoleReplica = "replica"
SuperuserKeyName = "superuser"
ReplicationUserKeyName = "replication"
RoleFlagSuperuser = "SUPERUSER"
RoleFlagInherit = "INHERIT"
RoleFlagLogin = "LOGIN"
RoleFlagNoLogin = "NOLOGIN"
RoleFlagCreateRole = "CREATEROLE"
RoleFlagCreateDB = "CREATEDB"
) )

152
pkg/util/users/users.go Normal file
View File

@ -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, ",")
}

View File

@ -14,6 +14,10 @@ import (
"github.bus.zalan.do/acid/postgres-operator/pkg/spec" "github.bus.zalan.do/acid/postgres-operator/pkg/spec"
) )
const (
MD5Prefix = "md5"
)
var passwordChars = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") var passwordChars = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
func init() { func init() {
@ -37,9 +41,12 @@ func NameFromMeta(meta v1.ObjectMeta) spec.NamespacedName {
} }
func PGUserPassword(user spec.PgUser) string { 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)) s := md5.Sum([]byte(user.Password + user.Name))
return MD5Prefix + hex.EncodeToString(s[:])
return "md5" + hex.EncodeToString(s[:])
} }
func Pretty(x interface{}) (f fmt.Formatter) { func Pretty(x interface{}) (f fmt.Formatter) {
@ -50,3 +57,18 @@ func PrettyDiff(a, b interface{}) (result string) {
diff := pretty.Diff(a, b) diff := pretty.Diff(a, b)
return strings.Join(diff, "\n") 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
}