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:
parent
411487e66d
commit
6983f444ed
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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{
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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, ",")
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue