Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions charts/postgres-operator/crds/operatorconfigurations.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@ spec:
users:
type: object
properties:
additional_owner_roles:
type: array
nullable: true
items:
type: string
enable_password_rotation:
type: boolean
default: false
Expand Down Expand Up @@ -500,6 +505,7 @@ spec:
type: string
default:
- admin
- cron_admin
role_deletion_suffix:
type: string
default: "_deleted"
Expand Down
11 changes: 11 additions & 0 deletions charts/postgres-operator/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@ configGeneral:

# parameters describing Postgres users
configUsers:
# roles to be granted to database owners
# additional_owner_roles:
# - cron_admin

# enable password rotation for app users that are not database owners
enable_password_rotation: false
# rotation interval for updating credentials in K8s secrets of app users
password_rotation_interval: 90
# retention interval to keep rotation users
password_rotation_user_retention: 180
# postgres username used for replication between instances
replication_username: standby
# postgres superuser name to be created by initdb
Expand Down Expand Up @@ -338,6 +348,7 @@ configTeamsApi:
# List of roles that cannot be overwritten by an application, team or infrastructure role
protected_role_names:
- admin
- cron_admin
# Suffix to add if members are removed from TeamsAPI or PostgresTeam CRD
role_deletion_suffix: "_deleted"
# role name to grant to team members created from the Teams API
Expand Down
11 changes: 10 additions & 1 deletion docs/reference/operator_parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,15 @@ under the `users` key.
Postgres username used for replication between instances. The default is
`standby`.

* **additional_owner_roles**
Specifies database roles that will become members of all database owners.
Then owners can use `SET ROLE` to obtain privileges of these roles to e.g.
create/update functionality from extensions as part of a migration script.
Note, that roles listed here should be preconfigured in the docker image
and already exist in the database cluster on startup. One such role can be
`cron_admin` which is provided by the Spilo docker image to set up cron
jobs inside the `postgres` database. Default is `empty`.

* **enable_password_rotation**
For all `LOGIN` roles that are not database owners the operator can rotate
credentials in the corresponding K8s secrets by replacing the username and
Expand Down Expand Up @@ -748,7 +757,7 @@ key.

* **protected_role_names**
List of roles that cannot be overwritten by an application, team or
infrastructure role. The default is `admin`.
infrastructure role. The default list is `admin` and `cron_admin`.

* **postgres_superuser_teams**
List of teams which members need the superuser role in each PG database
Expand Down
31 changes: 31 additions & 0 deletions e2e/tests/test_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,37 @@ def setUpClass(cls):
print('Operator log: {}'.format(k8s.get_operator_log()))
raise

@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
def test_additional_owner_roles(self):
'''
Test adding additional member roles to existing database owner roles
'''
k8s = self.k8s

# enable PostgresTeam CRD and lower resync
owner_roles = {
"data": {
"additional_owner_roles": "cron_admin",
},
}
k8s.update_config(owner_roles)
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"},
"Operator does not get in sync")

leader = k8s.get_cluster_leader_pod()
owner_query = """
SELECT a2.rolname
FROM pg_catalog.pg_authid a
JOIN pg_catalog.pg_auth_members am
ON a.oid = am.member
AND a.rolname = 'cron_admin'
JOIN pg_catalog.pg_authid a2
ON a2.oid = am.roleid
WHERE a2.rolname IN ('zalando', 'bar_owner', 'bar_data_owner');
"""
self.eventuallyEqual(lambda: len(self.query_database(leader.metadata.name, "postgres", owner_query)), 3,
"Not all additional users found in database", 10, 5)

