Skip to content

Commit 8b280b0

Browse files
committed
feat: add grant specs schema, objects, objectType
1 parent 23bec13 commit 8b280b0

File tree

6 files changed

+140
-40
lines changed

6 files changed

+140
-40
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@
88
.work
99
_output
1010
__debug_bin
11+
.tool-versions

apis/postgresql/v1alpha1/grant_types.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ type GrantPrivileges []GrantPrivilege
4747
// happen internally inside postgresql when making grants. When we query the
4848
// privileges back, we need to look for the expanded set.
4949
// https://www.postgresql.org/docs/15/ddl-priv.html
50+
// TODO: Grand ALL ON SCHEMA should be expanded to GRANT USAGE, CREATE ON SCHEMA
5051
var grantReplacements = map[GrantPrivilege]GrantPrivileges{
5152
"ALL": {"CREATE", "TEMPORARY", "CONNECT"},
5253
"ALL PRIVILEGES": {"CREATE", "TEMPORARY", "CONNECT"},
@@ -172,6 +173,22 @@ type GrantParameters struct {
172173
// RevokePublicOnDb apply the statement "REVOKE ALL ON DATABASE %s FROM PUBLIC" to make database unreachable from public
173174
// +optional
174175
RevokePublicOnDb *bool `json:"revokePublicOnDb,omitempty" default:"false"`
176+
177+
// ObjectType is the PostgreSQL object type to grant the privileges on.
178+
// +kubebuilder:validation:Enum=database;schema;table;sequence;function;procedure;routine;foreign_data_wrapper;foreign_server;column
179+
ObjectType string `json:"objectType" default:"database"`
180+
181+
// Objects are the objects upon which to grant the privileges.
182+
// An empty list (the default) means to grant permissions on all objects of the specified type.
183+
// You cannot specify this option if the objectType is database or schema.
184+
// When objectType is column, only one value is allowed.
185+
// +optional
186+
Objects []string `json:"objects,omitempty"`
187+
188+
// The columns upon which to grant the privileges.
189+
// Required when object_type is column. You cannot specify this option if the object_type is not column
190+
// +optional
191+
Columns []string `json:"columns,omitempty"`
175192
}
176193

177194
// A GrantStatus represents the observed state of a Grant.

apis/postgresql/v1alpha1/zz_generated.deepcopy.go

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

package/crds/postgresql.sql.crossplane.io_grants.yaml

Lines changed: 56 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ spec:
8383
description: GrantParameters define the desired state of a PostgreSQL
8484
grant instance.
8585
properties:
86+
columns:
87+
description: |-
88+
The columns upon which to grant the privileges.
89+
Required when object_type is column. You cannot specify this option if the object_type is not column
90+
items:
91+
type: string
92+
type: array
8693
database:
8794
description: Database this grant is for.
8895
type: string
@@ -242,6 +249,30 @@ spec:
242249
type: string
243250
type: object
244251
type: object
252+
objectType:
253+
description: ObjectType is the PostgreSQL object type to grant
254+
the privileges on.
255+
enum:
256+
- database
257+
- schema
258+
- table
259+
- sequence
260+
- function
261+
- procedure
262+
- routine
263+
- foreign_data_wrapper
264+
- foreign_server
265+
- column
266+
type: string
267+
objects:
268+
description: |-
269+
Objects are the objects upon which to grant the privileges.
270+
An empty list (the default) means to grant permissions on all objects of the specified type.
271+
You cannot specify this option if the objectType is database or schema.
272+
When objectType is column, only one value is allowed.
273+
items:
274+
type: string
275+
type: array
245276
privileges:
246277
description: |-
247278
Privileges to be granted.
@@ -351,21 +382,21 @@ spec:
351382
properties:
352383
resolution:
353384
default: Required
354-
description: Resolution specifies whether resolution of
355-
this reference is required. The default is 'Required',
356-
which means the reconcile will fail if the reference
357-
cannot be resolved. 'Optional' means this reference
358-
will be a no-op if it cannot be resolved.
385+
description: |-
386+
Resolution specifies whether resolution of this reference is required.
387+
The default is 'Required', which means the reconcile will fail if the
388+
reference cannot be resolved. 'Optional' means this reference will be
389+
a no-op if it cannot be resolved.
359390
enum:
360391
- Required
361392
- Optional
362393
type: string
363394
resolve:
364-
description: Resolve specifies when this reference should
365-
be resolved. The default is 'IfNotPresent', which will
366-
attempt to resolve the reference only when the corresponding
367-
field is not present. Use 'Always' to resolve the reference
368-
on every reconcile.
395+
description: |-
396+
Resolve specifies when this reference should be resolved. The default
397+
is 'IfNotPresent', which will attempt to resolve the reference only when
398+
the corresponding field is not present. Use 'Always' to resolve the
399+
reference on every reconcile.
369400
enum:
370401
- Always
371402
- IfNotPresent
@@ -379,8 +410,9 @@ spec:
379410
grant is for.
380411
properties:
381412
matchControllerRef:
382-
description: MatchControllerRef ensures an object with the
383-
same controller reference as the selecting object is selected.
413+
description: |-
414+
MatchControllerRef ensures an object with the same controller reference
415+
as the selecting object is selected.
384416
type: boolean
385417
matchLabels:
386418
additionalProperties:
@@ -393,21 +425,21 @@ spec:
393425
properties:
394426
resolution:
395427
default: Required
396-
description: Resolution specifies whether resolution of
397-
this reference is required. The default is 'Required',
398-
which means the reconcile will fail if the reference
399-
cannot be resolved. 'Optional' means this reference
400-
will be a no-op if it cannot be resolved.
428+
description: |-
429+
Resolution specifies whether resolution of this reference is required.
430+
The default is 'Required', which means the reconcile will fail if the
431+
reference cannot be resolved. 'Optional' means this reference will be
432+
a no-op if it cannot be resolved.
401433
enum:
402434
- Required
403435
- Optional
404436
type: string
405437
resolve:
406-
description: Resolve specifies when this reference should
407-
be resolved. The default is 'IfNotPresent', which will
408-
attempt to resolve the reference only when the corresponding
409-
field is not present. Use 'Always' to resolve the reference
410-
on every reconcile.
438+
description: |-
439+
Resolve specifies when this reference should be resolved. The default
440+
is 'IfNotPresent', which will attempt to resolve the reference only when
441+
the corresponding field is not present. Use 'Always' to resolve the
442+
reference on every reconcile.
411443
enum:
412444
- Always
413445
- IfNotPresent
@@ -423,6 +455,8 @@ spec:
423455
- ADMIN
424456
- GRANT
425457
type: string
458+
required:
459+
- objectType
426460
type: object
427461
managementPolicies:
428462
default:

pkg/controller/postgresql/grant/reconciler.go

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,41 @@ const (
138138
roleDatabase grantType = "ROLE_DATABASE"
139139
)
140140

141+
// sliceContainsStr checks if a slice contains a specific string.
142+
func sliceContainsStr(haystack []string, needle string) bool {
143+
for _, s := range haystack {
144+
if s == needle {
145+
return true
146+
}
147+
}
148+
return false
149+
}
150+
151+
func validateGrantParams(gp v1alpha1.GrantParameters) error {
152+
if gp.Schema == nil && !sliceContainsStr([]string{"database", "foreign_data_wrapper", "foreign_server"}, gp.ObjectType) {
153+
return fmt.Errorf("parameter 'schema' is mandatory for grant resource")
154+
}
155+
if len(gp.Objects) > 0 && (gp.ObjectType == "database" || gp.ObjectType == "schema") {
156+
return fmt.Errorf("cannot specify `objects` when `object_type` is `database` or `schema`")
157+
}
158+
if len(gp.Columns) > 0 && gp.ObjectType != "column" {
159+
return fmt.Errorf("cannot specify `columns` when `object_type` is not `column`")
160+
}
161+
if len(gp.Columns) == 0 && gp.ObjectType == "column" {
162+
return fmt.Errorf("must specify `columns` when `object_type` is `column`")
163+
}
164+
if len(gp.Privileges) != 1 && gp.ObjectType == "column" {
165+
return fmt.Errorf("must specify exactly 1 `privileges` when `object_type` is `column`")
166+
}
167+
if len(gp.Objects) != 1 && gp.ObjectType == "column" {
168+
return fmt.Errorf("must specify exactly 1 table in the `objects` field when `object_type` is `column`")
169+
}
170+
if len(gp.Objects) != 1 && (gp.ObjectType == "foreign_data_wrapper" || gp.ObjectType == "foreign_server") {
171+
return fmt.Errorf("one element must be specified in `objects` when `object_type` is `foreign_data_wrapper` or `foreign_server`")
172+
}
173+
return nil
174+
}
175+
141176
func identifyGrantType(gp v1alpha1.GrantParameters) (grantType, error) {
142177
pc := len(gp.Privileges)
143178

@@ -195,34 +230,32 @@ func selectGrantQuery(gp v1alpha1.GrantParameters, q *xsql.Query) error {
195230

196231
ep := gp.Privileges.ExpandPrivileges()
197232
sp := ep.ToStringSlice()
233+
198234
// Join grantee. Filter by database name and grantee name.
199235
// Finally, perform a permission comparison against expected
200236
// permissions.
201237
q.String = "SELECT EXISTS(SELECT 1 " +
202-
"FROM pg_database db, " +
203-
"aclexplode(datacl) as acl, " +
204-
"pg_namespace as schema " +
205-
"INNER JOIN pg_roles s ON acl.grantee = s.oid " +
206-
// Filter by database, role and grantable setting
207-
"WHERE db.datname=$1 " +
208-
"AND s.rolname=$2 " +
209-
"AND acl.is_grantable=$3 " +
210-
// TODO: Filter by schema this is not working
211-
"AND schema.nspname=$4 " +
212-
"GROUP BY db.datname, s.rolname, acl.is_grantable, schema.nspname " +
213-
// Check privileges match. Convoluted right-hand-side is necessary to
214-
// ensure identical sort order of the input permissions.
215-
"HAVING array_agg(acl.privilege_type ORDER BY privilege_type ASC) " +
216-
"= (SELECT array(SELECT unnest($5::text[]) as perms ORDER BY perms ASC)))"
238+
"FROM pg_database db " +
239+
"JOIN pg_namespace nsp ON db.datname = $1 " +
240+
"JOIN LATERAL aclexplode(nsp.nspacl) acl ON true " +
241+
"JOIN pg_roles s ON acl.grantee = s.oid " +
242+
// Filter by role, schema and grantable setting
243+
"WHERE nsp.nspname = $2 " +
244+
"AND s.rolname = $3 " +
245+
"AND acl.is_grantable = $6 " +
246+
"GROUP BY db.datname, nsp.nspname, s.rolname, acl.is_grantable " +
247+
"HAVING array_agg(acl.privilege_type ORDER BY privilege_type ASC) = " +
248+
"(SELECT array(SELECT unnest($7::text[]) ORDER BY 1)))"
217249

218250
q.Parameters = []interface{}{
219251
gp.Database,
252+
gp.Schema,
220253
gp.Role,
254+
gp.ObjectType,
255+
gp.Objects,
221256
gro,
222-
gp.Schema,
223257
pq.Array(sp),
224258
}
225-
return nil
226259
}
227260
return errors.New(errUnknownGrant)
228261
}
@@ -372,6 +405,11 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext
372405
return managed.ExternalCreation{}, errors.New(errNotGrant)
373406
}
374407

408+
// Validate grant specs
409+
if err := validateGrantParams(cr.Spec.ForProvider); err != nil {
410+
return managed.ExternalCreation{}, errors.Wrap(err, errInvalidParams)
411+
}
412+
375413
var queries []xsql.Query
376414

377415
cr.SetConditions(xpv1.Creating())

0 commit comments

Comments
 (0)