Documentation
¶
Overview ¶
Package quarktenant is the thin CLI wrapper that turns a quark.Client + a set of registered Go models into a tenant row-level-security install workflow. The pattern mirrors [quarkmigrate]: a small library the user embeds in a tiny `main.go`, no shipped binary that knows about user models.
Usage:
// myapp/tenant/main.go
package main
import (
"context"
"os"
"github.com/jcsvwinston/quark"
"github.com/jcsvwinston/quark/quarktenant"
"myapp/models"
)
func main() {
client, err := quark.New(os.Getenv("QUARK_DIALECT"), os.Getenv("QUARK_DSN"))
if err != nil { os.Exit(2) }
defer client.Close()
_ = client.RegisterModel(&models.Order{}, &models.Invoice{})
os.Exit(quarktenant.Run(context.Background(), os.Args[1:], client))
}
Then in CI / Makefile:
go run ./tenant install-rls-policies --dry-run # print DDL go run ./tenant install-rls-policies # actually install
Only PostgreSQL is supported. Other dialects return quark.ErrUnsupportedFeature from InstallRLSPolicies.
Index ¶
- Constants
- Variables
- func InstallRLSPolicies(ctx context.Context, client *quark.Client, opts InstallOptions) ([]string, error)
- func Run(ctx context.Context, args []string, client *quark.Client) int
- func RunWithIO(ctx context.Context, args []string, client *quark.Client, ...) int
- type Action
- type InstallOptions
Constants ¶
const ( ExitSuccess = 0 ExitError = 2 )
Exit codes returned by Run:
0 — success: DDL printed (dry-run) or applied successfully.
2 — operational error (unsupported dialect, generation failure,
apply failure, unknown action).
Variables ¶
var ErrInvalidCast = errors.New("quarktenant: invalid SQL cast")
ErrInvalidCast is returned by InstallRLSPolicies when the TenantColumnSQLCast value (or the `--cast` CLI flag) does not match a single PostgreSQL type token. See [castIdentifier] for the allowed shape. This guard exists to prevent the policy SQL from being extended with arbitrary statements via the cast input.
var ErrNoRegisteredModels = errors.New("quarktenant: no models registered on the Client")
ErrNoRegisteredModels is returned when InstallRLSPolicies is invoked on a Client that has no models registered. Register models with quark.Client.RegisterModel before calling.
var ErrNoTenantColumn = errors.New("quarktenant: model is missing the configured TenantColumn")
ErrNoTenantColumn is returned by InstallRLSPolicies when a registered model does not declare the configured TenantColumn. Wrap your model with the column or skip it from registration.
Functions ¶
func InstallRLSPolicies ¶
func InstallRLSPolicies(ctx context.Context, client *quark.Client, opts InstallOptions) ([]string, error)
InstallRLSPolicies generates the policy DDL (and, when opts.DryRun is false, applies it) for every model registered on the Client. Returns the rendered DDL statements in order so the caller can print, log, or pipe them — regardless of DryRun.
PostgreSQL-only. Returns quark.ErrUnsupportedFeature wrapped with the dialect name on any other engine.
Apply path: a single PostgreSQL transaction wraps every statement across every registered model. PG supports transactional DDL — `ALTER TABLE ... ENABLE ROW LEVEL SECURITY`, `CREATE POLICY`, the `FORCE` variant — so a failure mid-stream rolls back the whole install. On success the migration lock is released and all policies are visible together.
The apply path drops down to [Client.Raw] to manage the transaction directly; it does not pass through SQLGuard because the statements are generated from registered model metadata, not from caller input. The cast token in the policy USING/WITH CHECK expression is validated separately via [castIdentifier] (see ErrInvalidCast).
Idempotence: the policy name is deterministic (<table>_tenant_isolation). Re-running InstallRLSPolicies on a table that already has the policy installed will fail at apply time with a PostgreSQL duplicate-object error (SQLSTATE 42710). Callers who want idempotent install should DROP the policy first or guard the call with their own existence check.
func Run ¶
Run is the CLI entry point. Pass it the raw args slice (typically `os.Args[1:]`) and a quark.Client with models registered. Returns an exit code suitable for `os.Exit(...)`.
Flags consumed (in addition to the action positional):
--dry-run print DDL, do not apply --tenant-col <name> column name (default: tenant_id) --native-rls-var <name> PG setting name (default: app.tenant_id) --cast <sql> cast appended after current_setting (default: text) --no-force-rls omit ALTER TABLE ... FORCE ROW LEVEL SECURITY --lock-name <name> migration lock name (default: quark_install_rls_policies) --lock-timeout <duration> migration lock acquire timeout (default: 30s)
Output goes to stdout for DDL and stderr for errors / status; the destinations are overrideable with the RunWithIO variant for tests.
Types ¶
type Action ¶
type Action string
Action is the subcommand quarktenant is asked to run. The F5-3 scope ships a single action; the type is kept open-ended so the follow-up F5-N items (drop, list, audit) can extend it without breaking the CLI shape.
const ( // ActionInstallRLSPolicies generates the RLS policy DDL for // every model registered on the [quark.Client]. With --dry-run, // the DDL is printed to stdout and no database changes are made. // Without --dry-run, the DDL is applied under a distributed // migration lock so concurrent installers are serialised. ActionInstallRLSPolicies Action = "install-rls-policies" )
type InstallOptions ¶
type InstallOptions struct {
// TenantColumn is the column name used in policies and inserted
// into the WITH CHECK clause. Default: "tenant_id". Must exist
// on every registered model (validated at generation time).
TenantColumn string
// NativeRLSVar is the PostgreSQL session variable referenced by
// the policy via current_setting(NativeRLSVar, true). Must match
// the value the [quark.TenantRouter] uses at runtime
// (TenantConfig.NativeRLSVar). Default: "app.tenant_id".
NativeRLSVar string
// ForceRLS toggles `ALTER TABLE <t> FORCE ROW LEVEL SECURITY`
// alongside `ENABLE ROW LEVEL SECURITY`. Without FORCE the table
// owner can bypass the policy — usually the application role IS
// the owner, so leaving FORCE off makes the policy decorative.
// Default: true.
ForceRLS bool
// DryRun, when true, generates the DDL but does not apply it.
// InstallRLSPolicies still returns the rendered statements so the
// caller can print them or pipe them.
DryRun bool
// LockTimeout caps how long [InstallRLSPolicies] will wait for
// the distributed migration lock when DryRun is false. Default:
// 30 seconds. The lock prevents two installers from racing on
// the same database.
LockTimeout time.Duration
// LockName is the [quark.Client.AcquireMigrationLock] name used
// for the install run. Default: "quark_install_rls_policies".
// Override only if you need to coordinate with custom tooling
// that already takes the default migration lock.
LockName string
// TenantColumnSQLCast is the explicit PostgreSQL cast appended
// to `current_setting(NativeRLSVar, true)` inside the policy
// USING/WITH CHECK clauses, e.g. "::text", "::uuid",
// "::bigint". When empty, InstallRLSPolicies infers `::text`
// for string columns and falls back to `::text` otherwise —
// callers using uuid or bigint tenant IDs MUST set this
// explicitly. The cast is emitted verbatim, surrounded by the
// leading "::" if the caller did not include one, so both
// "uuid" and "::uuid" work.
TenantColumnSQLCast string
}
InstallOptions configures policy generation for InstallRLSPolicies.
The zero value is unusable — call DefaultInstallOptions to obtain a populated struct and override fields as needed.
func DefaultInstallOptions ¶
func DefaultInstallOptions() InstallOptions
DefaultInstallOptions returns the populated defaults the F5-2 runtime expects. Callers typically tweak DryRun and (rarely) TenantColumn / NativeRLSVar before passing the struct to InstallRLSPolicies.