Skip to content

Commit baa3022

Browse files
authored
Merge pull request #5 from hoppscale/add-database-privileges
feat(database): add support of roles privileges for databases
2 parents f2b35fd + 5b1b16a commit baa3022

File tree

7 files changed

+344
-0
lines changed

7 files changed

+344
-0
lines changed

api/v1alpha1/postgresdatabase_types.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ import (
2020
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2121
)
2222

23+
// PostgresDatabasePrivilegesSpec defines the desired database privileges to grant to roles
24+
type PostgresDatabasePrivilegesSpec struct {
25+
Create bool `json:"create,omitempty"`
26+
Connect bool `json:"connect,omitempty"`
27+
Temporary bool `json:"temporary,omitempty"`
28+
}
29+
2330
// PostgresDatabaseSpec defines the desired state of PostgresDatabase.
2431
type PostgresDatabaseSpec struct {
2532

@@ -39,6 +46,9 @@ type PostgresDatabaseSpec struct {
3946

4047
// PreserveConnectionsOnDelete will determine if the deletion of the object should drop the existing connections to the remote PostgreSQL database. Default is false.
4148
PreserveConnectionsOnDelete bool `json:"preserveConnectionsOnDelete,omitempty"`
49+
50+
// PrivilegesByRole will grant privileges to roles
51+
PrivilegesByRole map[string]PostgresDatabasePrivilegesSpec `json:"privilegesByRole,omitempty"`
4252
}
4353

4454
// PostgresDatabaseStatus defines the observed state of PostgresDatabase.

api/v1alpha1/zz_generated.deepcopy.go

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

config/crd/bases/managed-postgres-operator.hoppscale.com_postgresdatabases.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,20 @@ spec:
6565
of the object should drop the existing connections to the remote
6666
PostgreSQL database. Default is false.
6767
type: boolean
68+
privilegesByRole:
69+
additionalProperties:
70+
description: PostgresDatabasePrivilegesSpec defines the desired
71+
state of PostgresDatabase.
72+
properties:
73+
connect:
74+
type: boolean
75+
create:
76+
type: boolean
77+
temporary:
78+
type: boolean
79+
type: object
80+
description: PrivilegesByRole will grant privileges to roles
81+
type: object
6882
required:
6983
- name
7084
type: object

internal/controller/postgresdatabase_controller.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package controller
33
import (
44
"context"
55
"fmt"
6+
"slices"
67
"time"
78

89
"github.com/go-logr/logr"
@@ -107,6 +108,17 @@ func (r *PostgresDatabaseReconciler) Reconcile(ctx context.Context, req ctrl.Req
107108
return ctrlFailResult, err
108109
}
109110

111+
for roleName, rolePrivileges := range resource.Spec.PrivilegesByRole {
112+
err = r.reconcilePrivileges(
113+
desiredDatabase.Name,
114+
roleName,
115+
r.convertPrivilegesSpecToList(rolePrivileges),
116+
)
117+
if err != nil {
118+
return ctrlFailResult, err
119+
}
120+
}
121+
110122
if !resource.Status.Succeeded {
111123
resource.Status.Succeeded = true
112124
if err = r.Client.Status().Update(context.Background(), resource); err != nil {
@@ -243,3 +255,54 @@ func (r *PostgresDatabaseReconciler) reconcileExtensions(database *postgresql.Da
243255
}
244256
return err
245257
}
258+
259+
// reconcilePrivileges performs all actions related to the database privileges for a single role
260+
func (r *PostgresDatabaseReconciler) reconcilePrivileges(databaseName, roleName string, desiredPrivileges []string) (err error) {
261+
// We retrieve the existing privileges
262+
existingPrivileges, err := postgresql.GetDatabaseRolePrivileges(r.PGPools.Default, databaseName, roleName)
263+
if err != nil {
264+
r.logging.Error(err, "failed to retrieve privileges of database \"%s\" on role \"%s\": %s", databaseName, roleName, err)
265+
return err
266+
}
267+
268+
// We grant the missing privileges
269+
for _, desiredPrivilege := range desiredPrivileges {
270+
if !slices.Contains(existingPrivileges, desiredPrivilege) {
271+
err := postgresql.GrantDatabaseRolePrivilege(r.PGPools.Default, databaseName, roleName, desiredPrivilege)
272+
if err != nil {
273+
r.logging.Error(err, "failed to grant \"%s\" privilege on database \"%s\" to role \"%s\"", desiredPrivilege, databaseName, roleName)
274+
return err
275+
}
276+
277+
r.logging.Info(fmt.Sprintf("Privilege \"%s\" has been granted to \"%s\" on database \"%s\"", desiredPrivilege, roleName, databaseName))
278+
}
279+
}
280+
281+
// We revoke the non-declared privileges
282+
for _, existingPrivilege := range existingPrivileges {
283+
if !slices.Contains(desiredPrivileges, existingPrivilege) {
284+
err := postgresql.RevokeDatabaseRolePrivilege(r.PGPools.Default, databaseName, roleName, existingPrivilege)
285+
if err != nil {
286+
r.logging.Error(err, "failed to revoke \"%s\" privilege on database \"%s\" to role \"%s\"", existingPrivilege, databaseName, roleName)
287+
return err
288+
}
289+
290+
r.logging.Info(fmt.Sprintf("Privilege \"%s\" has been revoked from \"%s\" on database \"%s\"", existingPrivilege, roleName, databaseName))
291+
}
292+
}
293+
return err
294+
}
295+
296+
func (r *PostgresDatabaseReconciler) convertPrivilegesSpecToList(privilegesSpec managedpostgresoperatorhoppscalecomv1alpha1.PostgresDatabasePrivilegesSpec) []string {
297+
privileges := []string{}
298+
if privilegesSpec.Create {
299+
privileges = append(privileges, "CREATE")
300+
}
301+
if privilegesSpec.Connect {
302+
privileges = append(privileges, "CONNECT")
303+
}
304+
if privilegesSpec.Temporary {
305+
privileges = append(privileges, "TEMPORARY")
306+
}
307+
return privileges
308+
}

internal/controller/postgresdatabase_controller_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,85 @@ var _ = Describe("PostgresDatabase Controller", func() {
627627
}
628628
}
629629
})
630+
})
631+
632+
When("reconciling privileges", func() {
633+
It("should grant privileges to role 'myrole'", func() {
634+
controllerReconciler := &PostgresDatabaseReconciler{
635+
Client: k8sClient,
636+
Scheme: k8sClient.Scheme(),
637+
PGPools: pgpools,
638+
}
639+
640+
// Loop over all privileges
641+
for _, privilege := range postgresql.ListDatabaseAvailablePrivileges() {
642+
pgpoolsMock["default"].ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta(`SELECT has_database_privilege($1, $2, $3)`))).
643+
WithArgs("myrole", "mydb", privilege).
644+
WillReturnRows(
645+
pgxmock.NewRows([]string{
646+
"changeme",
647+
}).
648+
AddRow(
649+
false,
650+
),
651+
)
652+
}
653+
pgpoolsMock["default"].ExpectExec(fmt.Sprintf("^%s$", regexp.QuoteMeta(`GRANT CREATE ON DATABASE "mydb" TO "myrole"`))).
654+
WillReturnResult(pgxmock.NewResult("", 1))
655+
656+
err := controllerReconciler.reconcilePrivileges(
657+
"mydb",
658+
"myrole",
659+
[]string{
660+
"CREATE",
661+
},
662+
)
663+
Expect(err).NotTo(HaveOccurred())
664+
for _, poolMock := range pgpoolsMock {
665+
if err := poolMock.ExpectationsWereMet(); err != nil {
666+
Fail(err.Error())
667+
}
668+
}
669+
})
670+
671+
It("should revoke privileges to role 'myrole'", func() {
672+
controllerReconciler := &PostgresDatabaseReconciler{
673+
Client: k8sClient,
674+
Scheme: k8sClient.Scheme(),
675+
PGPools: pgpools,
676+
}
677+
678+
// Loop over all privileges
679+
for _, privilege := range postgresql.ListDatabaseAvailablePrivileges() {
680+
pgpoolsMock["default"].ExpectQuery(fmt.Sprintf("^%s$", regexp.QuoteMeta(`SELECT has_database_privilege($1, $2, $3)`))).
681+
WithArgs("myrole", "mydb", privilege).
682+
WillReturnRows(
683+
pgxmock.NewRows([]string{
684+
"changeme",
685+
}).
686+
AddRow(
687+
true,
688+
),
689+
)
690+
}
691+
pgpoolsMock["default"].ExpectExec(fmt.Sprintf("^%s$", regexp.QuoteMeta(`REVOKE CREATE ON DATABASE "mydb" FROM "myrole"`))).
692+
WillReturnResult(pgxmock.NewResult("", 1))
693+
694+
err := controllerReconciler.reconcilePrivileges(
695+
"mydb",
696+
"myrole",
697+
[]string{
698+
"CONNECT",
699+
"TEMPORARY",
700+
},
701+
)
702+
Expect(err).NotTo(HaveOccurred())
703+
for _, poolMock := range pgpoolsMock {
704+
if err := poolMock.ExpectationsWereMet(); err != nil {
705+
Fail(err.Error())
706+
}
707+
}
708+
})
630709

