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
142 changes: 142 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,148 @@ A policy will also be added to AWS IAM for the specific user allowing it to conn
}
```

## Custom Roles

The CRD `CustomRole` provisions a PostgreSQL role (with `NOLOGIN`) and keeps its server-level role memberships and per-database table privileges in sync across every host the controller manages.

The resource name becomes the PostgreSQL role name.

```yaml
apiVersion: postgresql.lunar.tech/v1alpha1
kind: CustomRole
metadata:
name: reporting
spec:
grantRoles:
- pg_read_all_data
grants:
- schema: public
privileges: [SELECT]
```

The controller reconciles the role on every reconcile loop:

1. Creates the role if it does not exist (idempotent).
2. Grants or revokes server-level roles (`grantRoles`) so the current membership exactly matches the spec.
3. For every user database on the host, grants or revokes table privileges (`grants`) so they exactly match the spec. Schema `USAGE` is managed automatically.

Grants that reference a schema or table absent from a particular database are silently skipped for that database, so a single `CustomRole` can safely target objects that only exist in some databases.

The controller also watches `PostgreSQLDatabase` resources and re-reconciles all `CustomRole` objects in the same namespace whenever a database transitions to the `Running` phase. This ensures grants are applied to a freshly provisioned database as soon as it is ready.

### `grantRoles`

`grantRoles` is a list of existing PostgreSQL roles to grant to this role at the server level. Common examples are built-in PostgreSQL roles such as `pg_monitor` or `pg_read_all_data`, or another `CustomRole` name to build a role hierarchy.

```yaml
spec:
grantRoles:
- pg_monitor
- pg_read_all_data
```

### `grants`

`grants` is a list of table privilege entries applied to every database on the host. Each entry has three fields:

| Field | Description |
|-------|-------------|
| `schema` | Schema to target. Use `"*"` or omit to target all user-defined schemas. |
| `table` | Table to target within the schema. Use `"*"` or omit to target all tables. |
| `privileges` | Non-empty list of PostgreSQL table-level privilege keywords. |

Valid privilege keywords: `SELECT`, `INSERT`, `UPDATE`, `DELETE`, `TRUNCATE`, `REFERENCES`, `TRIGGER`.

### Examples

#### Read-only role across all schemas and tables

Grants `SELECT` on every table in every user-defined schema in every database on the host. Useful for read-only reporting or analytics access.

```yaml
apiVersion: postgresql.lunar.tech/v1alpha1
kind: CustomRole
metadata:
name: readonly
spec:
grants:
- privileges: [SELECT]
```

#### Read-only role using pg_read_all_data (PostgreSQL 14+)

Uses the built-in `pg_read_all_data` server role, which grants `SELECT` on all tables, views, and sequences. No per-database grants are required.

```yaml
apiVersion: postgresql.lunar.tech/v1alpha1
kind: CustomRole
metadata:
name: readonly
spec:
grantRoles:
- pg_read_all_data
```

#### Write role on a specific schema

Grants full DML access (`SELECT`, `INSERT`, `UPDATE`, `DELETE`) on all tables in the `orders` schema only. Useful for a service that owns a single schema.

```yaml
apiVersion: postgresql.lunar.tech/v1alpha1
kind: CustomRole
metadata:
name: orders-writer
spec:
grants:
- schema: orders
privileges: [SELECT, INSERT, UPDATE, DELETE]
```

#### Targeted grant on a single table

Grants `SELECT` on the `audit_log` table in the `public` schema only. Tables absent from a given database are automatically skipped.

```yaml
apiVersion: postgresql.lunar.tech/v1alpha1
kind: CustomRole
metadata:
name: audit-reader
spec:
grants:
- schema: public
table: audit_log
privileges: [SELECT]
```

#### Role combining server-level membership and table grants

Grants `pg_monitor` for server monitoring and also grants `SELECT` on all tables in the `metrics` schema for application-level metrics queries.

```yaml
apiVersion: postgresql.lunar.tech/v1alpha1
kind: CustomRole
metadata:
name: monitoring
spec:
grantRoles:
- pg_monitor
grants:
- schema: metrics
privileges: [SELECT]
```

### Deletion

When a `CustomRole` resource is deleted the controller revokes all table privileges and schema `USAGE` grants it holds in every database, then drops the PostgreSQL role. The resource uses a Kubernetes finalizer to ensure this cleanup completes before the object is removed.

### Status

| Phase | Meaning |
|-------|---------|
| `Running` | Role and all grants are in sync. |
| `Failed` | A transient error occurred; the controller will retry. |
| `Invalid` | The spec is invalid (e.g. unknown privilege keyword); the resource will not be retried until the spec changes. |

# Development

This project uses the [Operator SDK framework](https://github.com/operator-framework/operator-sdk) and its associated CLI.
Expand Down
109 changes: 109 additions & 0 deletions api/v1alpha1/customrole_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
Copyright 2024.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v1alpha1

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// CustomRoleSpec defines the desired state of CustomRole
// +k8s:openapi-gen=true
type CustomRoleSpec struct {
// GrantRoles is a list of existing PostgreSQL roles to grant to this role
// (e.g. pg_monitor, pg_read_all_data, or another CustomRole's name).
// These are applied at the server level.
// +optional
GrantRoles []string `json:"grantRoles,omitempty"`

// Grants is a list of schema/table privilege grants applied to every database
// on the host. Reconciled whenever a new PostgreSQLDatabase is created.
// +optional
Grants []CustomRoleGrant `json:"grants,omitempty"`
}

// CustomRoleGrant defines schema/table privileges to grant to the role.
// +k8s:openapi-gen=true
type CustomRoleGrant struct {
// Schema is the schema to grant privileges on.
// Use "*" or omit to target all user-defined schemas.
// +optional
Schema string `json:"schema,omitempty"`

// Table is the table to grant privileges on within Schema.
// Use "*" or omit to target all tables in the schema.
// +optional
Table string `json:"table,omitempty"`

// Privileges is a list of PostgreSQL privilege keywords (SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER)
Privileges []string `json:"privileges"`
}

// CustomRolePhase represents the current phase of a CustomRole resource
// +k8s:openapi-gen=true
type CustomRolePhase string

const (
// CustomRolePhaseFailed indicates that the controller was unable to reconcile the CustomRole resource
CustomRolePhaseFailed CustomRolePhase = "Failed"
// CustomRolePhaseInvalid indicates that the resource specification is invalid and will not be reconciled until changed
CustomRolePhaseInvalid CustomRolePhase = "Invalid"
// CustomRolePhaseRunning indicates that the controller has successfully reconciled the CustomRole resource
CustomRolePhaseRunning CustomRolePhase = "Running"
)

// CustomRoleStatus defines the observed state of CustomRole
type CustomRoleStatus struct {
// Phase is the current phase of the CustomRole resource
// +optional
Phase CustomRolePhase `json:"phase,omitempty"`

// PhaseUpdated is the time when the phase last changed
// +optional
PhaseUpdated metav1.Time `json:"phaseUpdated,omitempty"`

// Error contains the error message when Phase is Failed or Invalid
// +optional
Error string `json:"error,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Role",type="string",JSONPath=".metadata.name"
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.phase"
// +kubebuilder:printcolumn:name="Error",type="string",JSONPath=".status.error"

// CustomRole is the Schema for the customroles API
type CustomRole struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec CustomRoleSpec `json:"spec"`
Status CustomRoleStatus `json:"status,omitempty"`
}

// +kubebuilder:object:root=true

// CustomRoleList contains a list of CustomRole
type CustomRoleList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata"`
Items []CustomRole `json:"items"`
}

func init() {
SchemeBuilder.Register(&CustomRole{}, &CustomRoleList{})
}
122 changes: 122 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

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

Loading
Loading