Documentation
¶
Overview ¶
Package adminauth authenticates administrative principals and evaluates Cedar authorization policies.
Design ¶
Principals have roles and root authority; API tokens are checksummed, stored as digests, and revocable. Route actions and request context are evaluated under default-deny Cedar policies. Root administration, credential-issuance restrictions and last-root protection are enforced outside policy grants. Only root may mutate principals or credentials, including self-rotation; role subsets do not establish equivalent authority under arbitrary Cedar. Store.ApplyPrincipal atomically preserves an active root under concurrent credential changes. Natural expiry and policy lockout remain operator concerns.
Managed roles are named records; membership alone grants no authority. Policies are schema-validated and inactive documents remain stored. Any invalid active policy or evaluation diagnostic denies operational access. Denial on diagnostics is project policy: Cedar itself skips policies whose evaluation fails, so another matching permit could otherwise allow a request. The reference server exchanges DM_BOOTSTRAP_TOKEN for the first stored root credential exactly once. Root can repair authorization independently of Cedar; ordinary device operations always need an explicit policy grant. This package does not implement users, passwords, sessions or federation.
References ¶
- Decision record: https://github.com/deploymenttheory/go-apple-dm/blob/main/docs/research/decisions/0034-admin-api-and-authorization.md
- Decision record: https://github.com/deploymenttheory/go-apple-dm/blob/main/docs/research/decisions/0035-dmctl-structure-and-credentials.md
- Threat model: https://github.com/deploymenttheory/go-apple-dm/blob/main/docs/security/threat-model.md (admin API, repudiation)
- E2E scenarios: https://github.com/deploymenttheory/go-apple-dm/blob/main/docs/testing/e2e-scenarios.md (E2E-024)
- Administrative authorization is project policy; decision record 0034 cites the implementation references that inform it.
- Cedar authorization: https://docs.cedarpolicy.com/auth/authorization.html
- Unified RBAC: https://github.com/deploymenttheory/go-apple-dm/blob/main/docs/research/decisions/0056-unified-server-rbac.md
- RFC 6750 (bearer tokens): https://www.rfc-editor.org/rfc/rfc6750
- RFC 9110 section 11 (HTTP authentication): https://www.rfc-editor.org/rfc/rfc9110#section-11
Index ¶
- Constants
- Variables
- func ActionUID(id string) types.EntityUID
- func Digest(t Token) string
- func ParseRoles(s string) ([]string, error)
- func PrincipalUID(name string) types.EntityUID
- func Redact(t Token) string
- func References(source string, entityType string) ([]string, error)
- func RoleUID(name string) types.EntityUID
- func Valid(t Token) bool
- func ValidName(name string) bool
- func Validate(reg *Registry, doc Policy) error
- func ValidateReferences(ctx context.Context, store Store, p Policy) error
- type Action
- type ContextAttribute
- type Decision
- type Manager
- func (m *Manager) Authenticate(ctx context.Context, t Token) (Principal, error)
- func (m *Manager) Authorize(ctx context.Context, p Principal, action string, resource types.EntityUID, ...) (Decision, error)
- func (m *Manager) Bootstrap(ctx context.Context, name string, expires time.Time) (Principal, Token, error)
- func (m *Manager) CreatePrincipal(ctx context.Context, actor Principal, p Principal, expires time.Time) (Principal, Token, error)
- func (m *Manager) DeletePolicy(ctx context.Context, actor Principal, name string) error
- func (m *Manager) DeletePrincipal(ctx context.Context, actor Principal, name string) error
- func (m *Manager) DeleteRole(ctx context.Context, actor Principal, name string) error
- func (m *Manager) GetPolicy(ctx context.Context, actor Principal, name string) (Policy, error)
- func (m *Manager) Initialized(ctx context.Context) (bool, error)
- func (m *Manager) Policies(ctx context.Context, actor Principal) ([]Policy, error)
- func (m *Manager) Principal(ctx context.Context, name string) (Principal, error)
- func (m *Manager) Principals(ctx context.Context, p Page) (Result[Principal], error)
- func (m *Manager) PutPolicy(ctx context.Context, actor Principal, doc Policy) (Policy, error)
- func (m *Manager) PutRole(ctx context.Context, actor Principal, role Role) (Role, error)
- func (m *Manager) Registry() *Registry
- func (m *Manager) Revoke(ctx context.Context, actor Principal, name string) error
- func (m *Manager) Role(ctx context.Context, name string) (Role, error)
- func (m *Manager) Roles(ctx context.Context, page Page) (Result[Role], error)
- func (m *Manager) Rotate(ctx context.Context, actor Principal, name string, expires time.Time) (Principal, Token, error)
- func (m *Manager) UpdatePrincipal(ctx context.Context, actor Principal, name string, roles []string, root bool) (Principal, error)
- func (m *Manager) ValidatePolicy(ctx context.Context, p Policy) error
- func (m *Manager) Version(ctx context.Context) (int64, error)
- type Option
- type Page
- type Policy
- type PolicySet
- type Principal
- type PrincipalChange
- type Registry
- type Result
- type Role
- type Store
- type Token
Constants ¶
const ( // EntityPrincipal is an admin caller, `MDM::Principal::"<name>"`. EntityPrincipal types.EntityType = "MDM::Principal" // EntityRole is a named group a principal belongs to, // `MDM::Role::"<name>"`, expressed as a Cedar entity parent so a policy // can say `principal in MDM::Role::"reader"`. EntityRole types.EntityType = "MDM::Role" // EntityAction is an operation a route declares, `MDM::Action::"<id>"`. EntityAction types.EntityType = "MDM::Action" // EntitySystem is the resource for routes that act on the deployment // rather than on one object: `MDM::System::"any"`. EntitySystem types.EntityType = "MDM::System" // EntityEnrollment is one enrollment as a resource. EntityEnrollment types.EntityType = "MDM::Enrollment" // EntityDeclaration is one declaration as a resource. EntityDeclaration types.EntityType = "MDM::Declaration" // EntityBlueprint is locally authored desired management state. EntityBlueprint types.EntityType = "MDM::Blueprint" // EntityConfigurationProfile is an immutable uploaded configuration profile. EntityConfigurationProfile types.EntityType = "MDM::ConfigurationProfile" // EntityDEPAccount is one device enrollment service account as a resource. EntityDEPAccount types.EntityType = "MDM::DEPAccount" EntitySet types.EntityType = "MDM::DeclarationSet" )
Cedar entity types. Every admin authorization decision is a Cedar request over these: a principal (which may be a member of roles), an action, and a resource. Namespacing them under MDM keeps action ids unique, which Cedar requires globally.
const ( // Prefix precedes every admin token. Prefix = "mdmt_" // TokenLen is the total length of a well-formed token. TokenLen = len(Prefix) + bodyLen + checksumLen )
Token format: a type prefix, a random body and a checksum.
mdmt_<30 base62 characters><6 base62 checksum characters>
The prefix supports secret scanning. The checksum rejects malformed values before storage lookup. The random body provides approximately 178 bits of entropy; storage holds its digest rather than the bearer value. See decision 0034.
const DefaultPageSize = 100
DefaultPageSize applies when Page.Limit is not positive.
Variables ¶
var ( // ErrNotFound is an unknown principal or policy, or a token matching none. ErrNotFound = errors.New("adminauth: not found") // ErrConflict is a name that already exists. ErrConflict = errors.New("adminauth: conflict") // ErrInvalid is a malformed name, token, or policy. ErrInvalid = errors.New("adminauth: invalid") // ErrRevoked is a principal whose token has been revoked. ErrRevoked = errors.New("adminauth: token revoked") // ErrExpired is a principal whose token has passed its expiry. ErrExpired = errors.New("adminauth: token expired") // ErrDenied is an authorization decision of deny. ErrDenied = errors.New("adminauth: denied") // ErrLastRoot guards the last active root credential against deletion, // demotion, revocation or immediate expiry. Natural expiry still requires // operational rotation before all root credentials expire. ErrLastRoot = errors.New("adminauth: last root principal") // ErrEscalation is an attempt to issue a credential for a principal whose // authority the caller does not already hold. ErrEscalation = errors.New("adminauth: cannot issue credentials for a more privileged principal") // ErrUnknownAction indicates a policy reference to an action absent from the // route registry. This additional validation follows Cedar syntax parsing. ErrUnknownAction = errors.New("adminauth: unknown action") )
Errors this package and its stores return.
var Root = Principal{Name: "root", Root: true}
Root is the implicit actor for bootstrap, before any principal exists.
var SystemResource = types.NewEntityUID(EntitySystem, "any")
SystemResource is the resource for deployment-wide routes.
Functions ¶
func Digest ¶
Digest is the SHA-256 hex digest a store persists in place of the token. The token is high-entropy random, so a plain hash is the right primitive; a password KDF would only add cost.
func ParseRoles ¶
ParseRoles validates a comma-separated role list, sorted and deduplicated.
func PrincipalUID ¶
PrincipalUID is the Cedar entity for a principal name.
func Redact ¶
Redact renders a token for a log line or an error: the prefix, the first four body characters, and an ellipsis. Never log a whole token.
func References ¶ added in v0.10.0
References extracts literal entity references from Cedar's parsed JSON form. String values and comments cannot be mistaken for entity references.
func Valid ¶
Valid reports whether t is well formed: the right prefix, the right length, only alphabet characters, and a checksum that matches the body. It says nothing about whether the token was ever issued.
Checking this before a store lookup means a malformed value never becomes a database round trip, which is the point of carrying a checksum at all.
func ValidName ¶
ValidName reports whether a principal or role name is usable: 1 to 64 characters of letters, digits, hyphen, underscore, or dot. Names appear in policies and audit lines, so they stay boring on purpose, and the character set keeps them safe to render inside a Cedar entity literal.
Types ¶
type Action ¶
type Action struct {
// ID is the Cedar action id, `MDM::Action::"<ID>"`.
ID string
// Help is operator-facing prose naming the consequence, shown by
// `dmctl policy actions`. Zentral writes the same for its secret-reveal
// actions, and it is the difference between an operator granting an
// action knowingly and granting it by name.
Help string
// Resource is the entity type this action acts on, EntitySystem when the
// action is deployment-wide.
Resource types.EntityType
Group string
Groups []string
Context map[string]ContextAttribute
Sensitive bool
RootOnly bool
}
Action describes an operation from the route registry. Policy validation rejects references outside this registry.
type ContextAttribute ¶ added in v0.10.0
ContextAttribute describes a server-derived Cedar context field.
type Decision ¶
type Decision struct {
Allowed bool
// Policy is the document and statement that decided, empty when nothing
// matched and the default deny applied.
Policy string
Policies []string
Version int64
// Errors are per-policy evaluation errors. A policy that errors neither
// permits nor forbids, so these are surfaced rather than swallowed.
Errors []string
}
Decision is the outcome of an authorization check, carrying the policy that decided so an audit line can name it.
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager authenticates admin callers, answers authorization decisions from the stored Cedar policies, and administers principals and policies.
Principal, credential and policy mutations require Principal.Root independently of Cedar. Callers authenticate the actor and authorize the route action before invoking these methods. A role-subset comparison cannot bound arbitrary Cedar authority. Stores atomically protect the last active root from removal, demotion, revocation and immediate expiry; later expiry and policy lockout remain operational responsibilities.
func (*Manager) Authenticate ¶
Authenticate resolves a plaintext token to its principal.
A malformed token is rejected on its checksum before any query runs, so a scanner spraying the endpoint costs no database round trips. The principal carries TokenID, so an audit line names the credential that acted without naming the secret.
func (*Manager) Authorize ¶
func (m *Manager) Authorize(ctx context.Context, p Principal, action string, resource types.EntityUID, reqCtx map[string]types.Value) (Decision, error)
Authorize answers one request against the current policies, recompiling them when the store's policy version has moved.
The default is deny: an unknown action, an empty policy set, and a policy that errors all produce a denial rather than an allow.
func (*Manager) Bootstrap ¶ added in v0.10.0
func (m *Manager) Bootstrap(ctx context.Context, name string, expires time.Time) (Principal, Token, error)
Bootstrap creates the first root exactly once, with no operational grants.
func (*Manager) CreatePrincipal ¶
func (m *Manager) CreatePrincipal(ctx context.Context, actor Principal, p Principal, expires time.Time) (Principal, Token, error)
CreatePrincipal adds a principal and mints its first token.
actor must be Root. The caller additionally authorizes its administrative action before entering this manager.
func (*Manager) DeletePolicy ¶
DeletePolicy removes a policy document.
func (*Manager) DeletePrincipal ¶
DeletePrincipal removes a principal.
func (*Manager) DeleteRole ¶ added in v0.10.0
DeleteRole requires root authority and removes an unreferenced role. Principal membership or a policy reference returns ErrConflict; an absent role returns ErrNotFound.
func (*Manager) Initialized ¶ added in v0.10.0
Initialized reports whether principal initialization has permanently consumed the bootstrap opportunity.
func (*Manager) Principals ¶
Principals pages principals by name.
func (*Manager) PutPolicy ¶
PutPolicy parses and validates the policy, rejects unknown action references, and stores it only after validation succeeds.
func (*Manager) PutRole ¶ added in v0.10.0
PutRole requires root authority and creates or updates a named role, preserving its creation time and advancing the policy version. Role membership alone grants no operational permission.
func (*Manager) Revoke ¶
Revoke clears a principal's token, leaving the principal in place so its name still resolves in audit history.
func (*Manager) Role ¶ added in v0.10.0
Role returns the named managed role, or ErrNotFound when no such role exists.
func (*Manager) Roles ¶ added in v0.10.0
Roles returns a page of managed roles in name order using an exclusive cursor.
func (*Manager) Rotate ¶
func (m *Manager) Rotate(ctx context.Context, actor Principal, name string, expires time.Time) (Principal, Token, error)
Rotate mints a replacement token, invalidating the previous one at once.
func (*Manager) UpdatePrincipal ¶
func (m *Manager) UpdatePrincipal(ctx context.Context, actor Principal, name string, roles []string, root bool) (Principal, error)
UpdatePrincipal replaces a principal's roles and root flag.
func (*Manager) ValidatePolicy ¶ added in v0.10.0
ValidatePolicy checks without persisting or changing the active policy set.
type Policy ¶
type Policy struct {
// Name identifies the document for editing and for audit lines.
Name string
// Source is the Cedar text, stored and served exactly as written so an
// operator sees back what they wrote.
Source string
// Description is operator prose, never interpreted.
Description string
// Active defaults to true when omitted. False retains a policy without evaluating it.
Active *bool `json:",omitempty"`
CreatedAt time.Time
UpdatedAt time.Time
}
Policy is a stored Cedar policy document. One record may hold several statements; the whole document is parsed and validated as a unit.
type PolicySet ¶
type PolicySet struct {
// contains filtered or unexported fields
}
PolicySet is a compiled, immutable set of policies ready to answer decisions. Compile once per policy version and share it across requests.
func Compile ¶
Compile parses policy documents with Cedar and checks referenced actions against the supplied registry and Cedar schema. Every active document must validate before any part of the set can authorize requests.
type Principal ¶
type Principal struct {
Name string
Roles []string
// Root may administer principals and policies. It confers no other
// authority: a root principal still needs a policy to enqueue a command.
Root bool
CreatedAt time.Time
UpdatedAt time.Time
// TokenID names the current credential in audit lines, empty once
// revoked. It is a fragment of the token, never enough to use.
TokenID string
// TokenAt is when the current token was minted (zero when revoked).
TokenAt time.Time
// ExpiresAt is when the current token stops being accepted; zero means no expiry.
ExpiresAt time.Time
}
Principal is an administrative caller authorized by policies naming the principal or its roles. Root is checked outside policy evaluation for privileged administration, so policy grants cannot authorize editing their own authority.
func (Principal) Active ¶
Active reports whether p has a credential that is neither revoked nor expired at now.
func (Principal) Covers ¶
Covers compares role membership and the root flag. It does not compare Cedar authority: policies can grant permissions directly to a named principal or distinguish otherwise identical roles using context and forbid clauses. Credential administration therefore requires root independently of Covers.
type PrincipalChange ¶
type PrincipalChange struct {
// Op is update, rotate, revoke or delete.
Op string
Roles []string
Root bool
Digest, TokenID string
ExpiresAt time.Time
}
PrincipalChange describes a credential mutation committed with the store's last-active-root check. It contains a token digest, never the bearer token.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry is the set of actions the server serves. It is built once from the route table and then read-only, so there is no global mutable state and no init-order dependence.
func NewRegistry ¶
NewRegistry returns a registry over actions, refusing duplicates and malformed ids.
func (*Registry) Schema ¶ added in v0.10.0
func (r *Registry) Schema() json.RawMessage
Schema returns the exact schema used to validate policies and requests.
type Role ¶ added in v0.10.0
Role groups administrative principals. Only Cedar policies grant authority.
type Store ¶
type Store interface {
// PutRole upserts a validated role while preserving creation time and advancing the
// authorization version.
PutRole(context.Context, Role, time.Time) (Role, error)
// Role returns a managed role by name, or ErrNotFound when absent.
Role(context.Context, string) (Role, error)
// Roles returns managed roles in name order using the requested cursor and limit.
Roles(context.Context, Page) (Result[Role], error)
// DeleteRole removes an unreferenced role, returning ErrConflict while principals or
// policies still refer to it.
DeleteRole(context.Context, string) error
// BootstrapPrincipal atomically creates the first active root credential and permanently
// closes bootstrap; an initialized or nonempty store returns ErrConflict.
BootstrapPrincipal(context.Context, Principal, string, time.Time) (Principal, error)
// Initialized reports whether principal initialization has permanently consumed
// the bootstrap opportunity, independently of current credential expiry.
Initialized(context.Context) (bool, error)
// ApplyPrincipal serializes mutations across instances and refuses removal,
// revocation, expiry or demotion of the last active root credential. Older
// low-level write methods below are intended for import/storage tooling;
// authenticated administration must use this method.
ApplyPrincipal(ctx context.Context, name string, change PrincipalChange, now time.Time) (Principal, error)
// CreatePrincipal adds a principal with its first token digest. An
// existing name is ErrConflict.
CreatePrincipal(ctx context.Context, p Principal, digest string, now time.Time) (Principal, error)
// Principal returns one principal by name.
Principal(ctx context.Context, name string) (Principal, error)
// PrincipalByDigest returns the principal whose current token digest
// matches. This is the authentication path and must be one indexed lookup.
PrincipalByDigest(ctx context.Context, digest string) (Principal, error)
// Principals pages principals by name.
Principals(ctx context.Context, p Page) (Result[Principal], error)
// UpdatePrincipal replaces the roles and root flag of a principal.
UpdatePrincipal(ctx context.Context, name string, roles []string, root bool, now time.Time) (Principal, error)
// SetToken replaces the current token digest, invalidating the previous
// one immediately.
SetToken(ctx context.Context, name, digest, tokenID string, expires, now time.Time) (Principal, error)
// RevokeToken clears the current token, leaving the principal in place.
RevokeToken(ctx context.Context, name string, now time.Time) error
// DeletePrincipal removes a principal entirely.
DeletePrincipal(ctx context.Context, name string) error
// CountRoot returns how many principals carry Root, for the anti-lockout
// invariant.
CountRoot(ctx context.Context) (int, error)
// PutPolicy stores or replaces a policy document.
PutPolicy(ctx context.Context, p Policy, now time.Time) (Policy, error)
// GetPolicy returns one policy document by name.
GetPolicy(ctx context.Context, name string) (Policy, error)
// Policies returns every stored policy, ordered by name. The whole set is
// compiled together, so this is not paged.
Policies(ctx context.Context) ([]Policy, error)
// DeletePolicy removes a policy document.
DeletePolicy(ctx context.Context, name string) error
// PolicyVersion changes whenever any policy changes, so a cached
// compilation can tell it is stale without recompiling.
PolicyVersion(ctx context.Context) (int64, error)
}
Store persists principals, the digests of their tokens, and the policy documents that grant them authority. Implementations are contract-tested by adminauth/adminauthtest, so the in-memory and the three SQL backends behave identically.
A store never sees a plaintext token: the caller mints one, hands the store its digest, and shows the value to the operator once.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package adminauthtest defines adminauth.Store contract tests and a failing-store wrapper.
|
Package adminauthtest defines adminauth.Store contract tests and a failing-store wrapper. |
|
Package inmem implements an in-memory adminauth.Store for tests and development.
|
Package inmem implements an in-memory adminauth.Store for tests and development. |
|
Package sqlstore persists administrative principals, token digests and policies in SQL.
|
Package sqlstore persists administrative principals, token digests and policies in SQL. |