631710
})
632711
})

internal/postgresql/database.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,59 @@ func DropDatabaseConnections(pgpool PGPoolInterface, name string) (err error) {
116116
}
117117
return
118118
}
119+
120+
func ListDatabaseAvailablePrivileges() []string {
121+
return []string{
122+
"CREATE",
123+
"CONNECT",
124+
"TEMPORARY",
125+
}
126+
}
127+
128+
func GetDatabaseRolePrivileges(pgpool PGPoolInterface, database, role string) (existingPrivileges []string, err error) {
129+
existingPrivileges = []string{}
130+
var hasPrivilege bool
131+
for _, privilege := range ListDatabaseAvailablePrivileges() {
132+
rows, err := pgpool.Query(context.Background(), "SELECT has_database_privilege($1, $2, $3)", role, database, privilege)
133+
if err != nil {
134+
err = fmt.Errorf("pg query failed: %s", err)
135+
return []string{}, err
136+
}
137+
defer rows.Close()
138+
139+
hasPrivilege, err = pgx.CollectOneRow(rows, pgx.RowTo[bool])
140+
if err != nil {
141+
err = fmt.Errorf("failed to collect rows: %s", err)
142+
return []string{}, err
143+
}
144+
145+
if hasPrivilege {
146+
existingPrivileges = append(existingPrivileges, privilege)
147+
}
148+
}
149+
return
150+
}
151+
152+
func GrantDatabaseRolePrivilege(pgpool PGPoolInterface, database, role, privilege string) (err error) {
153+
sanitizedDatabase := pgx.Identifier{database}.Sanitize()
154+
sanitizedRole := pgx.Identifier{role}.Sanitize()
155+
156+
_, err = pgpool.Exec(context.Background(), fmt.Sprintf("GRANT %s ON DATABASE %s TO %s", privilege, sanitizedDatabase, sanitizedRole))
157+
if err != nil {
158+
return fmt.Errorf("failed to grant privilege \"%s\" on database %s to role %s: %s", privilege, sanitizedDatabase, sanitizedRole, err)
159+
}
160+
161+
return
162+
}
163+
164+
func RevokeDatabaseRolePrivilege(pgpool PGPoolInterface, database, role, privilege string) (err error) {
165+
sanitizedDatabase := pgx.Identifier{database}.Sanitize()
166+
sanitizedRole := pgx.Identifier{role}.Sanitize()
167+
168+
_, err = pgpool.Exec(context.Background(), fmt.Sprintf("REVOKE %s ON DATABASE %s FROM %s", privilege, sanitizedDatabase, sanitizedRole))
169+
if err != nil {
170+
return fmt.Errorf("failed to revoke privilege \"%s\" on database %s from role %s: %s", privilege, sanitizedDatabase, sanitizedRole, err)
171+
}
172+
173+
return
174+
}

0 commit comments

Comments
 (0)