Skip to content

Commit 25fa45f

Browse files
sdudoladovJan-M
authored andcommitted
[WIP] Grant 'superuser' to the members of Postgres admin teams (zalando#371)
Added support for superuser team in addition to the admin team that owns the postgres cluster.
1 parent 1e53e22 commit 25fa45f

File tree

10 files changed

+211
-13
lines changed

10 files changed

+211
-13
lines changed

docs/administrator.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,3 +208,15 @@ generated from the current cluster manifest. There are two types of scans: a
208208
`sync scan`, running every `resync_period` seconds for every cluster, and the
209209
`repair scan`, coming every `repair_period` only for those clusters that didn't
210210
report success as a result of the last operation applied to them.
211+
212+
## Postgres roles supported by the operator
213+
214+
The operator is capable of maintaining roles of multiple kinds within a Postgres database cluster:
215+
216+
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.
217+
218+
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.
219+
220+
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.
221+
222+
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.

docs/reference/operator_parameters.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,9 @@ key.
377377
List of roles that cannot be overwritten by an application, team or
378378
infrastructure role. The default is `admin`.
379379

380+
* **postgres_superuser_teams**
381+
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`.
382+
380383
## Logging and REST API
381384

382385
Parameters affecting logging and REST API listener. In the CRD-based configuration they are grouped under the `logging_rest_api` key.

pkg/apis/acid.zalan.do/v1/operator_configuration_type.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ package v1
33
import (
44
"github.com/zalando-incubator/postgres-operator/pkg/util/config"
55

6+
"time"
7+
68
"github.com/zalando-incubator/postgres-operator/pkg/spec"
79
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8-
"time"
910
)
1011

1112
// +genclient
@@ -99,6 +100,7 @@ type TeamsAPIConfiguration struct {
99100
PamRoleName string `json:"pam_role_name,omitempty"`
100101
PamConfiguration string `json:"pam_configuration,omitempty"`
101102
ProtectedRoles []string `json:"protected_role_names,omitempty"`
103+
PostgresSuperuserTeams []string `json:"postgres_superuser_teams,omitempty"`
102104
}
103105

104106
type LoggingRESTAPIConfiguration struct {

pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/cluster/cluster.go

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -723,19 +723,21 @@ func (c *Cluster) initRobotUsers() error {
723723
return nil
724724
}
725725

726-
func (c *Cluster) initHumanUsers() error {
727-
teamMembers, err := c.getTeamMembers()
726+
func (c *Cluster) initTeamMembers(teamID string, isPostgresSuperuserTeam bool) error {
727+
teamMembers, err := c.getTeamMembers(teamID)
728+
728729
if err != nil {
729-
return fmt.Errorf("could not get list of team members: %v", err)
730+
return fmt.Errorf("could not get list of team members for team %q: %v", teamID, err)
730731
}
732+
731733
for _, username := range teamMembers {
732734
flags := []string{constants.RoleFlagLogin}
733735
memberOf := []string{c.OpConfig.PamRoleName}
734736

735737
if c.shouldAvoidProtectedOrSystemRole(username, "API role") {
736738
continue
737739
}
738-
if c.OpConfig.EnableTeamSuperuser {
740+
if c.OpConfig.EnableTeamSuperuser || isPostgresSuperuserTeam {
739741
flags = append(flags, constants.RoleFlagSuperuser)
740742
} else {
741743
if c.OpConfig.TeamAdminRole != "" {
@@ -761,6 +763,33 @@ func (c *Cluster) initHumanUsers() error {
761763
return nil
762764
}
763765

766+
func (c *Cluster) initHumanUsers() error {
767+
768+
var clusterIsOwnedBySuperuserTeam bool
769+
770+
for _, postgresSuperuserTeam := range c.OpConfig.PostgresSuperuserTeams {
771+
err := c.initTeamMembers(postgresSuperuserTeam, true)
772+
if err != nil {
773+
return fmt.Errorf("Cannot create a team %q of Postgres superusers: %v", postgresSuperuserTeam, err)
774+
}
775+
if postgresSuperuserTeam == c.Spec.TeamID {
776+
clusterIsOwnedBySuperuserTeam = true
777+
}
778+
}
779+
780+
if clusterIsOwnedBySuperuserTeam {
781+
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)
782+
return nil
783+
}
784+
785+
err := c.initTeamMembers(c.Spec.TeamID, false)
786+
if err != nil {
787+
return fmt.Errorf("Cannot create a team %q of admins owning the PG cluster: %v", c.Spec.TeamID, err)
788+
}
789+
790+
return nil
791+
}
792+
764793
func (c *Cluster) initInfrastructureRoles() error {
765794
// add infrastructure roles from the operator's definition
766795
for username, newRole := range c.InfrastructureRoles {

pkg/cluster/cluster_test.go

Lines changed: 145 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@ package cluster
22

33
import (
44
"fmt"
5+
"reflect"
6+
"testing"
7+
58
"github.com/Sirupsen/logrus"
69
acidv1 "github.com/zalando-incubator/postgres-operator/pkg/apis/acid.zalan.do/v1"
710
"github.com/zalando-incubator/postgres-operator/pkg/spec"
811
"github.com/zalando-incubator/postgres-operator/pkg/util/config"
912
"github.com/zalando-incubator/postgres-operator/pkg/util/k8sutil"
1013
"github.com/zalando-incubator/postgres-operator/pkg/util/teams"
1114
"k8s.io/api/core/v1"
12-
"reflect"
13-
"testing"
1415
)
1516

1617
const (
@@ -101,14 +102,17 @@ func (m *mockTeamsAPIClient) setMembers(members []string) {
101102
m.members = members
102103
}
103104

105+
// Test adding a member of a product team owning a particular DB cluster
104106
func TestInitHumanUsers(t *testing.T) {
105107

106108
var mockTeamsAPI mockTeamsAPIClient
107109
cl.oauthTokenGetter = &mockOAuthTokenGetter{}
108110
cl.teamsAPIClient = &mockTeamsAPI
109111
testName := "TestInitHumanUsers"
110112

113+
// members of a product team are granted superuser rights for DBs of their team
111114
cl.OpConfig.EnableTeamSuperuser = true
115+
112116
cl.OpConfig.EnableTeamsAPI = true
113117
cl.OpConfig.PamRoleName = "zalandos"
114118
cl.Spec.TeamID = "test"
@@ -146,6 +150,145 @@ func TestInitHumanUsers(t *testing.T) {
146150
}
147151
}
148152

153+
type mockTeam struct {
154+
teamID string
155+
members []string
156+
isPostgresSuperuserTeam bool
157+
}
158+
159+
type mockTeamsAPIClientMultipleTeams struct {
160+
teams []mockTeam
161+
}
162+
163+
func (m *mockTeamsAPIClientMultipleTeams) TeamInfo(teamID, token string) (tm *teams.Team, err error) {
164+
for _, team := range m.teams {
165+
if team.teamID == teamID {
166+
return &teams.Team{Members: team.members}, nil
167+
}
168+
}
169+
170+
// should not be reached if a slice with teams is populated correctly
171+
return nil, nil
172+
}
173+
174+
// Test adding members of maintenance teams that get superuser rights for all PG databases
175+
func TestInitHumanUsersWithSuperuserTeams(t *testing.T) {
176+
177+
var mockTeamsAPI mockTeamsAPIClientMultipleTeams
178+
cl.oauthTokenGetter = &mockOAuthTokenGetter{}
179+
cl.teamsAPIClient = &mockTeamsAPI
180+
cl.OpConfig.EnableTeamSuperuser = false
181+
testName := "TestInitHumanUsersWithSuperuserTeams"
182+
183+
cl.OpConfig.EnableTeamsAPI = true
184+
cl.OpConfig.PamRoleName = "zalandos"
185+
186+
teamA := mockTeam{
187+
teamID: "postgres_superusers",
188+
members: []string{"postgres_superuser"},
189+
isPostgresSuperuserTeam: true,
190+
}
191+
192+
userA := spec.PgUser{
193+
Name: "postgres_superuser",
194+
Origin: spec.RoleOriginTeamsAPI,
195+
MemberOf: []string{cl.OpConfig.PamRoleName},
196+
Flags: []string{"LOGIN", "SUPERUSER"},
197+
}
198+
199+
teamB := mockTeam{
200+
teamID: "postgres_admins",
201+
members: []string{"postgres_admin"},
202+
isPostgresSuperuserTeam: true,
203+
}
204+
205+
userB := spec.PgUser{
206+
Name: "postgres_admin",
207+
Origin: spec.RoleOriginTeamsAPI,
208+
MemberOf: []string{cl.OpConfig.PamRoleName},
209+
Flags: []string{"LOGIN", "SUPERUSER"},
210+
}
211+
212+
teamTest := mockTeam{
213+
teamID: "test",
214+
members: []string{"test_user"},
215+
isPostgresSuperuserTeam: false,
216+
}
217+
218+
userTest := spec.PgUser{
219+
Name: "test_user",
220+
Origin: spec.RoleOriginTeamsAPI,
221+
MemberOf: []string{cl.OpConfig.PamRoleName},
222+
Flags: []string{"LOGIN"},
223+
}
224+
225+
tests := []struct {
226+
ownerTeam string
227+
existingRoles map[string]spec.PgUser
228+
superuserTeams []string
229+
teams []mockTeam
230+
result map[string]spec.PgUser
231+
}{
232+
// case 1: there are two different teams of PG maintainers and one product team
233+
{
234+
ownerTeam: "test",
235+
existingRoles: map[string]spec.PgUser{},
236+
superuserTeams: []string{"postgres_superusers", "postgres_admins"},
237+
teams: []mockTeam{teamA, teamB, teamTest},
238+
result: map[string]spec.PgUser{
239+
"postgres_superuser": userA,
240+
"postgres_admin": userB,
241+
"test_user": userTest,
242+
},
243+
},
244+
// case 2: the team of superusers creates a new PG cluster
245+
{
246+
ownerTeam: "postgres_superusers",
247+
existingRoles: map[string]spec.PgUser{},
248+
superuserTeams: []string{"postgres_superusers"},
249+
teams: []mockTeam{teamA},
250+
result: map[string]spec.PgUser{
251+
"postgres_superuser": userA,
252+
},
253+
},
254+
// case 3: the team owning the cluster is promoted to the maintainers' status
255+
{
256+
ownerTeam: "postgres_superusers",
257+
existingRoles: map[string]spec.PgUser{
258+
// role with the name exists before w/o superuser privilege
259+
"postgres_superuser": spec.PgUser{
260+
Origin: spec.RoleOriginTeamsAPI,
261+
Name: "postgres_superuser",
262+
Password: "",
263+
Flags: []string{"LOGIN"},
264+
MemberOf: []string{cl.OpConfig.PamRoleName},
265+
Parameters: map[string]string(nil)}},
266+
superuserTeams: []string{"postgres_superusers"},
267+
teams: []mockTeam{teamA},
268+
result: map[string]spec.PgUser{
269+
"postgres_superuser": userA,
270+
},
271+
},
272+
}
273+
274+
for _, tt := range tests {
275+
276+
mockTeamsAPI.teams = tt.teams
277+
278+
cl.Spec.TeamID = tt.ownerTeam
279+
cl.pgUsers = tt.existingRoles
280+
cl.OpConfig.PostgresSuperuserTeams = tt.superuserTeams
281+
282+
if err := cl.initHumanUsers(); err != nil {
283+
t.Errorf("%s got an unexpected error %v", testName, err)
284+
}
285+
286+
if !reflect.DeepEqual(cl.pgUsers, tt.result) {
287+
t.Errorf("%s expects %#v, got %#v", testName, tt.result, cl.pgUsers)
288+
}
289+
}
290+
}
291+
149292
func TestShouldDeleteSecret(t *testing.T) {
150293
testName := "TestShouldDeleteSecret"
151294

pkg/cluster/util.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -210,12 +210,14 @@ func (c *Cluster) logVolumeChanges(old, new acidv1.Volume) {
210210
c.logger.Debugf("diff\n%s\n", util.PrettyDiff(old, new))
211211
}
212212

213-
func (c *Cluster) getTeamMembers() ([]string, error) {
214-
if c.Spec.TeamID == "" {
213+
func (c *Cluster) getTeamMembers(teamID string) ([]string, error) {
214+
215+
if teamID == "" {
215216
return nil, fmt.Errorf("no teamId specified")
216217
}
218+
217219
if !c.OpConfig.EnableTeamsAPI {
218-
c.logger.Debug("team API is disabled, returning empty list of members")
220+
c.logger.Debugf("team API is disabled, returning empty list of members for team %q", teamID)
219221
return []string{}, nil
220222
}
221223

@@ -225,9 +227,9 @@ func (c *Cluster) getTeamMembers() ([]string, error) {
225227
return []string{}, nil
226228
}
227229

228-
teamInfo, err := c.teamsAPIClient.TeamInfo(c.Spec.TeamID, token)
230+
teamInfo, err := c.teamsAPIClient.TeamInfo(teamID, token)
229231
if err != nil {
230-
c.logger.Warnf("could not get team info, returning empty list of team members: %v", err)
232+
c.logger.Warnf("could not get team info for team %q, returning empty list of team members: %v", teamID, err)
231233
return []string{}, nil
232234
}
233235

pkg/controller/operator_config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
8282
result.EnableTeamSuperuser = fromCRD.TeamsAPI.EnableTeamSuperuser
8383
result.TeamAdminRole = fromCRD.TeamsAPI.TeamAdminRole
8484
result.PamRoleName = fromCRD.TeamsAPI.PamRoleName
85+
result.PostgresSuperuserTeams = fromCRD.TeamsAPI.PostgresSuperuserTeams
8586

8687
result.APIPort = fromCRD.LoggingRESTAPI.APIPort
8788
result.RingLogLines = fromCRD.LoggingRESTAPI.RingLogLines

pkg/spec/types.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const fileWithNamespace = "/var/run/secrets/kubernetes.io/serviceaccount/namespa
2323
// RoleOrigin contains the code of the origin of a role
2424
type RoleOrigin int
2525

26-
// The rolesOrigin constant values should be sorted by the role priority.
26+
// The rolesOrigin constant values must be sorted by the role priority for resolveNameConflict(...) to work.
2727
const (
2828
RoleOriginUnknown RoleOrigin = iota
2929
RoleOriginManifest

pkg/util/config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ type Config struct {
103103
TeamAPIRoleConfiguration map[string]string `name:"team_api_role_configuration" default:"log_statement:all"`
104104
PodTerminateGracePeriod time.Duration `name:"pod_terminate_grace_period" default:"5m"`
105105
ProtectedRoles []string `name:"protected_role_names" default:"admin"`
106+
PostgresSuperuserTeams []string `name:"postgres_superuser_teams" default:""`
106107
}
107108

108109
// MustMarshal marshals the config or panics

0 commit comments

Comments
 (0)