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.
Downstream Caller-Allowlist (DELIVERED PR-7 #1345) ¶
The downstream funnel half — a caller-allowlist restricting Allow()/Deny() to only the PDP evaluation engine — was delivered in PR-7 (#1345) when the first production producer (the authorizationdecide ABAC evaluator) landed. The archtest AUTHZ-DECISION-ALLOW-DENY-CALLER-01 in tools/archtest/authz_decision_sealed_test.go pins every production reference to Allow()/Deny() to corecells/accesscore/slices/authorizationdecide/evaluator.go.
The funnel is now fully closed Hard/Hard (ai-robust.md §"Funnel 双向锁评级"): upstream Hard (sealed unexported fields → literal forge impossible; the only Decision-returning exported funcs are Allow/Deny) + downstream Hard (use-based caller-allowlist, invariant to import form). No Hard-upgrade issue is needed.
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 ¶
- func ValidAttributeKey(s string) error
- type Decision
- type Effect
- type FieldMask
- type Obligations
- type Permission
- func PermAuditRead() Permission
- func PermConfigPublish() Permission
- func PermConfigRead() Permission
- func PermConfigWrite() Permission
- func PermFlagRead() Permission
- func PermFlagWrite() Permission
- func PermPolicyRead() Permission
- func PermPolicyWrite() Permission
- func PermRoleRead() Permission
- func PermUserRead() Permission
- func PermUserWrite() Permission
- func Permissions() []Permission
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 ParseEffect ¶
ParseEffect converts a wire/log spelling back to its Effect value (inverse of String). Recognized codes: "allow" → EffectAllow, "deny" → EffectDeny. Any other input — including the empty string — returns an error (fail-closed).
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.
func IdentityFieldMask ¶
func IdentityFieldMask() FieldMask
IdentityFieldMask returns the empty (identity-projection) FieldMask: it masks nothing, so a PEP serializes the full column set. It is the NAMED intent marker for a read whose column visibility is not (yet) constrained — every such callsite is greppable as `authz.IdentityFieldMask()` rather than an anonymous `authz.FieldMask{}` that reads ambiguously as "identity" vs "TODO: real mask". When the ABAC policy engine is wired (PR-10 #1348), the swap is a one-line replacement of this call with Decision.Obligations().FieldMask at each site.
func (FieldMask) IsZero ¶
IsZero reports whether the FieldMask has no masking columns (len(Fields)==0). A zero FieldMask is a valid obligation meaning "no column masking required".
func (FieldMask) Masks ¶
Masks reports whether column is in the mask's field set. A zero FieldMask masks nothing (always false). This is the obligation-query a PEP uses to decide whether a given column is governed by the mask — e.g. to reject a query predicate on a column the caller cannot see (a masked-column filter would leak a match/no-match oracle on a value the response redacts).
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) IsZero ¶
func (o Obligations) IsZero() bool
IsZero reports whether the Obligations impose no PEP duty at all: no row-scope constraint (zero RowScope) and an empty FieldMask. A coarse route-gate PEP that cannot discharge obligations uses this to fail closed on any non-zero obligation rather than silently drop it (see runtime/auth.RequirePermission, F5).
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().
type Permission ¶
type Permission struct {
// contains filtered or unexported fields
}
Permission is a sealed, closed-set authorization action identifier.
A Permission names WHAT a request wants to do (e.g. "audit:read"), decoupled from WHO may do it (a role). It is the value a route declares via auth.RequirePermission(...) and the value carried as the `action` into a PDP (auth.Authorizer.Authorize). Keeping it a distinct type — not a bare string — is the point: a role-name string literal (e.g. "admin") cannot be passed where a Permission is expected, so the #914 migration cannot regress into a role-literal gate by accident.
Sealed construction (closed value set) ¶
The single field `s` is unexported and the only minter is the unexported newPermission, called solely from the package-level var declarations below. Outside this package there is NO way to construct a Permission: authz.Permission{s: "anything"} is a compile error (unexported field), and there is no exported constructor or Unmarshal. The set of Permissions that can ever exist is therefore exactly the exported Perm* vars in this file — a closed, audited registry. The zero value (Permission{}) is invalid; IsZero() reports it and downstream consumers fail closed on it.
AI-robust Grade ¶
Hard — "string-typed concept funnel" + "sealed construction" from the ai-robust.md Hard 范本目录. Upstream: newPermission is the sole minter, reachable only through this file's registry. Downstream: auth.RequirePermission is the sole route consumer. A new permission cannot be introduced without adding a perm* var + accessor here (which the registry test pins), so the action vocabulary cannot drift via scattered string literals.
Permissions are exposed as ACCESSOR FUNCTIONS (e.g. PermAuditRead()), never as exported vars. An exported var would be reassignable from outside (authz.PermAuditRead = Permission{} could zero the global registry value — the F6 gap); a function declaration cannot be reassigned, so the registry value is immutable to every external package. This makes the "sealed value" claim true by the type system (compile error to reassign), not merely by convention.
Growth: PR-10a seeds only PermAuditRead (the #914 / auditquery target). PR-10b adds one perm* var + accessor per migrated endpoint; the closed set grows only here.
func PermAuditRead ¶
func PermAuditRead() Permission
PermAuditRead returns the permission authorizing reading the audit ledger across actors within the caller's tenant (the gate auditquery's "query other actors" branch consults). Migrated from the role-literal auth.AnyRole(RoleAdmin, RoleSuperAdmin) gate in #914 (PR-10a).
It is an accessor function (not an exported var) so the registry value is immutable to external packages — reassigning a func is a compile error.
func PermConfigPublish ¶
func PermConfigPublish() Permission
PermConfigPublish authorizes the config publish/rollback lifecycle (configpublish slice). A domain verb distinct from CRUD write, mirroring the dedicated publish endpoints' own admin gate.
func PermConfigRead ¶
func PermConfigRead() Permission
PermConfigRead authorizes reading configuration entries (configread slice: GET config/{key}, GET config). Migrated from auth.AnyRole(RoleAdmin) in PR-10b.
func PermConfigWrite ¶
func PermConfigWrite() Permission
PermConfigWrite authorizes mutating configuration entries (configwrite slice: create/update/delete). "write" folds create+update+delete per the AWS-IAM Write access-level grouping the prior uniform admin gate already implied.
func PermFlagRead ¶
func PermFlagRead() Permission
PermFlagRead authorizes reading/evaluating feature flags (featureflag slice: GET flag/{key}, GET flags, POST evaluate). Evaluate is a read-only computation.
func PermFlagWrite ¶
func PermFlagWrite() Permission
PermFlagWrite authorizes mutating feature flags (flagwrite slice: create/update/toggle/delete).
func PermPolicyRead ¶
func PermPolicyRead() Permission
PermPolicyRead authorizes reading ABAC policies (policymanage slice: GET policies/{id}, GET policies). Migrated from auth.AnyRole(RoleAdmin) in PR-10c.
func PermPolicyWrite ¶
func PermPolicyWrite() Permission
PermPolicyWrite authorizes mutating ABAC policies (policymanage slice: create/update/delete). Migrated from auth.AnyRole(RoleAdmin) in PR-10c.
func PermRoleRead ¶
func PermRoleRead() Permission
PermRoleRead authorizes reading a user's role assignments (rbaccheck slice: GET roles/{userID}, GET roles/{userID}/{roleName}). The gate self-exempts a subject reading its OWN roles via auth.RequirePermissionOrSelf. Migrated from auth.SelfOr("userID", RoleAdmin) in PR-10c.
func PermUserRead ¶
func PermUserRead() Permission
PermUserRead authorizes reading a user account (identitymanage slice: GET users/{id}). The gate self-exempts a subject reading its OWN user via auth.RequirePermissionOrSelf; non-self reads require this permission. Migrated from auth.SelfOr("id", RoleAdmin) in PR-10c.
func PermUserWrite ¶
func PermUserWrite() Permission
PermUserWrite authorizes mutating a user account (identitymanage slice: create/update/patch/delete/lock/unlock/change-password). "write" folds the account-management verbs the prior admin/self gates already grouped; the self-exempt verbs (update/patch/change-password) self-exempt at the gate. Migrated from auth.AnyRole(RoleAdmin) / auth.SelfOr("id", RoleAdmin) in PR-10c.
func Permissions ¶
func Permissions() []Permission
Permissions returns a copy of the closed Permission registry. Intended for tests and conformance enumeration; the returned slice is independent so a caller cannot mutate the registry.
func (Permission) IsZero ¶
func (p Permission) IsZero() bool
IsZero reports whether p is the invalid zero value (no minter ran). Consumers that receive a Permission from an untyped path should reject IsZero() to fail closed.
func (Permission) String ¶
func (p Permission) String() string
String returns the action spelling carried into a PDP and stored in policy Action targets (e.g. "audit:read"). The zero value returns "" — never a valid action — so a forged/zero Permission cannot match a real policy rule.