cloudnative-pg / cloudnative-pg/cloudnative-pg
[Feature]: Support for database-level `GRANT`/`REVOKE` privileges in the `Database` CRD
- Dominant language
- Go
- Stars
- 9.3k
- Forks
- 759
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 44
Description
### Is there an existing issue already for this feature request/idea?
- [x] I have searched for an existing issue, and could not find anything. I believe this is a new feature request to be evaluated.
### What problem is this feature going to solve? Why should it be added?
The `Database` CRD currently has no declarative way to manage privileges on the database object itself. Users who need to grant or revoke `CONNECT`, `CREATE` or `TEMPORARY` fall back to post-initialisation SQL scripts, which are imperative, one-off and fragile.
The most common real-world operation here is hardening: PostgreSQL grants `CONNECT` and `TEMPORARY` to `PUBLIC` by default, so almost every production setup needs to run `REVOKE CONNECT ON DATABASE x FROM PUBLIC` and then grant `CONNECT` back to specific roles. Today there is no first-class way to express that.
This is a natural companion to #7872 (schema-level `CREATE`/`USAGE` grants). Both address the same gap — declarative privilege management on objects already owned by the `Database` CRD — and should share design and reconciliation machinery.
### Describe the solution you'd like
PostgreSQL's database-level `GRANT` syntax:
```sql
GRANT { { CREATE | CONNECT | TEMPORARY | TEMP } [, ...] | ALL [ PRIVILEGES ] }
ON DATABASE database_name [, ...]
TO role_specification [, ...] [ WITH GRANT OPTION ]
[ GRANTED BY role_specification ]
```
As with #7872, `WITH GRANT OPTION` and `GRANTED BY` are unsuitable for a declarative environment and are out of scope. We focus on `CREATE`, `CONNECT` and `TEMPORARY` (`TEMP` is an alias).
A `permissions` stanza on the `Database` spec, keyed by privilege, with each entry carrying a role `name` and a `type` of `grant` (default) or `revoke`:
```yaml
spec:
permissions:
connect:
- name: public
type: revoke
- name: app
type: grant
create:
- name: app
type: grant
temporary:
- name: app
type: revoke
```
This shape deliberately mirrors the existing `UsageSpec` precedent (`{name, type}` with `grant`/`revoke`) already used for `FOREIGN DATA WRAPPER` and `FOREIGN SERVER` usage in `internal/management/controller/database_controller_sql.go`, and the proposal in #7872. Reusing the same `GrantSpec`/`UsageSpec`-style type keeps the API surface consistent.
Design points that need to be settled:
- **`PUBLIC` is a reserved name, not an identifier.** A `name` of `public` must be emitted as the bare `PUBLIC` keyword and must not be passed through `pgx.Identifier{}.Sanitize()`, otherwise it becomes the quoted role `"public"`, which does not exist. Since `REVOKE ... FROM PUBLIC` is the headline use case, this needs explicit handling and a documented reserved name.
- **`ALL` precedence.** I propose treating `all` as pure sugar that expands to `CREATE`, `CONNECT` and `TEMPORARY` before reconciliation, so the reconciler never reasons about `ALL` directly. Mixing `all` with named privileges for the same role should either be forbidden via a CEL validation, or resolved by a documented precedence rule. Expansion is cleaner and keeps drift detection uniform.
- **Idempotent reconciliation.** Rather than the fire-and-forget approach of the current `applyUsagePermissions` helper, read the current ACL from `pg_database.datacl` (via `aclexplode`) and apply only the deltas. This avoids log noise, makes the resource status meaningful and should be implemented as a shared privilege-diffing helper used by both this feature and #7872.
- **Self-lockout guard rail.** The operator's connection role and the managed application role must not be able to revoke their own `CONNECT`, or the reconciler locks itself out of the database. Either exclude the active connection role from `REVOKE` targets, or clearly document that the user owns that footgun.
### Describe alternatives you've considered
- Do nothing and delegate to post-init scripts or an external tool — this is the current state and is exactly the fragility we want to remove.
- A desired-state map (`role -> [privileges]`, presence = grant, absence = revoke). Cleaner to read, but `REVOKE`-by-omission is a sharp edge on shared databases and is inconsistent with the explicit `grant`/`revoke` intent model already merged for FDW/Server usage. I favour the explicit model for consistency and predictability.
- A separate `DatabasePrivilege` CRD. Over-engineered for object-level grants that already belong conceptually to the `Database` resource; it would also fragment ownership and reconciliation.
### Additional context
Companion to #7872. Both features should share a single read-current-state-and-diff helper for privileges, generalising the existing `applyUsagePermissions` in `internal/management/controller/database_controller_sql.go` (currently hardcoded to the `USAGE` keyword and fire-and-forget).
### Backport?
No
### Are you willing to actively contribute to this feature?
Yes
### Code of Conduct
- [x] I agree to follow this project's Code of Conduct
Contributor guide
Assessment
This issue has not been assessed yet.