@timeout_decorator.timeout(TEST_TIMEOUT_SEC)
def test_additional_pod_capabilities(self):
'''
Expand Down
3 changes: 2 additions & 1 deletion manifests/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ kind: ConfigMap
metadata:
name: postgres-operator
data:
# additional_owner_roles: "cron_admin"
# additional_pod_capabilities: "SYS_NICE"
# additional_secret_mount: "some-secret-name"
# additional_secret_mount_path: "/some/dir"
Expand Down Expand Up @@ -109,7 +110,7 @@ data:
# pod_service_account_role_binding_definition: ""
pod_terminate_grace_period: 5m
# postgres_superuser_teams: "postgres_superusers"
# protected_role_names: "admin"
# protected_role_names: "admin,cron_admin"
ready_wait_interval: 3s
ready_wait_timeout: 30s
repair_period: 5m
Expand Down
6 changes: 6 additions & 0 deletions manifests/operatorconfiguration.crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ spec:
users:
type: object
properties:
additional_owner_roles:
type: array
nullable: true
items:
type: string
enable_password_rotation:
type: boolean
default: false
Expand Down Expand Up @@ -498,6 +503,7 @@ spec:
type: string
default:
- admin
- cron_admin
role_deletion_suffix:
type: string
default: "_deleted"
Expand Down
3 changes: 3 additions & 0 deletions manifests/postgresql-operator-default-configuration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ configuration:
# protocol: TCP
workers: 8
users:
# additional_owner_roles:
# - cron_admin
enable_password_rotation: false
password_rotation_interval: 90
password_rotation_user_retention: 180
Expand Down Expand Up @@ -163,6 +165,7 @@ configuration:
# - postgres_superusers
protected_role_names:
- admin
- cron_admin
role_deletion_suffix: "_deleted"
team_admin_role: admin
team_api_role_configuration:
Expand Down
18 changes: 18 additions & 0 deletions pkg/apis/acid.zalan.do/v1/crds.go
Original file line number Diff line number Diff line change
Expand Up @@ -1142,6 +1142,24 @@ var OperatorConfigCRDResourceValidation = apiextv1.CustomResourceValidation{
"users": {
Type: "object",
Properties: map[string]apiextv1.JSONSchemaProps{
"additional_owner_roles": {
Type: "array",
Nullable: true,
Items: &apiextv1.JSONSchemaPropsOrArray{
Schema: &apiextv1.JSONSchemaProps{
Type: "string",
},
},
},
"enable_password_rotation": {
Type: "boolean",
},
"password_rotation_interval": {
Type: "integer",
},
"password_rotation_user_retention": {
Type: "integer",
},
"replication_username": {
Type: "string",
},
Expand Down
11 changes: 6 additions & 5 deletions pkg/apis/acid.zalan.do/v1/operator_configuration_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,12 @@ type OperatorConfigurationList struct {

// PostgresUsersConfiguration defines the system users of Postgres.
type PostgresUsersConfiguration struct {
SuperUsername string `json:"super_username,omitempty"`
ReplicationUsername string `json:"replication_username,omitempty"`
EnablePasswordRotation bool `json:"enable_password_rotation,omitempty"`
PasswordRotationInterval uint32 `json:"password_rotation_interval,omitempty"`
PasswordRotationUserRetention uint32 `json:"password_rotation_user_retention,omitempty"`
SuperUsername string `json:"super_username,omitempty"`
ReplicationUsername string `json:"replication_username,omitempty"`
AdditionalOwnerRoles []string `json:"additional_owner_roles,omitempty"`
EnablePasswordRotation bool `json:"enable_password_rotation,omitempty"`
PasswordRotationInterval uint32 `json:"password_rotation_interval,omitempty"`
PasswordRotationUserRetention uint32 `json:"password_rotation_user_retention,omitempty"`
}

// MajorVersionUpgradeConfiguration defines how to execute major version upgrades of Postgres.
Expand Down
7 changes: 6 additions & 1 deletion pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 29 additions & 0 deletions pkg/cluster/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,8 @@ func (c *Cluster) initUsers() error {
return fmt.Errorf("could not init human users: %v", err)
}

c.initAdditionalOwnerRoles()

return nil
}

Expand Down Expand Up @@ -1297,6 +1299,33 @@ func (c *Cluster) initRobotUsers() error {
return nil
}

func (c *Cluster) initAdditionalOwnerRoles() {
for _, additionalOwner := range c.OpConfig.AdditionalOwnerRoles {
// fetch all database owners the additional should become a member of
memberOf := make([]string, 0)
for username, pgUser := range c.pgUsers {
if pgUser.IsDbOwner {
memberOf = append(memberOf, username)
}
}

if len(memberOf) > 1 {
namespace := c.Namespace
additionalOwnerPgUser := spec.PgUser{
Origin: spec.RoleOriginSpilo,
MemberOf: memberOf,
Name: additionalOwner,
Namespace: namespace,
}
if currentRole, present := c.pgUsers[additionalOwner]; present {
c.pgUsers[additionalOwner] = c.resolveNameConflict(&currentRole, &additionalOwnerPgUser)
} else {
c.pgUsers[additionalOwner] = additionalOwnerPgUser
}
}
}
}

func (c *Cluster) initTeamMembers(teamID string, isPostgresSuperuserTeam bool) error {
teamMembers, err := c.getTeamMembers(teamID)

Expand Down
48 changes: 44 additions & 4 deletions pkg/cluster/cluster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,25 @@ var cl = New(
Config{
OpConfig: config.Config{
PodManagementPolicy: "ordered_ready",
ProtectedRoles: []string{"admin"},
ProtectedRoles: []string{"admin", "cron_admin", "part_man"},
Auth: config.Auth{
SuperUsername: superUserName,
ReplicationUsername: replicationUserName,
SuperUsername: superUserName,
ReplicationUsername: replicationUserName,
AdditionalOwnerRoles: []string{"cron_admin", "part_man"},
},
Resources: config.Resources{
DownscalerAnnotations: []string{"downscaler/*"},
},
},
},
k8sutil.NewMockKubernetesClient(),
acidv1.Postgresql{ObjectMeta: metav1.ObjectMeta{Name: "acid-test", Namespace: "test", Annotations: map[string]string{"downscaler/downtime_replicas": "0"}}},
acidv1.Postgresql{
ObjectMeta: metav1.ObjectMeta{
Name: "acid-test",
Namespace: "test",
Annotations: map[string]string{"downscaler/downtime_replicas": "0"},
},
},
logger,
eventRecorder,
)
Expand Down Expand Up @@ -132,6 +139,39 @@ func TestInitRobotUsers(t *testing.T) {
}
}

func TestInitAdditionalOwnerRoles(t *testing.T) {
testName := "TestInitAdditionalOwnerRoles"

manifestUsers := map[string]acidv1.UserFlags{"foo_owner": {}, "bar_owner": {}, "app_user": {}}
expectedUsers := map[string]spec.PgUser{
"foo_owner": {Origin: spec.RoleOriginManifest, Name: "foo_owner", Namespace: cl.Namespace, Password: "f123", Flags: []string{"LOGIN"}, IsDbOwner: true},
"bar_owner": {Origin: spec.RoleOriginManifest, Name: "bar_owner", Namespace: cl.Namespace, Password: "b123", Flags: []string{"LOGIN"}, IsDbOwner: true},
"app_user": {Origin: spec.RoleOriginManifest, Name: "app_user", Namespace: cl.Namespace, Password: "a123", Flags: []string{"LOGIN"}, IsDbOwner: false},
"cron_admin": {Origin: spec.RoleOriginSpilo, Name: "cron_admin", Namespace: cl.Namespace, MemberOf: []string{"foo_owner", "bar_owner"}},
"part_man": {Origin: spec.RoleOriginSpilo, Name: "part_man", Namespace: cl.Namespace, MemberOf: []string{"foo_owner", "bar_owner"}},
}

cl.Spec.Databases = map[string]string{"foo_db": "foo_owner", "bar_db": "bar_owner"}
cl.Spec.Users = manifestUsers

// this should set IsDbOwner field for manifest users
if err := cl.initRobotUsers(); err != nil {
t.Errorf("%s could not init manifest users", testName)
}

// update passwords to compare with result
for manifestUser := range manifestUsers {
pgUser := cl.pgUsers[manifestUser]
pgUser.Password = manifestUser[0:1] + "123"
cl.pgUsers[manifestUser] = pgUser
}

cl.initAdditionalOwnerRoles()
if !reflect.DeepEqual(cl.pgUsers, expectedUsers) {
t.Errorf("%s expected: %#v, got %#v", testName, expectedUsers, cl.pgUsers)
}
}

type mockOAuthTokenGetter struct {
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/cluster/k8sres.go
Original file line number Diff line number Diff line change
Expand Up @@ -1622,7 +1622,7 @@ func (c *Cluster) generateUserSecrets() map[string]*v1.Secret {
func (c *Cluster) generateSingleUserSecret(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 == "" {
if pgUser.Origin != spec.RoleOriginTeamsAPI {
if pgUser.Origin != spec.RoleOriginTeamsAPI && pgUser.Origin != spec.RoleOriginSpilo {
c.logger.Warningf("could not generate secret for a non-teamsAPI role %q: role has no password",
pgUser.Name)
}
Expand Down
3 changes: 2 additions & 1 deletion pkg/controller/operator_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
// user config
result.SuperUsername = util.Coalesce(fromCRD.PostgresUsersConfiguration.SuperUsername, "postgres")
result.ReplicationUsername = util.Coalesce(fromCRD.PostgresUsersConfiguration.ReplicationUsername, "standby")
result.AdditionalOwnerRoles = fromCRD.PostgresUsersConfiguration.AdditionalOwnerRoles
result.EnablePasswordRotation = fromCRD.PostgresUsersConfiguration.EnablePasswordRotation
result.PasswordRotationInterval = util.CoalesceUInt32(fromCRD.PostgresUsersConfiguration.PasswordRotationInterval, 90)
result.PasswordRotationUserRetention = util.CoalesceUInt32(fromCRD.PostgresUsersConfiguration.DeepCopy().PasswordRotationUserRetention, 180)
Expand Down Expand Up @@ -186,7 +187,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
result.TeamAdminRole = fromCRD.TeamsAPI.TeamAdminRole
result.PamRoleName = util.Coalesce(fromCRD.TeamsAPI.PamRoleName, "zalandos")
result.PamConfiguration = util.Coalesce(fromCRD.TeamsAPI.PamConfiguration, "https://info.example.com/oauth2/tokeninfo?access_token= uid realm=/employees")
result.ProtectedRoles = util.CoalesceStrArr(fromCRD.TeamsAPI.ProtectedRoles, []string{"admin"})
result.ProtectedRoles = util.CoalesceStrArr(fromCRD.TeamsAPI.ProtectedRoles, []string{"admin", "cron_admin"})
result.PostgresSuperuserTeams = fromCRD.TeamsAPI.PostgresSuperuserTeams
result.EnablePostgresTeamCRD = fromCRD.TeamsAPI.EnablePostgresTeamCRD
result.EnablePostgresTeamCRDSuperusers = fromCRD.TeamsAPI.EnablePostgresTeamCRDSuperusers
Expand Down
1 change: 1 addition & 0 deletions pkg/spec/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const (
RoleOriginManifest
RoleOriginInfrastructure
RoleOriginTeamsAPI
RoleOriginSpilo
RoleOriginSystem
RoleOriginBootstrap
RoleConnectionPooler
Expand Down
3 changes: 2 additions & 1 deletion pkg/util/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ type Auth struct {
InfrastructureRolesDefs string `name:"infrastructure_roles_secrets"`
SuperUsername string `name:"super_username" default:"postgres"`
ReplicationUsername string `name:"replication_username" default:"standby"`
AdditionalOwnerRoles []string `name:"additional_owner_roles" default:""`
EnablePasswordRotation bool `name:"enable_password_rotation" default:"false"`
PasswordRotationInterval uint32 `name:"password_rotation_interval" default:"90"`
PasswordRotationUserRetention uint32 `name:"password_rotation_user_retention" default:"180"`
Expand Down Expand Up @@ -210,7 +211,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"`
PodManagementPolicy string `name:"pod_management_policy" default:"ordered_ready"`
ProtectedRoles []string `name:"protected_role_names" default:"admin"`
ProtectedRoles []string `name:"protected_role_names" default:"admin,cron_admin"`
PostgresSuperuserTeams []string `name:"postgres_superuser_teams" default:""`
SetMemoryRequestToLimit bool `name:"set_memory_request_to_limit" default:"false"`
EnableLazySpiloUpgrade bool `name:"enable_lazy_spilo_upgrade" default:"false"`
Expand Down
Loading