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-12 11:41:35 +02:00
committed by Murat Kabilov
parent 411487e66d
commit 6983f444ed
12 changed files with 378 additions and 87 deletions
+152
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, ",")
}