Documentation
¶
Overview ¶
Package authz is CCF's central authorization layer. It defines the engine-neutral PDP (Policy Decision Point) contract that the PEP middleware calls, a driver registry that mirrors database/sql, a loader for the authorization manifest, and a builtin driver that reproduces CCF's pre-authz access rules with zero behavior change.
See the "CCF Pluggable Authorization — Design Plan" (BCH-1313, Phase 1). CCF declares the vocabulary, the PEP supplies facts, and the configured PDP decides; CCF never stores or evaluates policies beyond what the builtin driver needs.
Index ¶
- Constants
- Variables
- func CompileRolePolicies(m *Manifest) (*cedar.PolicySet, error)
- func Drivers() []string
- func ReconcileConfigRoleAssignments(ctx context.Context, db *gorm.DB, path string, logger *zap.SugaredLogger) error
- func Register(name string, factory Factory)
- type AuthZen
- type Builtin
- type Cedar
- type ContextAttr
- type Decision
- type Deps
- type EvalRequest
- type ExportFormat
- type Factory
- type FailMode
- type GroupResolver
- type Healther
- type Manifest
- type Options
- type PDP
- type Resource
- type ResourceDef
- type RoleAssignments
- type RoleResolver
- type Subject
- type SubjectDef
Constants ¶
const ( // Platform / admin (archetype F). ResourceAdmin = "admin" ResourceUser = "user" ResourceAgent = "agent" ResourceNotification = "notification" ResourceRiskTemplate = "risk-template" ResourceSubjectTemplate = "subject-template" ResourceDigest = "digest" ResourceAIDiagnostics = "ai-diagnostics" ResourceImport = "import" // Telemetry / ingest (archetype C). ResourceEvidence = "evidence" ResourceHeartbeat = "heartbeat" // OSCAL authoring documents (archetype A). ResourceCatalog = "catalog" ResourceProfile = "profile" ResourceComponentDefinition = "component-definition" ResourceSSP = "ssp" ResourceAssessmentPlan = "assessment-plan" ResourceAssessmentResults = "assessment-results" ResourcePoamOSCAL = "poam_oscal" ResourceInventory = "inventory" ResourceParty = "party" ResourceRole = "role" ResourceActivity = "activity" // SSP-scoped register items (archetype B). ResourceRisk = "risk" ResourcePoamItem = "poam_item" // Dashboard / config (archetype E). ResourceFilter = "filter" ResourceDashboardSuggestion = "dashboard-suggestion" // Workflow engine (archetype D). ResourceWorkflowDefinition = "workflow-definition" ResourceWorkflowStepDefinition = "workflow-step-definition" ResourceWorkflowInstance = "workflow-instance" ResourceWorkflowExecution = "workflow-execution" ResourceStepExecution = "step-execution" ResourceRoleAssignment = "role-assignment" ResourceControlRelationship = "control-relationship" // Actions. read/create/update/delete are the CRUD verbs; the rest are resource-specific // (promote → risk; ingest → heartbeat/agent; register → agent; trigger → digest; // execute → import). ActionManage is the admin umbrella. ActionManage = "manage" ActionRead = "read" ActionCreate = "create" ActionUpdate = "update" ActionDelete = "delete" ActionPromote = "promote" ActionIngest = "ingest" ActionExecute = "execute" ActionTrigger = "trigger" )
Resource and action identifiers, all declared in manifest.yaml. These are the strings the PEP hands the PDP; they must match the manifest (and the cedar entity mapping in cedar.go) EXACTLY — the hyphen/underscore split is load-bearing (poam_item/poam_oscal use underscores, OSCAL document resources use hyphens). ActionManage is the umbrella admin action: every admin route enforces it uniformly, while the manifest also declares the granular admin.* actions for later per-route enforcement.
const DefaultAgentRole = "agent"
DefaultAgentRole is the role granted to every authenticated agent when the role-assignment file does not say otherwise. It is the manifest's service role (evidence:create, heartbeat:ingest, agent:register/ingest), so agents can ingest with zero configuration — matching the prior agent-ingest behavior the embedded engine replaces.
const DefaultRoleCacheTTL = 10 * time.Second
DefaultRoleCacheTTL is the lifetime of a cached role resolution. It is short so a grant or revocation takes effect within a few seconds across the fleet, mirroring the timeliness the group resolver buys by not baking groups into the JWT (BCH-1333, BCH-1319 §7).
const DriverAuthzen = "authzen"
DriverAuthzen is the name of the remote AuthZen HTTP driver: it turns any AuthZen-compliant PDP (Topaz, Axiomatics, PlainID, …) into CCF's decision engine by changing one config key. See the design plan §3.3 (Phase 3).
const DriverBuiltin = "builtin"
DriverBuiltin is the name of the default, in-process driver.
const DriverCedar = "cedar"
DriverCedar is the name of the embedded Cedar engine: the OSS built-in PDP (cedar-go, in-process). It honors the manifest's bundled global-RBAC roles (C0) and is the shared evaluation core the Enterprise external Cedar PDP also builds on (BCH-1319 §11; BCH-1316).
It is registered but NOT yet the default — `builtin` stays the default so this change is zero-behavior. Selecting it (authz.driver: cedar) opts a deployment into real RBAC, where access requires a role grant (deny-by-default). Making cedar the default is the migration tracked in BCH-1330.
const SubjectGroupsAttr = "groups"
SubjectGroupsAttr is the subject-attribute key under which resolved group memberships are exported into the evaluation tuple (Subject.Props). The value is native-only: SSO IdP groups reach authz only by first being materialized into native ccf_user_groups memberships at login (BCH-1331), so authorization is always evaluated against CCF's own group taxonomy (BCH-1319 §7, BCH-1328).
Variables ¶
ErrUnavailable marks a PDP that could not produce a decision because the decision engine was unreachable or timed out. The PEP applies the configured fail mode when an Evaluate error wraps ErrUnavailable; any other error is treated as an internal server error (HTTP 500). The builtin driver runs in-process and never returns ErrUnavailable.
var SupportedExportFormats = []ExportFormat{ExportJSON, ExportYAML, ExportCedar, ExportOpenFGA}
SupportedExportFormats lists the formats Export accepts, for CLI help and validation.
Functions ¶
func CompileRolePolicies ¶
CompileRolePolicies translates the manifest's roles: block into the bundled OSS Cedar policy set — the C0 global-RBAC policies the embedded engine is designed to honor (BCH-1319 §11.1, §11.3). The manifest roles: block is the single source of truth: each role→resource→actions grant becomes one permit policy whose scope references only the subject's role membership, the action, and the resource type (C0 — no attribute conditions, so the C1/C2 resolvers never fire for these policies, BCH-1319 §11.4).
Operators extend this set via their own .cedar files (the GitOps escape hatch, §11.2); because Cedar is deny-by-default with forbid overriding permit, an operator file can both add grants and carve exceptions out of the bundled roles without editing CCF.
func ReconcileConfigRoleAssignments ¶
func ReconcileConfigRoleAssignments(ctx context.Context, db *gorm.DB, path string, logger *zap.SugaredLogger) error
ReconcileConfigRoleAssignments makes the persisted ccf_role_assignments table reflect the user/group grants declared in the role-assignment config file (authz-roles.yaml), marking them source=config (BCH-1334). With BCH-1333 the table is the PDP's source of truth, so the file stops being read per request and is instead a boot seed reconciled into the table here.
Reconcile, don't duplicate:
- a config grant that already matches is left untouched (no write);
- a grant the file no longer declares is deleted (source=config rows only);
- a manual row whose (type,id,role) the file now declares is adopted as config — precedence: config ownership wins for an identical grant, so the admin API's 409-on-config-delete protects it and the file/API never fight over the same row;
- source=manual rows the file does not declare are never touched.
It is idempotent: re-running the same file produces zero writes and no duplicate rows. A missing file is "no config grants" — every source=config row is removed — so deleting the file revokes its grants on the next boot. A malformed file, or a role the manifest does not declare, is a hard error that blocks startup (the caller treats a migration error as fatal), preserving the fail-fast validate() behaviour the cedar loader had. It runs for every driver so the table, and the admin UI built on it, reflect config regardless of which engine enforces it.
Types ¶
type AuthZen ¶
type AuthZen struct {
// contains filtered or unexported fields
}
AuthZen is a PDP that delegates decisions to a remote AuthZen Authorization API. It supplies facts only (the PEP and handlers build the tuple) and holds no policy logic. It is safe for concurrent use (the underlying http.Client is).
func NewAuthZen ¶
func NewAuthZen(endpoint string, logger *zap.SugaredLogger) (*AuthZen, error)
NewAuthZen constructs the driver from the PDP's single-evaluation URL. The batch URL is derived by AuthZen convention; an empty or malformed endpoint is a startup error.
func (*AuthZen) Evaluate ¶
func (a *AuthZen) Evaluate(ctx context.Context, s Subject, action string, r Resource, reqCtx map[string]any) (Decision, error)
Evaluate implements PDP via the AuthZen single Access Evaluation API.
func (*AuthZen) Evaluations ¶
Evaluations implements PDP via the AuthZen batch Access Evaluations API — one HTTP call for the whole batch (list filtering / UI permission hints), not N calls.
type Builtin ¶
type Builtin struct {
// contains filtered or unexported fields
}
Builtin is the default, in-process PDP. It reproduces CCF's pre-authz access rules with zero behavior change: admin resources require SSO admin-group membership (password users are treated as super admins), and every other resource is allowed once the request is authenticated — the authn middleware having already enforced authentication and any public-endpoint policy before the PEP runs.
In Phase 1 the builtin driver resolves SSO facts itself (it holds db + config), acting as its own PIP, so behavior matches the prior admin-group middleware exactly. The "PEP supplies all facts" model the design describes is for the remote drivers and the manifest attribute surface designed in BCH-1319. Because it runs in-process it never returns ErrUnavailable, so the configured fail mode never changes its behavior.
func NewBuiltin ¶
NewBuiltin constructs the builtin PDP. A nil logger is replaced with a no-op logger.
func (*Builtin) Evaluate ¶
func (b *Builtin) Evaluate(ctx context.Context, s Subject, _ string, r Resource, _ map[string]any) (Decision, error)
Evaluate implements PDP.
func (*Builtin) Evaluations ¶
Evaluations implements PDP by evaluating each request independently, in order. Admin decisions are memoized per subject for the batch: evaluateAdmin ignores the action and keys only on the subject, so a batch enumerating several admin.* actions (e.g. /me/permissions) would otherwise repeat the same user + SSO-link DB lookups once per action. The memo keeps the facts inside the builtin driver and preserves both ordering and the error path.
type Cedar ¶
type Cedar struct {
// contains filtered or unexported fields
}
Cedar is the embedded Cedar PDP. It evaluates the bundled role policies (compiled from the manifest) plus any operator .cedar files against an in-process entity store it builds per request from the subject's resolved roles. The policy set is read-only after construction and each Evaluate builds its own entity store, so it is safe for concurrent use. Roles come from a RoleResolver: the DB-backed resolver (the persisted ccf_role_assignments source of truth, BCH-1333) reads CCF's database, so unlike Phase 1 the engine can now return an error when the role source is unreachable — the PEP's fail mode decides what that means.
func NewCedar ¶
func NewCedar(policies *cedar.PolicySet, roles RoleResolver, logger *zap.SugaredLogger) *Cedar
NewCedar constructs the embedded Cedar PDP from an already-compiled policy set and a role resolver. A nil logger is replaced with a no-op; a nil resolver defaults to agents-only (the agent service role), which denies every user — callers should pass a loaded resolver. The factory does the file IO (manifest compile, operator policy dir, role assignment file) and wires the DB-backed resolver; this constructor stays pure for testing. *RoleAssignments satisfies RoleResolver, so file-only tests can pass static assignments directly.
func (*Cedar) Evaluate ¶
func (c *Cedar) Evaluate(ctx context.Context, s Subject, action string, r Resource, _ map[string]any) (Decision, error)
Evaluate implements PDP. It resolves the subject's roles, then asks Cedar. A subject with no role is denied without consulting Cedar (no bundled permit could match it anyway), which also keeps anonymous and unassigned principals from building an entity store needlessly. A role-source error is returned to the caller so the PEP fail mode (not a silent allow/deny) governs an unreachable DB.
func (*Cedar) Evaluations ¶
Evaluations implements PDP by deciding each request in order. Role resolution is memoized per subject for the batch (e.g. /me/permissions enumerates ~all actions for one subject), so the group parsing and map lookups happen once rather than per action.
type ContextAttr ¶
type ContextAttr struct {
Type string `yaml:"type" json:"type"`
Relationship string `yaml:"relationship,omitempty" json:"relationship,omitempty"`
Status string `yaml:"status,omitempty" json:"status,omitempty"`
Note string `yaml:"note,omitempty" json:"note,omitempty"`
}
ContextAttr declares a relationship attribute exposed in the evaluation tuple's context rather than as a static subject or resource prop. These are (subject × resource) relationship attributes (e.g. oscal_roles, assigned_to) the PEP cannot read from a single row; a PIP resolves them lazily when a policy references them (BCH-1319 §7.1, §8). They are declared in the public contract now but reserved — no resolver or policy consumes them yet — so they are kept out of the static Attributes map until then.
type Decision ¶
Decision is the PDP's verdict. Reason is for logging only and is never echoed to clients (see the PEP).
type Deps ¶
Deps are the runtime dependencies handed to a driver factory. The builtin driver uses all three; remote drivers (later phases) may ignore the DB.
type EvalRequest ¶
EvalRequest is one entry in a batch evaluation (AuthZen "evaluations"), used for list filtering and UI permission hints.
type ExportFormat ¶
type ExportFormat string
ExportFormat is a target the manifest vocabulary can be rendered into for GitOps pipelines and engines without an admin API (parent design §3.4). All formats export the VOCABULARY (subjects, resources, actions, attributes, roles) — not operator policies.
const ( // ExportJSON renders the manifest as indented JSON. ExportJSON ExportFormat = "json" // ExportYAML renders the manifest as YAML (a normalized round-trip of the source). ExportYAML ExportFormat = "yaml" // ExportCedar renders a Cedar schema (entity + action shapes) for the embedded/external // Cedar engines. uuid maps to String and map<string,string> to Set<String>, since Cedar // has no native equivalents. ExportCedar ExportFormat = "cedar" // ExportOpenFGA renders an OpenFGA authorization model (a type per resource with a // relation per action, directly assignable to the subject types). ExportOpenFGA ExportFormat = "openfga" )
func ParseExportFormat ¶
func ParseExportFormat(s string) (ExportFormat, error)
ParseExportFormat maps a (case-insensitive) string to an ExportFormat.
type FailMode ¶
type FailMode string
FailMode controls what the PEP does when the PDP is unavailable.
func ParseFailMode ¶
ParseFailMode maps a config string to a FailMode, defaulting to FailClosed for any value other than "open".
type GroupResolver ¶
type GroupResolver interface {
// ResolveGroups returns the native CCF group names the subject belongs to. SSO IdP groups are
// NOT read here: they enter authz only once the login sync has materialized them as native
// memberships through an SSOGroupMapping (BCH-1331). Non-user subjects resolve to no groups.
ResolveGroups(ctx context.Context, s Subject) ([]string, error)
}
GroupResolver is the in-process Policy Information Point (PIP) for the subject.groups attribute. It is OSS and reads CCF's database directly — an external PDP cannot, so it only ever receives already-resolved values (BCH-1319 §8, §11.5). Implementations must be safe for concurrent use.
func NewDBGroupResolver ¶
func NewDBGroupResolver(db *gorm.DB, logger *zap.SugaredLogger) GroupResolver
NewDBGroupResolver constructs the DB-backed group PIP. A nil db disables resolution (ResolveGroups returns no groups); a nil logger becomes a no-op.
type Healther ¶
Healther is an optional capability a PDP may implement to report whether its decision engine is reachable. The readiness check surfaces it; in-process drivers that cannot fail (builtin) simply don't implement it and are treated as healthy.
type Manifest ¶
type Manifest struct {
SchemaVersion int `yaml:"schemaVersion" json:"schemaVersion"`
Subjects map[string]SubjectDef `yaml:"subjects" json:"subjects,omitempty"`
Resources map[string]ResourceDef `yaml:"resources" json:"resources"`
Roles map[string]map[string][]string `yaml:"roles" json:"roles,omitempty"`
}
Manifest is CCF's engine-neutral authorization vocabulary: the subjects, resources, actions and suggested default roles that operator policies are written against. It is treated as a public contract — renaming an action or removing an attribute is a breaking change for every customer policy set.
func DefaultManifest ¶
DefaultManifest returns the manifest embedded in the binary, parsed once and cached. The embedded copy is the source of truth for the builtin driver; operators targeting external engines export it via later-phase tooling (ccf authz export). It lives inside the package (rather than a repo-root authz/ path) because go:embed cannot reach outside the package directory and embedding removes any runtime working-directory dependency.
func LoadManifest ¶
LoadManifest reads and parses a manifest from disk.
func ParseManifest ¶
ParseManifest unmarshals and validates a manifest document.
type Options ¶
Options selects and configures the driver to open. Endpoint is the remote PDP URL used by HTTP drivers (authzen); CacheTTL, when > 0, wraps the constructed PDP in a short-TTL decision cache to absorb the network hop. Both are ignored by the in-process builtin.
type PDP ¶
type PDP interface {
// Evaluate returns the decision for a single (subject, action, resource) tuple.
Evaluate(ctx context.Context, s Subject, action string, r Resource, reqCtx map[string]any) (Decision, error)
// Evaluations returns decisions for a batch of requests, in the same order.
Evaluations(ctx context.Context, reqs []EvalRequest) ([]Decision, error)
}
PDP is the pluggable decision engine. Implementations must be safe for concurrent use.
type ResourceDef ¶
type ResourceDef struct {
Actions []string `yaml:"actions" json:"actions"`
Attributes map[string]string `yaml:"attributes,omitempty" json:"attributes,omitempty"`
Context map[string]ContextAttr `yaml:"context,omitempty" json:"context,omitempty"`
}
ResourceDef declares a resource's actions and the attributes it exports. Attributes are the C0/C1/C2 static props the PEP supplies directly (a request param or one resource-row load); Context holds relationship attributes that don't fit a static prop and are resolved lazily by a PIP (BCH-1319 §7.1, §8).
type RoleAssignments ¶
type RoleAssignments struct {
Users map[string]string `yaml:"users"`
Groups map[string]string `yaml:"groups"`
Agents string `yaml:"agents"`
Anonymous string `yaml:"anonymous"`
}
RoleAssignments is the OSS static role-assignment configuration (BCH-1319 §11.3): a GitOps-friendly mapping of principals to one of the bundled roles, with no UI and no DB. A subject's effective roles are the union of its direct user grant and a grant for each group it belongs to (plus the agent default for agent subjects), so a subject can hold several roles even though each principal/group maps to a single role.
- Users — by user: `alice@example.com → auditor`. Keyed by the subject's email (subject.id), matched case-insensitively. Works for every user today.
- Groups — by group: `sec-team → admin`. Keyed by group name. Live only once the subject carries a groups attribute (native CCF groups ∪ SSO groups), which is BCH-1328's surface; until then this map is inert (no subject has groups), exactly the documented "group-based assignment requires BCH-1328" dependency. Structurally ready so it goes live additively when BCH-1328 lands — no change here.
- Agents — the role granted to authenticated agents (default DefaultAgentRole).
- Anonymous — the role granted to unauthenticated requests (no default; empty = deny). Use with care: `anonymous: viewer` makes all read routes open without a login. This is an explicit opt-in; omitting the field keeps the deny-by-default behaviour.
func LoadRoleAssignments ¶
func LoadRoleAssignments(path string) (*RoleAssignments, error)
LoadRoleAssignments reads and parses a role-assignment file. A missing file is returned as an os.IsNotExist error so the caller can treat the optional file as "no static assignments" (deny-by-default for users, agent default still applies); a malformed file is a hard error so a typo fails fast at startup rather than silently denying everyone.
type RoleResolver ¶
type RoleResolver interface {
// RolesFor returns the subject's effective roles: the union of its direct grant and a grant
// for each group it belongs to (plus the agent/anonymous defaults for non-user subjects).
// The result is sorted and de-duplicated; an empty result is deny-by-default. An error means
// the source could not be read — the caller (PDP) surfaces it so the PEP fail mode decides.
RolesFor(ctx context.Context, s Subject) ([]string, error)
}
RoleResolver resolves a subject to its effective manifest roles. It is the engine-neutral seam the PDP reads roles through, so the source can be the persisted ccf_role_assignments table (the BCH-1333 source of truth) without the engine knowing. Implementations must be safe for concurrent use.
func NewDBRoleResolver ¶
func NewDBRoleResolver(db *gorm.DB, defaults *RoleAssignments, ttl time.Duration, logger *zap.SugaredLogger) RoleResolver
NewDBRoleResolver constructs the DB-backed role resolver. defaults supplies the agent/anonymous roles (the table holds only user/group grants); a nil defaults becomes the agent service role only. A nil db returns the static defaults directly (no DB to read), so callers can wire it unconditionally. A ttl <= 0 disables caching (every lookup hits the DB).
type Subject ¶
Subject is the actor a decision is made about — an authenticated user or agent (or an anonymous subject on public-allowed routes). Props carries provisional, minimal attributes in Phase 1; the authoritative per-resource attribute surface is designed separately in BCH-1319 and must not be grown ad hoc here.
type SubjectDef ¶
type SubjectDef struct {
Attributes map[string]string `yaml:"attributes" json:"attributes,omitempty"`
}
SubjectDef declares the attributes a subject type exports into the evaluation tuple.