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.
The reference server separately accepts DM_ADMIN_TOKEN as a root credential that bypasses policy, including when a principal store is configured. Its use is audited as break-glass; removal requires unsetting it and restarting. Use it to bootstrap principals, then verify their access and remove it. This package does not implement administrative users, passwords, sessions or federation; applications can supply another authorizer.
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)
- Apple documents nothing about administering an MDM server; the device-facing protocol is elsewhere. The prior art is catalogued in https://github.com/deploymenttheory/go-apple-dm/blob/main/docs/research/reference_projects.md and read in record 0034.
- RFC 6750: bearer token usage, including the WWW-Authenticate challenge
- RFC 9110 section 11: the 401 and 403 distinction the API relies on
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 RoleUID(name string) types.EntityUID
- func Valid(t Token) bool
- func ValidName(name string) bool
- func Validate(reg *Registry, doc Policy) error
- type Action
- 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) 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) GetPolicy(ctx context.Context, actor Principal, name string) (Policy, 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) Registry() *Registry
- func (m *Manager) Revoke(ctx context.Context, actor Principal, name string) 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)
- type Option
- type Page
- type Policy
- type PolicySet
- type Principal
- type PrincipalChange
- type Registry
- type Result
- 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" // EntityDEPAccount is one device enrollment service account as a resource. EntityDEPAccount types.EntityType = "MDM::DEPAccount" )
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 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.
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
}
Action describes an operation from the route registry. Policy validation rejects references outside this registry.
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
// 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) 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) 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) Revoke ¶
Revoke clears a principal's token, leaving the principal in place so its name still resolves in audit history.
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
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. It returns a compiled set only when both checks succeed. Action validation uses the stable API without the experimental schema package.
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.
type Store ¶
type Store interface {
// 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.
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. |