[WIP] Grant 'superuser' to the members of Postgres admin teams (#371)
Added support for superuser team in addition to the admin team that owns the postgres cluster.
This commit is contained in:
parent
1e53e22773
commit
25fa45fd58
|
|
@ -208,3 +208,15 @@ generated from the current cluster manifest. There are two types of scans: a
|
|||
`sync scan`, running every `resync_period` seconds for every cluster, and the
|
||||
`repair scan`, coming every `repair_period` only for those clusters that didn't
|
||||
report success as a result of the last operation applied to them.
|
||||
|
||||
## Postgres roles supported by the operator
|
||||
|
||||
The operator is capable of maintaining roles of multiple kinds within a Postgres database cluster:
|
||||
|
||||
1. **System roles** are roles necessary for the proper work of Postgres itself such as a replication role or the initial superuser role. The operator delegates creating such roles to Patroni and only establishes relevant secrets.
|
||||
|
||||
2. **Infrastructure roles** are roles for processes originating from external systems, e.g. monitoring robots. The operator creates such roles in all PG clusters it manages assuming k8s secrets with the relevant credentials exist beforehand.
|
||||
|
||||
3. **Per-cluster robot users** are also roles for processes originating from external systems but defined for an individual Postgres cluster in its manifest. A typical example is a role for connections from an application that uses the database.
|
||||
|
||||
4. **Human users** originate from the Teams API that returns list of the team members given a team id. Operator differentiates between (a) product teams that own a particular Postgres cluster and are granted admin rights to maintain it, and (b) Postgres superuser teams that get the superuser access to all PG databases running in a k8s cluster for the purposes of maintaining and troubleshooting.
|
||||
|
|
@ -377,6 +377,9 @@ key.
|
|||
List of roles that cannot be overwritten by an application, team or
|
||||
infrastructure role. The default is `admin`.
|
||||
|
||||
* **postgres_superuser_teams**
|
||||
List of teams which members need the superuser role in each PG database cluster to administer Postgres and maintain infrastructure built around it. The default is `postgres_superuser`.
|
||||
|
||||
## Logging and REST API
|
||||
|
||||
Parameters affecting logging and REST API listener. In the CRD-based configuration they are grouped under the `logging_rest_api` key.
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@ package v1
|
|||
import (
|
||||
"github.com/zalando-incubator/postgres-operator/pkg/util/config"
|
||||
|
||||
"time"
|
||||
|
||||
"github.com/zalando-incubator/postgres-operator/pkg/spec"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"time"
|
||||
)
|
||||
|
||||
// +genclient
|
||||
|
|
@ -99,6 +100,7 @@ type TeamsAPIConfiguration struct {
|
|||
PamRoleName string `json:"pam_role_name,omitempty"`
|
||||
PamConfiguration string `json:"pam_configuration,omitempty"`
|
||||
ProtectedRoles []string `json:"protected_role_names,omitempty"`
|
||||
PostgresSuperuserTeams []string `json:"postgres_superuser_teams,omitempty"`
|
||||
}
|
||||
|
||||
type LoggingRESTAPIConfiguration struct {
|
||||
|
|
|
|||
|
|
@ -631,6 +631,11 @@ func (in *TeamsAPIConfiguration) DeepCopyInto(out *TeamsAPIConfiguration) {
|
|||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.PostgresSuperuserTeams != nil {
|
||||
in, out := &in.PostgresSuperuserTeams, &out.PostgresSuperuserTeams
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -723,11 +723,13 @@ func (c *Cluster) initRobotUsers() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *Cluster) initHumanUsers() error {
|
||||
teamMembers, err := c.getTeamMembers()
|
||||
func (c *Cluster) initTeamMembers(teamID string, isPostgresSuperuserTeam bool) error {
|
||||
teamMembers, err := c.getTeamMembers(teamID)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get list of team members: %v", err)
|
||||
return fmt.Errorf("could not get list of team members for team %q: %v", teamID, err)
|
||||
}
|
||||
|
||||
for _, username := range teamMembers {
|
||||
flags := []string{constants.RoleFlagLogin}
|
||||
memberOf := []string{c.OpConfig.PamRoleName}
|
||||
|
|
@ -735,7 +737,7 @@ func (c *Cluster) initHumanUsers() error {
|
|||
if c.shouldAvoidProtectedOrSystemRole(username, "API role") {
|
||||
continue
|
||||
}
|
||||
if c.OpConfig.EnableTeamSuperuser {
|
||||
if c.OpConfig.EnableTeamSuperuser || isPostgresSuperuserTeam {
|
||||
flags = append(flags, constants.RoleFlagSuperuser)
|
||||
} else {
|
||||
if c.OpConfig.TeamAdminRole != "" {
|
||||
|
|
@ -761,6 +763,33 @@ func (c *Cluster) initHumanUsers() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *Cluster) initHumanUsers() error {
|
||||
|
||||
var clusterIsOwnedBySuperuserTeam bool
|
||||
|
||||
for _, postgresSuperuserTeam := range c.OpConfig.PostgresSuperuserTeams {
|
||||
err := c.initTeamMembers(postgresSuperuserTeam, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Cannot create a team %q of Postgres superusers: %v", postgresSuperuserTeam, err)
|
||||
}
|
||||
if postgresSuperuserTeam == c.Spec.TeamID {
|
||||
clusterIsOwnedBySuperuserTeam = true
|
||||
}
|
||||
}
|
||||
|
||||
if clusterIsOwnedBySuperuserTeam {
|
||||
c.logger.Infof("Team %q owning the cluster is also a team of superusers. Created superuser roles for its members instead of admin roles.", c.Spec.TeamID)
|
||||
return nil
|
||||
}
|
||||
|
||||
err := c.initTeamMembers(c.Spec.TeamID, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Cannot create a team %q of admins owning the PG cluster: %v", c.Spec.TeamID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cluster) initInfrastructureRoles() error {
|
||||
// add infrastructure roles from the operator's definition
|
||||
for username, newRole := range c.InfrastructureRoles {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ package cluster
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
acidv1 "github.com/zalando-incubator/postgres-operator/pkg/apis/acid.zalan.do/v1"
|
||||
"github.com/zalando-incubator/postgres-operator/pkg/spec"
|
||||
|
|
@ -9,8 +12,6 @@ import (
|
|||
"github.com/zalando-incubator/postgres-operator/pkg/util/k8sutil"
|
||||
"github.com/zalando-incubator/postgres-operator/pkg/util/teams"
|
||||
"k8s.io/api/core/v1"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -101,6 +102,7 @@ func (m *mockTeamsAPIClient) setMembers(members []string) {
|
|||
m.members = members
|
||||
}
|
||||
|
||||
// Test adding a member of a product team owning a particular DB cluster
|
||||
func TestInitHumanUsers(t *testing.T) {
|
||||
|
||||
var mockTeamsAPI mockTeamsAPIClient
|
||||
|
|
@ -108,7 +110,9 @@ func TestInitHumanUsers(t *testing.T) {
|
|||
cl.teamsAPIClient = &mockTeamsAPI
|
||||
testName := "TestInitHumanUsers"
|
||||
|
||||
// members of a product team are granted superuser rights for DBs of their team
|
||||
cl.OpConfig.EnableTeamSuperuser = true
|
||||
|
||||
cl.OpConfig.EnableTeamsAPI = true
|
||||
cl.OpConfig.PamRoleName = "zalandos"
|
||||
cl.Spec.TeamID = "test"
|
||||
|
|
@ -146,6 +150,145 @@ func TestInitHumanUsers(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
type mockTeam struct {
|
||||
teamID string
|
||||
members []string
|
||||
isPostgresSuperuserTeam bool
|
||||
}
|
||||
|
||||
type mockTeamsAPIClientMultipleTeams struct {
|
||||
teams []mockTeam
|
||||
}
|
||||
|
||||
func (m *mockTeamsAPIClientMultipleTeams) TeamInfo(teamID, token string) (tm *teams.Team, err error) {
|
||||
for _, team := range m.teams {
|
||||
if team.teamID == teamID {
|
||||
return &teams.Team{Members: team.members}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// should not be reached if a slice with teams is populated correctly
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Test adding members of maintenance teams that get superuser rights for all PG databases
|
||||
func TestInitHumanUsersWithSuperuserTeams(t *testing.T) {
|
||||
|
||||
var mockTeamsAPI mockTeamsAPIClientMultipleTeams
|
||||
cl.oauthTokenGetter = &mockOAuthTokenGetter{}
|
||||
cl.teamsAPIClient = &mockTeamsAPI
|
||||
cl.OpConfig.EnableTeamSuperuser = false
|
||||
testName := "TestInitHumanUsersWithSuperuserTeams"
|
||||
|
||||
cl.OpConfig.EnableTeamsAPI = true
|
||||
cl.OpConfig.PamRoleName = "zalandos"
|
||||
|
||||
teamA := mockTeam{
|
||||
teamID: "postgres_superusers",
|
||||
members: []string{"postgres_superuser"},
|
||||
isPostgresSuperuserTeam: true,
|
||||
}
|
||||
|
||||
userA := spec.PgUser{
|
||||
Name: "postgres_superuser",
|
||||
Origin: spec.RoleOriginTeamsAPI,
|
||||
MemberOf: []string{cl.OpConfig.PamRoleName},
|
||||
Flags: []string{"LOGIN", "SUPERUSER"},
|
||||
}
|
||||
|
||||
teamB := mockTeam{
|
||||
teamID: "postgres_admins",
|
||||
members: []string{"postgres_admin"},
|
||||
isPostgresSuperuserTeam: true,
|
||||
}
|
||||
|
||||
userB := spec.PgUser{
|
||||
Name: "postgres_admin",
|
||||
Origin: spec.RoleOriginTeamsAPI,
|
||||
MemberOf: []string{cl.OpConfig.PamRoleName},
|
||||
Flags: []string{"LOGIN", "SUPERUSER"},
|
||||
}
|
||||
|
||||
teamTest := mockTeam{
|
||||
teamID: "test",
|
||||
members: []string{"test_user"},
|
||||
isPostgresSuperuserTeam: false,
|
||||
}
|
||||
|
||||
userTest := spec.PgUser{
|
||||
Name: "test_user",
|
||||
Origin: spec.RoleOriginTeamsAPI,
|
||||
MemberOf: []string{cl.OpConfig.PamRoleName},
|
||||
Flags: []string{"LOGIN"},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
ownerTeam string
|
||||
existingRoles map[string]spec.PgUser
|
||||
superuserTeams []string
|
||||
teams []mockTeam
|
||||
result map[string]spec.PgUser
|
||||
}{
|
||||
// case 1: there are two different teams of PG maintainers and one product team
|
||||
{
|
||||
ownerTeam: "test",
|
||||
existingRoles: map[string]spec.PgUser{},
|
||||
superuserTeams: []string{"postgres_superusers", "postgres_admins"},
|
||||
teams: []mockTeam{teamA, teamB, teamTest},
|
||||
result: map[string]spec.PgUser{
|
||||
"postgres_superuser": userA,
|
||||
"postgres_admin": userB,
|
||||
"test_user": userTest,
|
||||
},
|
||||
},
|
||||
// case 2: the team of superusers creates a new PG cluster
|
||||
{
|
||||
ownerTeam: "postgres_superusers",
|
||||
existingRoles: map[string]spec.PgUser{},
|
||||
superuserTeams: []string{"postgres_superusers"},
|
||||
teams: []mockTeam{teamA},
|
||||
result: map[string]spec.PgUser{
|
||||
"postgres_superuser": userA,
|
||||
},
|
||||
},
|
||||
// case 3: the team owning the cluster is promoted to the maintainers' status
|
||||
{
|
||||
ownerTeam: "postgres_superusers",
|
||||
existingRoles: map[string]spec.PgUser{
|
||||
// role with the name exists before w/o superuser privilege
|
||||
"postgres_superuser": spec.PgUser{
|
||||
Origin: spec.RoleOriginTeamsAPI,
|
||||
Name: "postgres_superuser",
|
||||
Password: "",
|
||||
Flags: []string{"LOGIN"},
|
||||
MemberOf: []string{cl.OpConfig.PamRoleName},
|
||||
Parameters: map[string]string(nil)}},
|
||||
superuserTeams: []string{"postgres_superusers"},
|
||||
teams: []mockTeam{teamA},
|
||||
result: map[string]spec.PgUser{
|
||||
"postgres_superuser": userA,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
mockTeamsAPI.teams = tt.teams
|
||||
|
||||
cl.Spec.TeamID = tt.ownerTeam
|
||||
cl.pgUsers = tt.existingRoles
|
||||
cl.OpConfig.PostgresSuperuserTeams = tt.superuserTeams
|
||||
|
||||
if err := cl.initHumanUsers(); err != nil {
|
||||
t.Errorf("%s got an unexpected error %v", testName, err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(cl.pgUsers, tt.result) {
|
||||
t.Errorf("%s expects %#v, got %#v", testName, tt.result, cl.pgUsers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldDeleteSecret(t *testing.T) {
|
||||
testName := "TestShouldDeleteSecret"
|
||||
|
||||
|
|
|
|||
|
|
@ -210,12 +210,14 @@ func (c *Cluster) logVolumeChanges(old, new acidv1.Volume) {
|
|||
c.logger.Debugf("diff\n%s\n", util.PrettyDiff(old, new))
|
||||
}
|
||||
|
||||
func (c *Cluster) getTeamMembers() ([]string, error) {
|
||||
if c.Spec.TeamID == "" {
|
||||
func (c *Cluster) getTeamMembers(teamID string) ([]string, error) {
|
||||
|
||||
if teamID == "" {
|
||||
return nil, fmt.Errorf("no teamId specified")
|
||||
}
|
||||
|
||||
if !c.OpConfig.EnableTeamsAPI {
|
||||
c.logger.Debug("team API is disabled, returning empty list of members")
|
||||
c.logger.Debugf("team API is disabled, returning empty list of members for team %q", teamID)
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
|
|
@ -225,9 +227,9 @@ func (c *Cluster) getTeamMembers() ([]string, error) {
|
|||
return []string{}, nil
|
||||
}
|
||||
|
||||
teamInfo, err := c.teamsAPIClient.TeamInfo(c.Spec.TeamID, token)
|
||||
teamInfo, err := c.teamsAPIClient.TeamInfo(teamID, token)
|
||||
if err != nil {
|
||||
c.logger.Warnf("could not get team info, returning empty list of team members: %v", err)
|
||||
c.logger.Warnf("could not get team info for team %q, returning empty list of team members: %v", teamID, err)
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
|
|||
result.EnableTeamSuperuser = fromCRD.TeamsAPI.EnableTeamSuperuser
|
||||
result.TeamAdminRole = fromCRD.TeamsAPI.TeamAdminRole
|
||||
result.PamRoleName = fromCRD.TeamsAPI.PamRoleName
|
||||
result.PostgresSuperuserTeams = fromCRD.TeamsAPI.PostgresSuperuserTeams
|
||||
|
||||
result.APIPort = fromCRD.LoggingRESTAPI.APIPort
|
||||
result.RingLogLines = fromCRD.LoggingRESTAPI.RingLogLines
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const fileWithNamespace = "/var/run/secrets/kubernetes.io/serviceaccount/namespa
|
|||
// RoleOrigin contains the code of the origin of a role
|
||||
type RoleOrigin int
|
||||
|
||||
// The rolesOrigin constant values should be sorted by the role priority.
|
||||
// The rolesOrigin constant values must be sorted by the role priority for resolveNameConflict(...) to work.
|
||||
const (
|
||||
RoleOriginUnknown RoleOrigin = iota
|
||||
RoleOriginManifest
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ type Config struct {
|
|||
TeamAPIRoleConfiguration map[string]string `name:"team_api_role_configuration" default:"log_statement:all"`
|
||||
PodTerminateGracePeriod time.Duration `name:"pod_terminate_grace_period" default:"5m"`
|
||||
ProtectedRoles []string `name:"protected_role_names" default:"admin"`
|
||||
PostgresSuperuserTeams []string `name:"postgres_superuser_teams" default:""`
|
||||
}
|
||||
|
||||
// MustMarshal marshals the config or panics
|
||||
|
|
|
|||
Loading…
Reference in New Issue