Documentation
¶
Overview ¶
Package auth provides authentication and authorization for Wadjet's HTTP API.
Supports three authentication methods:
- API keys: Simple bearer tokens for internal tools (Grafana, ETL pipelines)
- JWT: HMAC-SHA256 or RSA-signed tokens for service-to-service auth
- mTLS: Client certificate authentication for zero-trust deployments
Authorization is role-based with table-level granularity.
Index ¶
- Constants
- Variables
- func AttachProvider(ctx context.Context, p *Provider, cat *catalog.Catalog, logger *slog.Logger)
- func AuthorizeTableFunction(ctx context.Context, provider *Provider, protocol string, funcName string, ...) error
- func AuthorizeUDFRead(ctx context.Context, provider *Provider) error
- func BindPoliciesToCatalog(ctx context.Context, cat *catalog.Catalog, abacIn []AccessControlPolicy, ...) ([]AccessControlPolicy, *PolicySet, error)
- func Build(cfg Config) (*Authenticator, *Authorizer, error)
- func ContextWithEnvironment(ctx context.Context, env Environment) context.Context
- func ContextWithIdentity(ctx context.Context, id *Identity) context.Context
- func ContextWithRowFilters(ctx context.Context, filters RowFilters) context.Context
- func ContextWithTableDecisions(ctx context.Context, decisions TableDecisions) context.Context
- func EnforceDMLPolicies(ctx context.Context, provider *Provider, cat *catalog.Catalog, ...) error
- func EnforceOptimizedPlan(ctx context.Context, cat *catalog.Catalog, plan *logical.Node) (*logical.Node, error)
- func EnforcePlanPolicies(ctx context.Context, provider *Provider, cat *catalog.Catalog, ...) (context.Context, *logical.Node, error)
- func LoadClientCA(caFile string) (*x509.CertPool, error)
- func Middleware(authn *Authenticator, logger *slog.Logger) func(http.Handler) http.Handler
- func NarrowQueryLimits(a, b *config.QueryLimits) *config.QueryLimits
- func New(cfg Config) (*Authenticator, *Authorizer)
- func NewTLSConfig(serverCertFile, serverKeyFile string, clientCA *x509.CertPool) (*tls.Config, error)
- func ProviderMiddleware(provider *Provider, logger *slog.Logger) func(http.Handler) http.Handler
- func QueryLimitObligation(ob Obligation) (target string, n int64, err error)
- func RateLimitMiddleware(rl *RateLimiter, logger *slog.Logger) func(http.Handler) http.Handler
- func RequirePermission(provider *Provider, ctx context.Context, perm string) error
- func StampDefiner(ctx context.Context, provider *Provider, snap IdentitySnapshot) (context.Context, bool)
- func StatementBaseTables(ctx context.Context, cat *catalog.Catalog, info *plansql.SelectInfo) []string
- func TableAccess(ctx context.Context, provider *Provider, table string, action Action) error
- func ValidateABACPolicies(policies []AccessControlPolicy) error
- func ValidateStatementColumns(ctx context.Context, provider *Provider, cat *catalog.Catalog, ...) error
- func VisibleTables(ctx context.Context, provider *Provider, tables []string) []string
- type APIKeyDef
- type AccessControlPolicy
- type AccessPolicy
- type Action
- type Attributes
- type AuditLogger
- func (a *AuditLogger) LogAccessDenied(identity *Identity, resource, reason string)
- func (a *AuditLogger) LogAuthFailure(remoteAddr, path, reason string)
- func (a *AuditLogger) LogColumnPolicy(identity *Identity, table string, masked, denied []string)
- func (a *AuditLogger) LogQuery(identity *Identity, sql string, tables []string, elapsed time.Duration, ...)
- func (a *AuditLogger) LogRowFilterApplied(identity *Identity, table, filter string)
- type Authenticator
- type Authorizer
- type CellMaskFunc
- type ColumnDecision
- type ColumnPolicy
- type Condition
- type Config
- type Decision
- type Effect
- type Environment
- type Identity
- type IdentitySnapshot
- type JWTConfig
- type JWTVerifier
- type MTLSConfig
- type MTLSVerifier
- type Obligation
- type PolicyConfig
- type PolicyEvaluator
- type PolicyRule
- type PolicySet
- type Provider
- func (p *Provider) Audit() *AuditLogger
- func (p *Provider) Authenticator() *Authenticator
- func (p *Provider) Authorizer() *Authorizer
- func (p *Provider) BindError() error
- func (p *Provider) BindToCatalog(ctx context.Context, cat *catalog.Catalog) error
- func (p *Provider) Enabled() bool
- func (p *Provider) Evaluator() *PolicyEvaluator
- func (p *Provider) Policies() *PolicySet
- func (p *Provider) Update(authn *Authenticator, authz *Authorizer, policies *PolicySet)
- func (p *Provider) UpdateFromConfig(cfg Config, policyCfgs []PolicyConfig, abacPolicies ...AccessControlPolicy) error
- func (p *Provider) UpdateWithEvaluator(authn *Authenticator, authz *Authorizer, policies *PolicySet, ...)
- type RateLimitConfig
- type RateLimiter
- type Resource
- type RoleConfig
- type RoleDef
- type RowFilters
- type SerializedDecision
- type Subject
- type TableDecision
- type TableDecisions
- type UDFMutation
Constants ¶
const ( QueryLimitRows = "max_scan_rows" QueryLimitBytes = "max_scan_bytes" QueryLimitFiles = "max_scan_files" )
A `query_limit` obligation narrows the query cost guard for one identity on one relation.
`target` names WHICH of the guard's ceilings it sets and `value` is the number. An empty target means `max_scan_rows`, which is the only reading docs/security.md's obligation table ever gave it ("Value: row count").
obligations:
- type: query_limit
value: "1000000" # max_scan_rows
- type: query_limit
target: max_scan_bytes
value: "1073741824"
The three ceilings are the three the guard already has. `require_filter_ above_bytes` and `require_limit_above_rows` are deliberately NOT settable from a policy: they are shaped like advice to the query author, not like a ceiling on an identity, and an obligation that made a statement fail for missing a WHERE clause would be a worse message than one that names a size.
const ( ResourceTable = "table" ResourceTableFunction = "table_function" )
The ABAC resource TYPEs. `ResourceTable` is what `EvaluateTableAccess` stamps for a catalog relation; `ResourceTableFunction` is what a table-function scan presents itself as, and it is not a relation — the policy binder must not resolve its `resource.name` against the catalog, and a rule written for tables must not reach it by breadth.
Variables ¶
var ( ErrNoCredentials = errors.New("no credentials provided") )
Errors returned by authentication.
var ColumnActions = []string{"allow", "mask", "deny"}
ColumnActions lists the actions a policy's `columns:` map accepts, in the order an error message should offer them.
Functions ¶
func AttachProvider ¶ added in v0.18.47
AttachProvider binds p to cat from a constructor that has no error return.
It is BindToCatalog for the three doors whose constructors predate the bind (`server.New`, `pgwire.NewServer`, `NewGRPCServer`): the refusal is logged and, more importantly, REMEMBERED, so `Provider.BindError` refuses every statement rather than letting an unbound set enforce nothing. Attaching a provider to a catalog goes through this or through BindToCatalog and through nothing else — `TestEveryProviderFieldIsAttachedThroughTheBindingFunction` fails when a site appears that does neither.
func AuthorizeTableFunction ¶ added in v0.18.53
func AuthorizeTableFunction(ctx context.Context, provider *Provider, protocol string, funcName string, args []string, namedArgs map[string]string) error
AuthorizeTableFunction is the decision for ONE table-function scan: may this identity read this destination?
A table function is a CAPABILITY, not a relation. `read_csv('/etc/shadow')` reads a file off the server's own disk; `read_json('http://10.0.0.5/x')` makes the server issue an HTTP request from inside the network perimeter; `postgres_query(dsn, sql)` opens an outbound database connection to wherever the caller points it. None of these is in the catalog, so before #943 none of them became an ABAC Resource: `logical.PolicedScanTables` and `StatementBaseTables` both skip a function scan, and a role restricted to one catalog table could still read any file the process could read.
The rule, with auth ENABLED, is DEFAULT DENY:
- No provider / auth disabled: allowed, unchanged. The CLI, the embedded engine without SetAuthProvider and every existing no-auth test read files exactly as they did — that is the documented single-user use.
- A pure function (generate_series, unnest): allowed. It opens nothing.
- No identity under auth enabled: refused.
- An ABAC evaluator installed: the evaluator decides, over `Resource{Type: "table_function", Name: <func>, Attributes: {path, url, host}}` with ActionRead. Deny-overrides with a default deny, so a deployment that has never written a rule about table functions refuses them.
- Legacy roles only (no evaluator): the `admin` permission. There is no way to spell "may read server files" in the `read`/`write`/`admin` vocabulary, and reading arbitrary server-local files is an administrator's capability. No new permission words (ADR-0034).
PostgreSQL's precedent for the default: `pg_read_file` is superuser-only and answers `42501 permission denied for function pg_read_file` to an ordinary role, and `COPY … FROM PROGRAM` needs the `pg_execute_server_program` role (both verified on the oracle server). Server-side file and program access is a privilege there too.
func AuthorizeUDFRead ¶ added in v0.18.53
AuthorizeUDFRead decides a SHOW FUNCTIONS.
It requires an identity and no particular permission. That is PostgreSQL's rule for the same information: a role with no privileges on a function reads its body and its owner out of `pg_proc.prosrc`, and `\sf` prints the whole definition. A function body is not data; it is part of the schema the server publishes to the sessions that may call it.
Under auth ENABLED a caller with no identity is still refused, because a door that reaches here with nobody has no one to answer.
func BindPoliciesToCatalog ¶ added in v0.18.47
func BindPoliciesToCatalog(ctx context.Context, cat *catalog.Catalog, abacIn []AccessControlPolicy, legacyIn *PolicySet) ([]AccessControlPolicy, *PolicySet, error)
BindPoliciesToCatalog resolves every relation and column a policy set names against cat and returns the BOUND COPY — each name rewritten to the catalog's own spelling — or an error naming the first that does not resolve.
It binds a COPY and never touches its inputs. Two reasons, and both are contracts this package already makes elsewhere. The evaluator it would otherwise rewrite is being READ by every query in flight (`PolicyEvaluator.ruleMatches`), so rewriting in place is a data race on a live security decision. And a bind that fails partway would leave the RUNNING set half-rewritten, which is the opposite of the promise `UpdateFromConfig` makes: a policy set that cannot be installed installs nothing and the previous one keeps running (#802).
All arguments are optional: a deployment may run ABAC only, legacy `policies:` only, or both. A nil catalog binds nothing and returns the inputs unchanged — the caller has no catalog to resolve against, and the fold-aware comparison in relationEq / policyKey is the floor there.
func Build ¶ added in v0.18.53
func Build(cfg Config) (*Authenticator, *Authorizer, error)
Build creates an Authenticator and Authorizer from configuration, reporting a configuration it cannot honour.
A configuration error is an ERROR, never "authentication disabled" (#931):
- `jwt.enabled` with no secret and no readable public key, an unparseable key file, a typo in the path — `NewJWTVerifier`'s error used to be DISCARDED and the verifier left nil.
- `enabled: true` with no usable mechanism at all: no API keys, no JWT, no mTLS. An operator who wrote `enabled: true` did not ask for an open server.
- A credential naming a role the configuration does not define — an `api_keys` entry, an mTLS `role_map` value, an mTLS `default_role` — and `enabled: true` with credentials but no `roles:` at all. Such a credential authenticates and then holds NO permission, so it can do nothing; before this batch it could read everything, because the data path did not consult the role at all. Either way the configuration cannot mean what the operator wrote, and the doctrine is that such a configuration refuses at LOAD rather than behaving surprisingly at runtime.
On error the returned Authenticator is not a disabled one. It reports Enabled() and refuses EVERY credential with the configuration error, so a caller that ignores the error — and every door reads `Provider.Enabled()`, not this — is closed rather than open. Startup and hot reload both report it: `cmd/wadjet` refuses to start, and `Provider.UpdateFromConfig` refuses the swap and keeps the running state.
func ContextWithEnvironment ¶ added in v0.18.53
func ContextWithEnvironment(ctx context.Context, env Environment) context.Context
ContextWithEnvironment attaches the TRUSTED request environment — the one the PROTOCOL BOUNDARY observed, not one any caller below it can assert.
It is attached where the connection is: the HTTP middleware, the pgwire connection handshake and the gRPC authentication interceptor each know the peer address and which protocol they are, and nothing further down does. Anything the client can set (an `X-Forwarded-For` header, a startup parameter, a gRPC metadata value) is NOT trusted here — a source-address condition that a client could forge is not a control.
`Time` is deliberately NOT stamped at attach time. A pgwire connection lives for hours and a policy conditioned on `env.hour` has to mean the hour the STATEMENT ran, not the hour the socket opened; the decision stamps it (see DecisionEnvironment).
A `SourceIP` carrying a PORT is reduced to its host here, once, so no door has to remember: `net.Conn.RemoteAddr()` and `http.Request.RemoteAddr` are `host:port`, while `env.source_ip` is documented and written as an IP. The HTTP door passed `r.RemoteAddr` straight through, so a documented `env.source_ip eq "127.0.0.1"` rule compared against `127.0.0.1:54321` and never matched (#933).
func ContextWithIdentity ¶
ContextWithIdentity returns a new context with the given identity.
func ContextWithRowFilters ¶
func ContextWithRowFilters(ctx context.Context, filters RowFilters) context.Context
ContextWithRowFilters returns a context carrying row filter predicates.
func ContextWithTableDecisions ¶
func ContextWithTableDecisions(ctx context.Context, decisions TableDecisions) context.Context
ContextWithTableDecisions returns a context carrying ABAC table decisions.
func EnforceDMLPolicies ¶ added in v0.18.38
func EnforceDMLPolicies(ctx context.Context, provider *Provider, cat *catalog.Catalog, parsed *plansql.ParsedQuery, protocol string) error
EnforceDMLPolicies applies ABAC to an INSERT / UPDATE / DELETE / MERGE before any row is read or written. It is called from the ONE DML entry point (wadjet.DB.ExecuteParsed), so the embedded door, the pgwire door and the HTTP API server all carry it.
Two rules, and they are the SELECT door's rules said for a statement that writes (ADR-0033):
**A DML statement is an ActionWrite.** An identity whose policies grant it no write on the table is refused with 42501 before anything happens. A role allowed only `read` used to be able to run `DELETE FROM t WHERE ssn = '<stored value>'` and destroy the row.
**A read inside the statement sees what a SELECT would see.** A column the policy DENIES does not exist, so naming it in a predicate, a SET target or a SET expression is 42703 — before #859 `UPDATE t SET dept='z' WHERE salary = 700009` matched exactly the row with that salary, a working oracle for a column the identity may not read. A MASKED column reads as its mask, so `WHERE ssn = '<stored value>'` matches nothing and `SET dept = ssn` writes '***' instead of copying the stored value into a column the identity may read.
Rule 2 is a SUBSTITUTION rather than a projection because a DML predicate is compiled, not planned (ADR-0031): there is no Scan for a security projection to sit on. Where the substitution cannot be done soundly the statement is REFUSED, never run against the stored row.
No-ops when the provider is nil/disabled, no identity is attached, or the provider has no evaluator — the same contract EnforcePlanPolicies keeps.
func EnforceOptimizedPlan ¶ added in v0.18.38
func EnforceOptimizedPlan(ctx context.Context, cat *catalog.Catalog, plan *logical.Node) (*logical.Node, error)
EnforceOptimizedPlan re-applies the column policies the context carries to any policed scan the OPTIMIZER minted. Call it immediately after logical.Optimize, at every entry point that optimizes a plan.
The decorrelation passes re-parse a subquery from its SQL text and build a fresh Scan for it, after enforcement has run. Those scans read the stored column: `WHERE a.ssn IN (SELECT ssn FROM t b)` compared the outer's mask against the inner's stored value and answered 0 where both sides masked answer every row. It is a no-op — one tree walk — when the context carries no policy.
func EnforcePlanPolicies ¶
func EnforcePlanPolicies(ctx context.Context, provider *Provider, cat *catalog.Catalog, selectInfo *plansql.SelectInfo, plan *logical.Node, protocol string) (context.Context, *logical.Node, error)
EnforcePlanPolicies applies ABAC to a query at plan level: table-access denial, column deny/mask injection and row-filter injection for every table the plan READS. It is THE shared enforcement path — the embedded engine (wadjet.DB.Query), the HTTP door and the coordinator's native-DAG executor all call it with the same inputs, so an identity sees identical policy behavior regardless of which door and which execution path answers.
Masking and denial are PLAN-TIME, at the scan, unconditionally (#859):
- The security projection is built from the TABLE's catalog schema, which is always known here, and never from the scan's pruned column list. `SELECT *`, an aggregate-only SELECT list and a derived table all leave that list empty, and those are exactly the queries a mask matters most for.
- The relations to police come from the PLAN, not from the statement's FROM list. `plansql.SelectInfo.Tables` carries a derived table under its own subquery TEXT, a CTE reference under the CTE's name, and NOTHING at all for the arms of a UNION — so a `UNION ALL` over a masked column was unmasked on every door, and a query with a derived table or a CTE was default-DENIED under the name `"(SELECT ...)"`.
- A column policy that cannot be applied REFUSES. A security control never degrades to a grant (#802).
The returned context carries the resolved policies so the physical planner applies the same projection to an expression subquery, which it plans on its own (physical.buildSubqueryPipeline).
No-ops (returns the plan unchanged) when the provider is nil/disabled, no identity is attached to ctx, or the provider has no evaluator — matching the embedded engine's historical behavior. protocol labels the evaluation environment for policy conditions and audit.
func LoadClientCA ¶
LoadClientCA loads a CA certificate pool for verifying client certificates. Used when building the tls.Config for the HTTP server.
func Middleware ¶
Middleware returns HTTP middleware that authenticates requests. Health check endpoint is always allowed without auth.
func NarrowQueryLimits ¶ added in v0.18.42
func NarrowQueryLimits(a, b *config.QueryLimits) *config.QueryLimits
NarrowQueryLimits folds b into a, keeping the SMALLER of every ceiling either sets. Two policed relations in one statement each impose their own ceiling and the statement is held to the tighter.
func New ¶
func New(cfg Config) (*Authenticator, *Authorizer)
New creates an Authenticator and Authorizer from configuration.
It is Build with the error folded into the Authenticator rather than returned: on a configuration error the Authenticator is ENABLED and refuses every credential (see Build). The signature is kept because it is what the embedded API and a long tail of tests call; a caller that can report the error should call Build.
func NewTLSConfig ¶
func NewTLSConfig(serverCertFile, serverKeyFile string, clientCA *x509.CertPool) (*tls.Config, error)
NewTLSConfig creates a tls.Config for the server with mTLS client verification. serverCert and serverKey are paths to the server's TLS certificate and key. clientCA is the CA pool for verifying client certificates.
func ProviderMiddleware ¶
ProviderMiddleware returns HTTP middleware that reads the current Authenticator from a Provider on every request. This enables hot-reload: when auth config changes, the Provider is atomically updated and subsequent requests see the new config with zero downtime.
func QueryLimitObligation ¶ added in v0.18.42
func QueryLimitObligation(ob Obligation) (target string, n int64, err error)
QueryLimitObligation reads one query_limit obligation, or says why it cannot be enforced as written. ValidateABACPolicies calls it at config load and at hot reload so a policy that cannot be enforced does not load; the evaluator calls it again on a provider built in process.
func RateLimitMiddleware ¶
RateLimitMiddleware returns HTTP middleware that enforces per-identity rate limits. Unauthenticated requests use the remote address as identity.
func RequirePermission ¶
RequirePermission enforces that the caller in ctx holds perm, but only when the provider is present and auth is enabled. It is the gate for privileged DDL (e.g. CREATE/DROP/ALTER ALERT, CREATE/DROP TABLE, ANALYZE, CREATE/DROP FUNCTION) that has no per-row ABAC surface of its own. Fail-closed contract: with auth enabled, a missing identity or an identity lacking perm is rejected; with auth absent/disabled it returns nil (dev/embedded, nothing to enforce).
The refusal carries PostgreSQL's 42501 (insufficient_privilege), because a client branches on the CLASS and this is the only thing it can branch on: an authorization refusal that crossed pgwire without a code arrived as the blanket 42000, indistinguishable from a syntax error, and through the HTTP door as a bare message. `sqlerr.Wrap` keeps the chain, so `errors.Is(err, ErrUnauthorized)` still holds for the in-process callers.
func StampDefiner ¶
func StampDefiner(ctx context.Context, provider *Provider, snap IdentitySnapshot) (context.Context, bool)
StampDefiner stamps snap's identity onto ctx for definer's-rights execution (e.g. a scheduled alert query running as its creator) and reports whether the definer is attributed — i.e. whether a real identity was recorded.
- provider nil / auth disabled: ctx is returned unchanged with true — there is no policy to enforce (dev/embedded).
- auth enabled: snap.ToIdentity() is ALWAYS stamped, even for an empty (legacy) snapshot. A nil identity is refused outright now (ADR-0034 item 7), and an unattributed alert should say WHY rather than fail as "authentication required": stamping a role-less identity routes it into the same default-deny with attributed=false, so the caller can warn that the alert needs recreating under an identity.
The stamped identity's GRANTS are re-resolved from the Authorizer's current role definitions (`Authorizer.ResolveRole`). A snapshot records who the definer was and not what they could do, so the grants cannot be stale: an alert whose creator's role has since lost `write`, or been deleted, is refused on its next tick. Without this the definer carried an empty `Perms` and `Tables`, which the coarse gate and `CanAccessTable` read — so on a `roles:`-only deployment every scheduled alert stopped running.
func StatementBaseTables ¶ added in v0.18.38
func StatementBaseTables(ctx context.Context, cat *catalog.Catalog, info *plansql.SelectInfo) []string
StatementBaseTables lists the relations a statement's FROM and JOIN clauses name that the catalog recognises as TABLES.
It is the filter every access check needs, because plansql.SelectInfo.Tables is not a list of tables: a derived table appears under its own subquery TEXT (`"(SELECT ssn FROM t)"`) and a CTE reference under the CTE's name. Handing those to a default-deny evaluator refused every query with a derived table or a CTE under a policy that named neither (#859). A table function is not a catalog relation and is skipped for the same reason.
With no catalog to ask, it returns the FROM-list names unchanged — the pre-#859 behavior for a caller that cannot tell a table from a subquery.
func TableAccess ¶ added in v0.18.53
TableAccess is the ONE effective table-access decision every door asks for a catalog table it is about to read metadata of or act on.
It exists because the doors did not agree. `SHOW TABLES` filtered on the HTTP door and on no other; `DESCRIBE` refused on one door and answered on the rest; a DDL statement asked `HasPermission` and never asked whether the identity may touch THAT relation. Each of those is a separate reading of the same question, and a security decision with several readings has the weakest one for its answer. The decision lives here now, and a door that re-implements it has forked it.
The rule, in order:
- Provider nil, or auth disabled: nil. There is nothing to enforce (dev, embedded without SetAuthProvider), and nothing changes.
- A policy set that could not be BOUND to the catalog: refused. An unbindable set enforces nothing, and a rule that matches nothing is a grant beside a broad allow (#882, ADR-0033 rule 3).
- Auth enabled with NO identity in the context: refused. Authentication proves who; a door that reaches here with nobody has no one to authorize.
- The role's `allow` list, in BOTH provider shapes: `HasPermission(id, perm(action))`. It is a COARSE GATE — a policy narrows what a role may do and never widens it — and `admin` grants everything, as it always has.
- Then, with an ABAC evaluator installed: the EVALUATOR decides (`EvaluateTableAccess`), which is deny-overrides with a default deny — explicit denies win, an unmatched request is refused.
- With no evaluator: `CanAccessTable(id, table)`, the other half of the legacy rule. The permission alone is not access to a relation, and the relation alone is not permission to write it.
`table` must be the CATALOG-RESOLVED spelling (`catalog.ResolveTableName`): an unquoted identifier folds at the lexer (#731), and a policy bound to `Users` must police a statement that spelled it `users`.
The environment comes from the context — attached at the protocol boundary, never from a caller below it — through the one builder every enforcement path uses (`DecisionEnvironment`), so `Time` is stamped at DECISION time. A pgwire connection lives for hours; an `env.hour` condition means the hour the statement ran.
The refusal is a `sqlerr` 42501, which every door renders in its own class: pgwire SQLSTATE 42501, HTTP 403, gRPC codes.PermissionDenied, and the same error verbatim on the embedded API. Its TEXT is PostgreSQL's and carries nothing else — not the rule that denied, not the reason. Which rule fired is operator information and goes to the audit log; telling the refused caller is a disclosure, and a policy rule id names the control that stopped them.
func ValidateABACPolicies ¶ added in v0.18.38
func ValidateABACPolicies(policies []AccessControlPolicy) error
ValidateABACPolicies refuses a policy set whose obligations cannot be enforced as written. It runs where ParsePolicies' own refusal runs — at config load and at hot reload — and for the same reason (#802): a column-access control that cannot be understood must refuse to load, because the alternative is an operator who believes a column is masked and is served it in the clear.
The two refusals:
- A `mask_column` obligation carrying NEITHER `value` nor `mask_func`. The enforcement path dropped such an obligation, so the column came back in the clear on every door. The type-derived placeholder ('***', 0, false) belongs to the LEGACY `policies: columns: {col: mask}` form, which MigrateRBACToABAC spells as `mask_func: redact`; an `abac_policies:` obligation says what it means with `value:`.
- A `mask_column` whose `value` is not a SQL EXPRESSION. `value: "***REDACTED***"` — the spelling docs/configuration.md shipped for twelve releases — does not parse, and the fallback turned every masked column, string numeric and timestamp alike, into `0`. A mask that silently redefines itself is the same class of defect as one that silently disappears.
A `deny_column` or `mask_column` with no target names no column and is refused for the same reason.
It also enumerates the SECURITY VOCABULARY — effect, action, condition operator, condition attribute namespace, obligation type — and refuses a word outside it (#932). Every one of those fields used to accept anything: `effect: dney` became an ALLOW that granted what the operator wrote a deny for, an obligation type `deny_colum` was dropped and the column came back in plaintext, and an operator or attribute the evaluator does not implement made its rule never match, which beside a broad allow is a grant. A closed vocabulary that nothing checks is not a vocabulary.
func ValidateStatementColumns ¶ added in v0.18.38
func ValidateStatementColumns(ctx context.Context, provider *Provider, cat *catalog.Catalog, info *plansql.SelectInfo, protocol string) error
ValidateStatementColumns is the plan-time name binding every query entry point runs before it builds a logical plan, done over the schema the CALLING IDENTITY can see.
It replaces a bare physical.Planner.ValidateColumns at those entry points. The unfiltered binder answers `SELECT nosuchcol FROM t` with a hint that lists the table's columns, and a column the policy DENIES has no business in that list: an identity that may not read `salary` may not learn that `salary` exists either. The policy is resolved LAZILY, per table, as the binder resolves relations — so it covers CTE bodies, derived tables, subquery blocks and set-operation arms without needing a plan.
TABLE-LEVEL DENIAL IS DECIDED HERE TOO, since #946, and it has to be.
The position this comment used to state — that table denial stays in EnforcePlanPolicies so the order of the two refusals does not change — was answerable only while the binder's diagnostic said nothing about a relation. It says a great deal: `SELECT nocol FROM secret` answered `unknown column "nocol" (available: id, note)` for an identity that may not read `secret`, on every door and under BOTH provider shapes, and the hint POOLS relations — `SELECT id FROM emp WHERE id = (SELECT MAX(nocol) FROM secret)` published emp's columns unioned with secret's. `docs/security.md` and ADR-0034 say an identity that may not read a table may not read its schema either, and an error's hint is schema.
So the decision is asked PER RELATION as the binder resolves it (policedColumnSource.GetTable), which is the only seam that sees CTE bodies, derived tables, subquery blocks and set-operation arms as well as the FROM list. It is the SHARED rule — `TableAccess`, ADR-0034 item 5 — so the answer and its sentence are the ones every other door gives, and it is installed in BOTH provider shapes: the legacy `roles:` shape denies relations too, and it was publishing their columns just as loudly.
What changes for a denied relation is the CLASS of an existing refusal: 42703 with a column list becomes 42501 with none. What does not change is a relation the identity MAY read (42703 with the list, exactly as before), a relation that does not exist (42P01 — the decision is asked only after the catalog finds the table), or a statement with no provider.
func VisibleTables ¶ added in v0.18.53
VisibleTables filters tables to the ones TableAccess allows this identity to READ, preserving order.
It is what a listing door (`SHOW TABLES`, the HTTP table list, the gRPC ListTables) hands back, so a name an identity may not read is not published by the listing either. That is a deliberate divergence from PostgreSQL, which shows `\d` to anyone: the product's position is that metadata follows the effective table-access decision (ADR-0034), and the HTTP door already behaved this way before the other doors did.
With the provider nil or auth disabled the input is returned as it is — same slice, same nil-ness — so a no-auth deployment allocates nothing and sees no change.
Types ¶
type APIKeyDef ¶
type APIKeyDef struct {
Key string `yaml:"key"` // the bearer token value
Name string `yaml:"name"` // human label
Role string `yaml:"role"` // role name reference
Attributes map[string]string `yaml:"attributes"` // optional ABAC attributes
}
APIKeyDef defines an API key in configuration.
type AccessControlPolicy ¶
type AccessControlPolicy struct {
Name string `yaml:"name" json:"name"`
Version int `yaml:"version" json:"version"`
Rules []PolicyRule `yaml:"rules" json:"rules"`
Enabled bool `yaml:"enabled" json:"enabled"`
}
AccessControlPolicy groups related rules.
func MigrateRBACToABAC ¶
func MigrateRBACToABAC(roles []RoleConfig, cellPolicies []PolicyConfig) ([]AccessControlPolicy, error)
MigrateRBACToABAC converts legacy RBAC role and policy configs into equivalent ABAC policies. This runs at startup when ABACPolicies is empty but Roles/Policies are populated, providing full backward compatibility.
It is THE shipped enforcement path for a `policies:` block — the obligations it emits are what auth.EnforcePlanPolicies injects into the plan — so it reads column actions through the same ParseColumnAction as ParsePolicies and fails on the same inputs (#802). Its own switch previously matched "deny" and "mask" case-sensitively and dropped everything else, which turned both a typo AND a capitalised `Mask` into no obligation at all: a silent grant on the very path that enforces.
type AccessPolicy ¶
type AccessPolicy struct {
Table string // table name this policy applies to
Role string // role name this policy applies to
Columns map[string]ColumnPolicy // column name -> policy
MaskValues map[string]CellMaskFunc // column name -> custom mask function (optional)
RowFilter string // SQL predicate appended to WHERE clause (e.g. "region = 'us'")
}
AccessPolicy defines cell-level access control for a table. Policies are evaluated per-role to filter rows and mask/deny columns.
type Attributes ¶
Attributes is a string-keyed bag of values carried by subjects, resources, and environments.
func (Attributes) Get ¶
func (a Attributes) Get(key string) any
Get returns the value for a key, or nil if not present.
func (Attributes) GetString ¶
func (a Attributes) GetString(key string) string
GetString returns the string value for a key, or "" if not present or not a string.
type AuditLogger ¶
type AuditLogger struct {
// contains filtered or unexported fields
}
AuditLogger records security-relevant events for compliance and forensics.
func NewAuditLogger ¶
func NewAuditLogger(logger *slog.Logger) *AuditLogger
NewAuditLogger creates an audit logger that writes structured security events.
func (*AuditLogger) LogAccessDenied ¶
func (a *AuditLogger) LogAccessDenied(identity *Identity, resource, reason string)
LogAccessDenied records authorization failures.
func (*AuditLogger) LogAuthFailure ¶
func (a *AuditLogger) LogAuthFailure(remoteAddr, path, reason string)
LogAuthFailure records authentication failures.
func (*AuditLogger) LogColumnPolicy ¶
func (a *AuditLogger) LogColumnPolicy(identity *Identity, table string, masked, denied []string)
LogColumnPolicy records when columns are masked or denied.
func (*AuditLogger) LogQuery ¶
func (a *AuditLogger) LogQuery(identity *Identity, sql string, tables []string, elapsed time.Duration, err error)
LogQuery records a query execution with identity context.
func (*AuditLogger) LogRowFilterApplied ¶
func (a *AuditLogger) LogRowFilterApplied(identity *Identity, table, filter string)
LogRowFilterApplied records when a row filter is injected into a query plan.
type Authenticator ¶
type Authenticator struct {
// contains filtered or unexported fields
}
Authenticator verifies caller identity from HTTP requests.
func (*Authenticator) Authenticate ¶
func (a *Authenticator) Authenticate(r *http.Request) (*Identity, error)
Authenticate extracts and verifies identity from an HTTP request. It tries methods in order: mTLS (from TLS state), API key (Bearer token), JWT (Bearer token).
func (*Authenticator) AuthenticateToken ¶
func (a *Authenticator) AuthenticateToken(token string) (*Identity, error)
AuthenticateToken verifies identity from a raw bearer token (API key or JWT). Used by non-HTTP frontends (pgwire, gRPC) where there is no http.Request.
func (*Authenticator) ConfigError ¶ added in v0.18.53
func (a *Authenticator) ConfigError() error
ConfigError is the configuration error this Authenticator was built from, or nil. Non-nil means every credential is refused.
func (*Authenticator) Enabled ¶
func (a *Authenticator) Enabled() bool
Enabled reports whether authentication is active.
It is the operator's `auth.enabled` intent OR the presence of a mechanism — the second half because a configuration that lists api_keys without saying `enabled: true` has always been enforced, and dropping that would open every such deployment. What it is NOT any more is a census that a FAILED mechanism can turn off: an Authenticator built from a broken configuration reports enabled and refuses everything (#931).
type Authorizer ¶
type Authorizer struct {
// contains filtered or unexported fields
}
Authorizer checks whether an identity has permission to perform operations.
func (*Authorizer) CanAccessTable ¶
func (a *Authorizer) CanAccessTable(id *Identity, tableName string) bool
CanAccessTable checks whether the identity's role grants access to a table.
func (*Authorizer) FilterTables ¶
func (a *Authorizer) FilterTables(id *Identity, tables []string) []string
FilterTables returns only tables the identity is allowed to see.
func (*Authorizer) HasPermission ¶
func (a *Authorizer) HasPermission(id *Identity, perm string) bool
HasPermission checks whether the identity has the given permission. Valid permissions: "read", "write", "admin".
func (*Authorizer) ResolveRole ¶ added in v0.18.53
func (a *Authorizer) ResolveRole(id *Identity) bool
ResolveRole fills id's Tables and Perms from the role definitions this Authorizer holds NOW, and reports whether the role was found.
It is how an identity reconstructed from a stored snapshot gets its grants. A snapshot records who the definer WAS — name, role, method, attributes — and deliberately not what they could do: persisting `Perms` and `Tables` would freeze a grant at creation time, so an alert created by a role that has since been narrowed, or removed, would keep running under the old one. Re-resolving at run time means the CURRENT configuration decides, every tick, and a definer whose role is gone holds nothing at all — which is the right answer and the fail-closed one.
An identity that already carries grants is left alone: only a role the configuration defines can add any.
type CellMaskFunc ¶
CellMaskFunc replaces a cell value with a masked version. Receives the original value and returns the masked value.
type ColumnDecision ¶
type ColumnDecision struct {
Column string `json:"column"`
Allowed bool `json:"allowed"`
MaskFunc string `json:"mask_func,omitempty"`
MaskExpr string `json:"mask_expr,omitempty"`
}
ColumnDecision describes the access decision for a single column.
type ColumnPolicy ¶
type ColumnPolicy int
ColumnPolicy defines how a column is handled for a given role.
const ( // ColumnAllow permits full access to the column value. ColumnAllow ColumnPolicy = iota // ColumnMask replaces the value with a redacted placeholder. ColumnMask // ColumnDeny removes the column from results entirely. ColumnDeny )
func ParseColumnAction ¶ added in v0.18.14
func ParseColumnAction(column, action string) (ColumnPolicy, error)
ParseColumnAction maps one YAML column action to its ColumnPolicy.
An unrecognised action is an ERROR, not a default. Before #802 the default arm returned ColumnAllow, so `columns: {src_ip: "***REDACTED***"}` — the spelling every version of docs/security.md recommended — parsed to a full grant, and an operator who believed a PII column was masked was served it in the clear. A column-access control that cannot be understood must refuse to load: loud beats plausible, and a security control never degrades to a grant.
type Condition ¶
type Condition struct {
Attribute string `yaml:"attribute" json:"attribute"`
Op string `yaml:"op" json:"op"` // eq, neq, in, not_in, gt, lt, gte, lte, contains, exists
Value any `yaml:"value" json:"value"`
}
Condition is a single predicate in a policy rule. Attribute uses dot-path notation: "subject.role", "resource.name", "env.source_ip".
type Config ¶
type Config struct {
Enabled bool `yaml:"enabled"`
APIKeys []APIKeyDef `yaml:"api_keys"`
JWT JWTConfig `yaml:"jwt"`
MTLS MTLSConfig `yaml:"mtls"`
Roles []RoleConfig `yaml:"roles"`
}
Config holds authentication configuration.
type Decision ¶
type Decision struct {
Allowed bool
Effect Effect
MatchedRule string
Obligations []Obligation
Reason string
}
Decision is the outcome of a single policy evaluation.
type Environment ¶
type Environment struct {
Time time.Time
SourceIP string
Protocol string // "http", "pgwire", "grpc"
Custom Attributes
}
Environment carries contextual attributes for policy evaluation.
func DecisionEnvironment ¶ added in v0.18.53
func DecisionEnvironment(ctx context.Context, protocol string) Environment
DecisionEnvironment is the Environment a policy decision is evaluated against: what the protocol boundary attached, with `Time` stamped NOW and `protocol` as the fallback label when no boundary named one.
Every shared enforcement path builds its environment through this and through nothing else. They used to build `Environment{Protocol: protocol}` by hand — no time, no address — so `env.time`, `env.hour` and `env.source_ip` were never published to the evaluator and every environment-conditioned rule matched nothing. Beside a broad allow, a deny that matches nothing is a grant (#933).
The attached protocol WINS over the label when there is one: the label describes the execution path (`"embedded"` even for a statement that arrived over pgwire), and `env.protocol` means the door the client used.
func EnvironmentFromContext ¶ added in v0.18.53
func EnvironmentFromContext(ctx context.Context) Environment
EnvironmentFromContext returns the environment attached by the protocol boundary, or the zero Environment when none was attached (an embedded caller, a background task, a test).
The zero value is not a refusal: a policy that names no environment condition decides the same either way, and one that does simply does not match — which, beside a broad allow, is why the environment has to be attached on every door rather than left to whoever remembers (#933).
type Identity ¶
type Identity struct {
Name string // human-readable label (e.g. "grafana-prod", "alice@example.com")
Role string // resolved role name
Method string // how they authenticated: "apikey", "jwt", "mtls"
Tables []string // allowed tables from role (["*"] = all)
Perms []string // allowed permissions from role
Attributes Attributes // extended attributes for ABAC (from JWT claims, mTLS cert, config)
}
Identity represents an authenticated caller.
func IdentityFromContext ¶
IdentityFromContext extracts the authenticated identity from a request context. Returns nil if no identity is present (auth disabled or unauthenticated).
type IdentitySnapshot ¶
type IdentitySnapshot struct {
Name string `json:"name,omitempty"`
Role string `json:"role,omitempty"`
Method string `json:"method,omitempty"`
Attributes map[string]string `json:"attributes,omitempty"`
}
IdentitySnapshot is the persistable subset of an Identity sufficient to re-establish its ABAC subject later (see Identity.ToSubject, which keys on role/name/method plus attributes). It is stored with definer's-rights resources — an alert runs under its creator's identity on every scheduled tick, so the creator's role and attributes must survive in the catalog. Tables/Perms are intentionally omitted, and that is now load-bearing rather than an economy: they are pure configuration, resolved from the ROLE, so persisting them would freeze a grant at creation time. `StampDefiner` re-resolves them from the Authorizer's current roles on every tick (`Authorizer.ResolveRole`), which is what makes a narrowed or deleted role take effect on the alerts its holder created.
func SnapshotIdentity ¶
func SnapshotIdentity(ctx context.Context) IdentitySnapshot
SnapshotIdentity captures the identity in ctx as an IdentitySnapshot. The zero snapshot (all fields empty) means no identity was present — callers persisting it record "no definer", which the scheduler treats fail-closed.
func (IdentitySnapshot) Empty ¶
func (s IdentitySnapshot) Empty() bool
Empty reports whether the snapshot carries no usable identity — either no definer was recorded (pre-definer-rights alert) or it was created with no authenticated identity. Enforcement treats an empty snapshot fail-closed.
func (IdentitySnapshot) ToIdentity ¶
func (s IdentitySnapshot) ToIdentity() *Identity
ToIdentity reconstructs an *Identity for context stamping. Attributes are widened back to the Attributes (map[string]any) shape ToSubject expects.
type JWTConfig ¶
type JWTConfig struct {
Enabled bool `yaml:"enabled"`
Secret string `yaml:"secret"` // HMAC-SHA256 secret
PublicKeyFile string `yaml:"public_key_file"` // PEM file path for RSA verification
RoleClaim string `yaml:"role_claim"` // JWT claim containing role (default: "role")
Issuer string `yaml:"issuer"` // expected issuer (optional)
}
JWTConfig holds JWT verification configuration.
type JWTVerifier ¶
type JWTVerifier struct {
// contains filtered or unexported fields
}
JWTVerifier validates JWT tokens and extracts identities.
func NewJWTVerifier ¶
func NewJWTVerifier(cfg JWTConfig, roles map[string]*RoleDef) (*JWTVerifier, error)
NewJWTVerifier creates a JWT verifier from configuration.
type MTLSConfig ¶
type MTLSConfig struct {
Enabled bool `yaml:"enabled"`
CAFile string `yaml:"ca_file"` // CA certificate PEM file for verifying client certs
RoleMap map[string]string `yaml:"role_map"` // CN or SAN -> role mapping
DefaultRole string `yaml:"default_role"` // role for valid certs not in role_map
}
MTLSConfig holds mTLS authentication configuration.
type MTLSVerifier ¶
type MTLSVerifier struct {
// contains filtered or unexported fields
}
MTLSVerifier authenticates clients via TLS client certificates.
func NewMTLSVerifier ¶
func NewMTLSVerifier(cfg MTLSConfig, roles map[string]*RoleDef) *MTLSVerifier
NewMTLSVerifier creates an mTLS verifier from configuration.
func (*MTLSVerifier) Verify ¶
func (v *MTLSVerifier) Verify(cert *x509.Certificate) (*Identity, error)
Verify extracts identity from a verified client certificate. The TLS handshake (certificate chain validation) is handled by Go's TLS stack via tls.Config.ClientAuth + ClientCAs. This method maps the verified cert to a role.
type Obligation ¶
type Obligation struct {
Type string `yaml:"type" json:"type"` // mask_column, deny_column, row_filter, query_limit
Target string `yaml:"target" json:"target"` // column name, table name
Value string `yaml:"value" json:"value"` // SQL predicate, mask expression
MaskFunc string `yaml:"mask_func" json:"mask_func"` // redact, hash, nullify, partial
}
Obligation is a side-effect applied when a rule matches.
type PolicyConfig ¶
type PolicyConfig struct {
Table string `yaml:"table"`
Role string `yaml:"role"`
Columns map[string]string `yaml:"columns"` // column name -> "allow", "mask", "deny"
RowFilter string `yaml:"row_filter"` // SQL WHERE predicate
}
PolicyConfig is the YAML representation of cell-level access policies.
type PolicyEvaluator ¶
type PolicyEvaluator struct {
// contains filtered or unexported fields
}
PolicyEvaluator evaluates access control policies using deny-overrides.
func NewPolicyEvaluator ¶
func NewPolicyEvaluator(policies []AccessControlPolicy) *PolicyEvaluator
NewPolicyEvaluator creates an evaluator from a set of policies.
An effect that resolves to neither "allow" nor "deny" becomes a DENY. It used to become an ALLOW — every string but a case-insensitive "deny" did, the empty string included — so `effect: dney` granted exactly the action the operator had written a deny for (#932). `ValidateABACPolicies` refuses such a rule at load and is the message an operator sees; this is the floor under a programmatic caller who never ran the validator, and a floor under a security decision falls the safe way.
func (*PolicyEvaluator) Evaluate ¶
func (pe *PolicyEvaluator) Evaluate(subject Subject, resource Resource, action Action, env Environment) Decision
Evaluate runs deny-overrides policy evaluation.
Algorithm:
- Collect all rules whose conditions match the request context.
- If ANY matching rule has Effect=Deny, result is Deny (highest-priority deny cited).
- If no deny matches and at least one Allow matches, result is Allow. Obligations are merged from all matching Allow rules.
- If no rules match at all, default is Deny (closed-world assumption).
func (*PolicyEvaluator) EvaluateTableAccess ¶
func (pe *PolicyEvaluator) EvaluateTableAccess(subject Subject, tableName string, action Action, env Environment) *TableDecision
EvaluateTableAccess evaluates table-level access and collects column/row obligations.
type PolicyRule ¶
type PolicyRule struct {
ID string `yaml:"id" json:"id"`
Description string `yaml:"description" json:"description"`
Effect Effect `yaml:"-" json:"-"`
EffectStr string `yaml:"effect" json:"effect"` // "allow" or "deny" for serialization
Priority int `yaml:"priority" json:"priority"` // lower = higher priority
Subjects []Condition `yaml:"subjects" json:"subjects"`
Resources []Condition `yaml:"resources" json:"resources"`
Actions []Action `yaml:"actions" json:"actions"`
Environment []Condition `yaml:"environment" json:"environment"`
Obligations []Obligation `yaml:"obligations" json:"obligations"`
}
PolicyRule is one rule within an access control policy.
type PolicySet ¶
type PolicySet struct {
// contains filtered or unexported fields
}
PolicySet holds all access policies, indexed by table+role.
func ParsePolicies ¶
func ParsePolicies(configs []PolicyConfig) (*PolicySet, error)
ParsePolicies converts YAML policy configs into a PolicySet. It fails on the first unrecognised column action, naming the table, role, column and the value it could not read (#802).
func (*PolicySet) Lookup ¶
func (ps *PolicySet) Lookup(table, role string) *AccessPolicy
Lookup returns the policy for a given table and role, or nil if none exists.
type Provider ¶
type Provider struct {
// contains filtered or unexported fields
}
Provider wraps Authenticator, Authorizer, PolicySet, and PolicyEvaluator behind an atomic pointer so they can be swapped on config reload without locks.
func NewProvider ¶
func NewProvider(authn *Authenticator, authz *Authorizer, policies *PolicySet, logger *slog.Logger) *Provider
NewProvider creates a Provider from initial auth components. Any parameter may be nil (auth disabled).
func (*Provider) Audit ¶ added in v0.18.38
func (p *Provider) Audit() *AuditLogger
Audit returns the provider's audit logger. Never nil.
func (*Provider) Authenticator ¶
func (p *Provider) Authenticator() *Authenticator
Authenticator returns the current Authenticator. Lock-free.
func (*Provider) Authorizer ¶
func (p *Provider) Authorizer() *Authorizer
Authorizer returns the current Authorizer. Lock-free.
func (*Provider) BindError ¶ added in v0.18.47
BindError reports why the attached policy set could not be bound to the catalog, or nil.
A non-nil value is a REFUSAL, not a warning: `EnforcePlanPolicies` and `EnforceDMLPolicies` both return it rather than run, so a set that names a relation the catalog does not hold cannot be attached and then silently enforce nothing. That is ADR-0033 rule 3 read the way an attach has to implement it — the alternative is a policy file that loads clean, matches nothing, and beside a broad allow is a grant (#882).
func (*Provider) BindToCatalog ¶ added in v0.18.47
BindToCatalog attaches the catalog a policy's names are resolved against and binds the CURRENT policy set to it.
It is separate from NewProvider because of startup order: the provider is built from the config file, and the catalog does not exist yet at that point. Calling this is what turns "a policy that names no relation" from a rule that silently never matches into a startup refusal.
A failure returns the error and swaps NOTHING — the caller decides whether that is fatal (it is, at startup) — and the provider keeps running on the policy set it already had.
It is IDEMPOTENT: attaching a set that is already bound to this catalog writes nothing at all. The HTTP DML door re-attaches per statement, so that is a request-path property, not an optimization.
The swap is a CAS, not a store. BindToCatalog reads the running set, binds a COPY of it, and installs the copy; a set installed between the read and the install would otherwise be OVERWRITTEN by the older snapshot — a retired policy set coming back, which is a security control silently reverting. On a lost CAS the newly installed set is bound instead.
func (*Provider) Evaluator ¶
func (p *Provider) Evaluator() *PolicyEvaluator
Evaluator returns the current ABAC PolicyEvaluator. Lock-free.
func (*Provider) Update ¶
func (p *Provider) Update(authn *Authenticator, authz *Authorizer, policies *PolicySet)
Update atomically replaces all auth components, binding them to the attached catalog (installState). A set that cannot be bound is NOT installed, the previous one keeps running, and the refusal is remembered: every statement is refused until a bindable set is installed.
func (*Provider) UpdateFromConfig ¶
func (p *Provider) UpdateFromConfig(cfg Config, policyCfgs []PolicyConfig, abacPolicies ...AccessControlPolicy) error
UpdateFromConfig rebuilds auth from a Config and atomically swaps. If abacPolicies is non-empty, builds an ABAC evaluator. Otherwise, if RBAC roles and cell policies are present, auto-migrates them to ABAC.
A policy this cannot read returns an error and swaps NOTHING: the provider keeps the state it already had. That matters most on hot reload, where the alternative to refusing an unreadable `columns:` action is installing a weaker policy set than the operator asked for (#802).
func (*Provider) UpdateWithEvaluator ¶
func (p *Provider) UpdateWithEvaluator(authn *Authenticator, authz *Authorizer, policies *PolicySet, evaluator *PolicyEvaluator)
UpdateWithEvaluator atomically replaces all auth components including the ABAC evaluator, binding them to the attached catalog (see Update).
type RateLimitConfig ¶
type RateLimitConfig struct {
RequestsPerSecond float64 // max requests per second per identity
Burst int // max burst size (token bucket capacity)
CleanupInterval time.Duration // how often to evict idle entries (default 5m)
}
RateLimitConfig configures per-identity rate limiting.
type RateLimiter ¶
type RateLimiter struct {
// contains filtered or unexported fields
}
RateLimiter tracks per-identity request rates.
func NewRateLimiter ¶
func NewRateLimiter(cfg RateLimitConfig) *RateLimiter
NewRateLimiter creates a new per-identity rate limiter.
func (*RateLimiter) Allow ¶
func (rl *RateLimiter) Allow(identity string) bool
Allow checks if the identity is within its rate limit.
type Resource ¶
type Resource struct {
Type string // "table", "column", "function"
Name string // e.g. "events", "users.ssn"
Attributes Attributes // e.g. {"classification": "SECRET", "owner": "alice"}
}
Resource describes what is being accessed.
func TableFunctionResource ¶ added in v0.18.53
TableFunctionResource is the ABAC resource one table-function scan presents.
The attributes are derived from the ARGUMENTS, before anything is opened, so a policy can scope the capability to a destination:
path a local filesystem path or glob — with a leading "~/" expanded and
the result cleaned, so `~/x`, `/data/../etc/passwd` and
`/etc/passwd` are not three different strings a prefix rule has to
know about. Cleaning is safe because the cleaned and uncleaned forms
open the same file.
url an http(s) source, verbatim.
host the host of `url`, or the host of a database connector's connection
string. Empty when the connection string cannot be parsed — which,
under a default-deny evaluator, refuses.
The connection STRING itself is never an attribute: it carries a password.
type RoleConfig ¶
type RoleConfig struct {
Name string `yaml:"name"` // e.g. "admin", "reader", "analyst"
Tables []string `yaml:"tables"` // table names or "*" for all
Allow []string `yaml:"allow"` // permissions: "read", "write", "admin"
}
RoleConfig defines a role in configuration.
type RowFilters ¶
RowFilters is a map of table name → SQL predicate for row-level security.
func RowFiltersFromContext ¶
func RowFiltersFromContext(ctx context.Context) RowFilters
RowFiltersFromContext extracts row filter predicates from context.
type SerializedDecision ¶
type SerializedDecision struct {
Allowed bool `json:"allowed"`
TableDecisions map[string]*TableDecision `json:"table_decisions,omitempty"`
MaxScanBytes int64 `json:"max_scan_bytes,omitempty"`
MaxScanRows int64 `json:"max_scan_rows,omitempty"`
}
SerializedDecision is the pre-evaluated policy decision that travels with distributed tasks. Workers enforce these without access to policies.
type Subject ¶
type Subject struct {
Identity *Identity
Attributes Attributes // merged from JWT claims, mTLS cert fields, config, API key def
}
Subject represents the authenticated caller with attributes for policy evaluation.
type TableDecision ¶
type TableDecision struct {
Allowed bool `json:"allowed"`
Columns []ColumnDecision `json:"columns,omitempty"`
RowFilter string `json:"row_filter,omitempty"`
Reason string `json:"reason,omitempty"`
RuleID string `json:"rule_id,omitempty"`
// QueryLimits is the cost ceiling a `query_limit` obligation puts on this
// identity for this relation, or nil for none. EnforcePlanPolicies
// narrows the statement's guard with it; see query_limit.go.
QueryLimits *config.QueryLimits `json:"query_limits,omitempty"`
}
TableDecision is the fully-resolved access decision for a table.
type TableDecisions ¶
type TableDecisions map[string]*TableDecision
TableDecisions maps table names to their ABAC-evaluated access decisions.
func TableDecisionsFromContext ¶
func TableDecisionsFromContext(ctx context.Context) TableDecisions
TableDecisionsFromContext extracts ABAC table decisions from context.
type UDFMutation ¶ added in v0.18.53
type UDFMutation struct {
// Owner is the name recorded on the definition, and the name the lock is
// later checked against. Empty only when there is no provider — with one,
// a mutation without an identity does not get this far.
Owner string
// IsAdmin says whether this caller may override ANOTHER owner's
// `WITH LOCK`. It comes from `Authorizer.HasPermission(id, "admin")` —
// the configured permission set — never from the role's name.
IsAdmin bool
}
UDFMutation is everything a CREATE / DROP FUNCTION needs from the authorizer, decided in ONE place because the two doors that ran those statements had each decided it for themselves and each got it wrong in a different direction (#940, #942):
- `wadjet.DB.Query` — the boundary the embedded caller and pgwire both reach — asked for no permission at all, recorded an EMPTY owner, and passed the literal `isAdmin = true` into `UDFStore.Register` / `Unregister`. That bit is the only thing the `WITH LOCK` ownership check consults, so a role holding only `read` installed process-global functions, replaced another user's locked one and dropped it.
- The HTTP handlers asked for no permission either, and computed `isAdmin` as `identity.Role == "admin"` — the role's NAME rather than its permissions. Both directions were wrong at once: a role literally named `admin` whose `allow:` list is `[read]` overrode another owner's lock, while a role named `ops` that actually HOLDS `admin` was refused.
`expr.DefaultUDFs` and `expr.DefaultRegistry` are process-global, so every one of those crossed connection, role and tenant boundaries: replacing a widely used UDF changes other identities' query RESULTS.
func AuthorizeUDFMutation ¶ added in v0.18.53
func AuthorizeUDFMutation(ctx context.Context, provider *Provider) (UDFMutation, error)
AuthorizeUDFMutation decides a CREATE / DROP FUNCTION.
Mutating the registry needs `write`, the same permission every other mutation on this engine needs; overriding another owner's locked function needs `admin`. No new permission words (ADR-0034).
Fail-closed contract, the one `RequirePermission` already keeps: provider nil or auth disabled → the mutation is ALLOWED (there is no permission to require), but the caller is NOT an administrator and records no owner. Auth enabled and no identity → refused 42501.