Documentation
¶
Overview ¶
Package authz is the framework-level **authorization decision vocabulary** (the PDP contract) returned by auth.Authorizer. It is reusable by any cell.
XACML PDP/PEP/PIP/PAP Role Mapping ¶
- PDP contract = auth.Authorizer + pkg/authz (Decision / Effect / Obligations)
- PEP = repos / handlers that enforce the verdict (check IsAllow(), apply Obligations.RowScope, mask columns per Obligations.FieldMask)
- PIP = JWT claims / repos / clock that supply attribute values to the policy engine
- PAP = policymanage cell (policy CRUD, PR-10)
Layering ¶
pkg/authz may import ONLY pkg/* (pkg/tenant, pkg/errcode) and the standard library. It must NOT import kernel/, runtime/, cells/, or adapters/.
Sealed Decision — P0 Bypass Prevention ¶
Decision is the sealed authorization verdict. All fields are unexported: the ONLY way to produce a Decision is via Allow() or Deny(). A business handler cannot forge `authz.Decision{Effect: authz.EffectAllow}` because that struct literal is a compile error outside this package. This seals the P0 authz-bypass vector (allowing access without an actual policy evaluation).
The zero-value Decision{} has Effect() that does NOT equal EffectAllow (fail-closed), so any consumer testing d.IsAllow() fails closed on an uninitialized Decision.
FR-019: a Decision never crosses an async boundary. With no exported fields and no Unmarshal capability it is structurally non-serializable, reinforcing that invariant.
Deferred PR-7 Downstream Caller-Allowlist ¶
The downstream funnel half — a caller-allowlist restricting Allow()/Deny() to only the PDP evaluation engine (authorizationdecide) — lands in PR-7 (tracking issue: gh #1345) when a production producer first exists; today there is no production caller so an allowlist would be vacuous. The upstream Hard (sealed fields → literal forge impossible) is achieved here.
Per ai-robust.md §"Funnel 双向锁评级": a Medium-upstream → Hard-downstream transitional funnel must name its Hard-ization tracker. That tracker is gh #1345 (PR-7). Once PR-7 ships a production Allow()/Deny() caller, the companion archtest in tools/archtest/authz_decision_sealed_test.go must be extended with an AST caller-allowlist guard (only the authorizationdecide engine may call Allow() or Deny()).
Combining Algorithm ¶
Deny-overrides (XACML §7.16) / forbid-wins (Cedar): when multiple policies apply to a request, any Deny result causes the combined decision to be Deny, regardless of how many Allow results are also present. This semantics is DOCUMENTED here but the ENFORCEMENT (an evaluator that applies deny-overrides) is PR-7, not this package.
References ¶
- ref: cedar-policy/cedar-go types.go (Decision 2-value enum pattern)
- ref: cedar-policy/cedar-go authorize.go (Decision+Diagnostic projection)
- ref: XACML-3.0 §3.5 Obligations (mandatory enforcement obligations)
- ref: XACML-3.0 §7.16 deny-overrides combining algorithm
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ValidAttributeKey ¶
ValidAttributeKey validates a canonical attribute/column identifier. It must be non-empty, contain no leading/trailing or internal whitespace, no control chars, and match ^[A-Za-z][A-Za-z0-9_.]*$. Returns KindInvalid/ErrValidationFailed otherwise.
This function is shared by FieldMask.Validate and Condition.Validate to prevent silent masking/evaluation no-ops caused by keys like " ssn" or "ssn\n" that would never match at enforcement time.
Types ¶
type Decision ¶
type Decision struct {
// contains filtered or unexported fields
}
Decision is the sealed authorization verdict returned by a PDP (auth.Authorizer).
All fields are unexported: the ONLY way to produce a Decision is Allow() / Deny(), so a business handler cannot forge `authz.Decision{Effect: EffectAllow}` (compile error outside this package) — a P0 authz-bypass vector this seal closes.
The zero value (Decision{}) has Effect() that is NOT EffectAllow → consumers testing d.IsAllow() fail closed on a zero value.
FR-019: a Decision never crosses an async boundary; with no exported fields and no Unmarshal this type is structurally non-serializable, reinforcing that invariant.
Combining Algorithm ¶
Deny-overrides (XACML §7.16) / forbid-wins (Cedar): any EffectDeny result wins over any number of EffectAllow results. This semantics is DOCUMENTED here but ENFORCEMENT (an evaluator that applies deny-overrides) lands in PR-7.
AI-robust Grade ¶
Upstream Hard: sealed unexported fields make outside-package struct-literal construction a compile error. This is the "sealed construction" range item from the ai-robust.md Hard 范本目录.
Downstream caller-allowlist: deferred to PR-7 (#1345) — vacuous today because there is no production Allow()/Deny() caller yet.
func Allow ¶
func Allow(o Obligations) (Decision, error)
Allow validates o and returns a permit Decision carrying the cloned obligations the PEP must discharge. It returns an error if o fails Obligations.Validate() — the returned Decision is the zero value (which IsAllow()==false, fail-closed) in that case.
The obligations are only meaningful when IsAllow() == true; PEPs SHOULD NOT enforce Obligations from a Deny Decision.
This is an intentional breaking signature change from the zero-error variant: invalid obligations must not produce an Allow decision (fail-closed).
func Deny ¶
Deny returns a fail-closed deny Decision.
reason MUST be a programmer-authored, static/descriptive string — runtime business data (subject IDs, resource IDs, policy IDs, etc.) must NOT be embedded in reason. Use the errcode Internal channel for runtime diagnostics. This mirrors the errcode.Message const-literal discipline: reason flows to slog and must not carry PII or user-controlled content.
An empty reason is discouraged. A fail-closed deny should carry a brief diagnostic string (e.g. "policy evaluation: no matching allow rule") per observability rules — it aids on-call triage without exposing sensitive data. Deny("") is accepted (no behavior change) but callers are expected to provide a meaningful static string.
The framework does not interpret the reason's structure, and reason intentionally does NOT carry accesscore's PolicyID type — keeping pkg/authz free of accesscore dependencies.
func (Decision) Effect ¶
Effect reports the verdict. The zero-value Decision returns a non-Allow effect (fail-closed): any consumer testing d.Effect() == EffectAllow will deny access on a zero Decision.
func (Decision) IsAllow ¶
IsAllow reports whether the decision permits the action. This is the idiomatic check for PEP enforcement code:
if !dec.IsAllow() {
return nil, ErrForbidden
}
Returns false for the zero-value Decision (fail-closed).
func (Decision) Obligations ¶
func (d Decision) Obligations() Obligations
Obligations returns a defensive copy of the mandatory obligations the PEP must enforce when the Decision is Allow. The returned value has a fresh FieldMask.Fields backing array so callers cannot mutate the sealed verdict by modifying the slice in place.
Obligations are meaningless on a Deny Decision.
func (Decision) Reason ¶
Reason returns the opaque server-side diagnostic string supplied to Deny(). Empty string on an Allow Decision. Intended for slog/logging only.
Callers MUST NOT surface this string to external clients or embed it in wire payloads — it is a server-side diagnostic aid only (per observability rules). For runtime context (IDs, counts), attach them as slog attributes separately rather than concatenating into reason.
type Effect ¶
type Effect uint8
Effect is the authorization verdict enum. iota+1 zero-invalid convention (mirroring kernel/saga.Status, kernel/outbox.Disposition, pkg/tenant.RowScope) ensures the zero value is never mistaken for a valid effect.
XACML: Permit/Deny; Cedar: permit/forbid.
const ( // EffectAllow permits the requested action. Corresponds to XACML Permit / // Cedar permit. EffectAllow Effect = iota + 1 // EffectDeny forbids the requested action. Corresponds to XACML Deny / // Cedar forbid. Deny-overrides (XACML §7.16) means any EffectDeny wins // over any number of EffectAllow results. EffectDeny )
func (Effect) String ¶
String returns the wire/log spelling of the effect, or "invalid" for the zero value and any out-of-range value.
type FieldMask ¶
type FieldMask struct {
// Fields is the ordered list of column names to mask. Order is advisory
// (PEPs apply masking by set membership). An empty slice is valid and
// means "mask nothing" (identity projection).
Fields []string
}
FieldMask specifies the set of column names that the PEP must mask in the response. It is an open (non-sealed) obligation data struct — the names are inert strings and carry no runtime security themselves; enforcement is entirely on the PEP side.
An empty FieldMask (IsZero() == true) is VALID and means "identity projection / no masking". This avoids MVP→PR-11 response-type churn: a Decision carrying FieldMask{} is identical to a Decision with no column masking obligation. PEPs SHOULD treat a zero FieldMask as a no-op.
type Obligations ¶
type Obligations struct {
// RowScope is the row-visibility obligation. A zero value is valid and
// means "this policy does not impose a row-scope constraint"; the PR-7
// evaluator resolves the effective scope from all applicable obligations
// combined with the principal→RowScope derivation (PR-5). A non-zero
// value must pass tenant.RowScope.Validate().
RowScope tenant.RowScope
// FieldMask specifies which response columns to mask. An empty FieldMask
// is valid (no masking required).
FieldMask FieldMask
}
Obligations is the bounded set of mandatory XACML obligations that the PEP must discharge when the Decision is Allow. Obligations are only meaningful on an Allow decision; a PEP MUST NOT enforce obligations from a Deny decision.
Two obligation axes are modeled:
RowScope: the row-visibility predicate the PEP must apply to list/get queries. A zero RowScope is valid here — it means "this policy does not itself constrain row scope" (the policy obligation is absent). The *effective* row scope for a request is resolved by the PR-7 evaluator, which combines all policy obligations with the principal→RowScope derivation (PR-5). Only a non-zero RowScope carries an explicit obligation; it must be a valid tenant.RowScope value.
FieldMask: the set of columns to mask in the response. An empty FieldMask is valid and means "identity projection / no masking".
ref: XACML-3.0 §3.5 — obligations are MANDATORY; advice is OPTIONAL. GoCell only models mandatory obligations in this type.
func (Obligations) Validate ¶
func (o Obligations) Validate() error
Validate returns an error if any obligation field is in an invalid state. A zero RowScope is valid (zero = "this policy does not impose a row-scope constraint"; the PR-7 evaluator resolves the effective scope). A non-zero RowScope must pass tenant.RowScope.Validate().