identity

package
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 13, 2026 License: MPL-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package identity holds service principals and their credentials.

A principal is modelled separately from its credentials so it can later carry an OIDC subject binding instead of, or alongside, a secret — swapping how a token is minted without touching anything that consumes one (ADR-0004).

Index

Constants

View Source
const (
	SystemScannerPrincipalID       = "system:scanner"
	SystemBagDropPrincipalID       = "system:bagdrop"
	SystemPluginPrunerPrincipalID  = "system:plugin-pruner"
	SystemPluginBundlerPrincipalID = "system:plugin-bundler"
)
View Source
const DummySecretHash = "$argon2id$v=19$m=19456,t=2,p=1$XlTPhjxTv+KIT+AriTHNpg$gPa3WcRFECTrrH3qzoICZ7jHlTnQju0Tt5CJicDsZcM"

DummySecretHash carries the current verification parameters for timing equalisation. Authentication padding and the repository's client-id miss path must share this source so a parameter change cannot make their costs diverge.

View Source
const MaxVerificationMemoryBytes = argonMemory * 1024 * maxActiveSecrets

MaxVerificationMemoryBytes is the most memory one authentication attempt can spend, so a caller admitting requests can size a budget against it.

Multiplied by maxActiveSecrets because Authenticate always performs exactly that many verifications, padding missing usable secrets with DummySecretHash.

View Source
const TokenAudience = "https://api.hashicorp.cloud"

Variables

View Source
var (
	ErrInvalid  = errors.New("invalid")
	ErrConflict = errors.New("conflict")
	ErrNotFound = errors.New("not found")
	// ErrRootPermanence marks the refusals that keep a root principal holding
	// at least one usable, never-expiring secret — without one, expiry locks
	// the instance out on a timer (duf-2rw). Distinguished so the platform
	// plane can answer it with its own message rather than the cap's.
	ErrRootPermanence = errors.New("a root principal must hold a usable secret that never expires")
	// ErrIntegrity reports an identity row failing keyring MAC verification on
	// an encrypted deployment (ADR-0024): the row was not written by this
	// application. Audited distinctly; answered indistinguishably on the wire.
	ErrIntegrity = errors.New("identity integrity verification failed")
)
View Source
var ErrSharesRejected = errors.New("recovery shares rejected")

ErrSharesRejected reports shares that decoded and recombined but did not reproduce the stored verifier — the caller does not hold custody.

Functions

func NewRecoveryShares

func NewRecoveryShares(shareCount, threshold int) (shares []string, digest []byte, err error)

NewRecoveryShares mints the recovery secret and splits it into shareCount shares, any threshold of which prove custody to /sys/recovery.

The shares appear here and nowhere else. What the caller stores is the digest: the secret is 256-bit random material, not a password, so a plain digest is the correct verifier — memory-hard hashing defends low-entropy inputs (ADR-0024).

func VerifyRecoveryShares

func VerifyRecoveryShares(shares []string, threshold int, digest []byte) error

VerifyRecoveryShares recombines the presented shares and checks the result against the stored digest. Malformed or duplicated shares and too few of them wrap ErrInvalid; shares that recombine to the wrong secret return ErrSharesRejected.

func WithActor

func WithActor(ctx context.Context, principalID, name string) context.Context

Types

type Actor

type Actor struct {
	PrincipalID string
	Name        string
}

func ActorFromContext

func ActorFromContext(ctx context.Context) (Actor, bool)

type AuditOperation

type AuditOperation string

AuditOperation names the security-relevant action being attempted.

const (
	AuditOperationTokenIssue         AuditOperation = "token.issue"
	AuditOperationProjectList        AuditOperation = "project.list"
	AuditOperationProjectRead        AuditOperation = "project.read"
	AuditOperationInstanceInitialize AuditOperation = "instance.initialize"
	AuditOperationInstanceRecover    AuditOperation = "instance.recover"

	// The console's cookie session (duf-1cn): a credential materializing into
	// or leaving the browser's cookie jar is a lifecycle event like issuance.
	AuditOperationSessionCreate AuditOperation = "session.create"
	AuditOperationSessionRenew  AuditOperation = "session.renew"
	AuditOperationSessionDelete AuditOperation = "session.delete"

	// The principal and credential lifecycle. ADR-0019 makes audit a
	// PRECONDITION of the role model rather than a follow-up: permitting a human
	// to hold root is justified by attribution, and that justification is only as
	// good as the trail. Granting and revoking authority are the highest-value
	// entries in it.
	AuditOperationPrincipalList AuditOperation = "principal.list"
	AuditOperationPrincipalRead AuditOperation = "principal.read"

	AuditOperationPrincipalCreate AuditOperation = "principal.create"
	AuditOperationPrincipalDelete AuditOperation = "principal.delete"
	AuditOperationSecretIssue     AuditOperation = "secret.issue"
	AuditOperationSecretRevoke    AuditOperation = "secret.revoke"

	AuditOperationScanExecute   AuditOperation = "scan.execute"
	AuditOperationScanRetention AuditOperation = "scan.retention"

	// A scheduled prune of finished plugin jobs; the same operation names a
	// maintainer's Prune now through the API.
	AuditOperationPluginImportPrune         AuditOperation = "plugin.import.prune"
	AuditOperationPluginRegistryBundleBuild AuditOperation = "plugin_registry.bundle.build"
	// An uploaded bundle read into the registry by the bundle importer.
	AuditOperationPluginRegistryBundleIngest AuditOperation = "plugin_registry.bundle.ingest"
)

type AuditOutcome

type AuditOutcome string

AuditOutcome distinguishes completed actions from refusals and system failures.

const (
	AuditOutcomeSuccess AuditOutcome = "success"
	AuditOutcomeRefused AuditOutcome = "refused"
	AuditOutcomeFailure AuditOutcome = "failure"
)

type AuditScope

type AuditScope string

AuditScope identifies where an operation was attempted.

const (
	AuditScopePlatform     AuditScope = "platform"
	AuditScopeOrganization AuditScope = "organization"
	AuditScopeProject      AuditScope = "project"
)

type AuditTarget

type AuditTarget struct {
	ID        string
	Path      string
	CreatedAt time.Time
}

AuditTarget is one file the broker writes every audit event to.

Slot is deliberately absent. The database assigns 1..3 to make a fourth target unwriteable (the baseline's three-slot constraint), but that is storage enforcing a limit, not a property of the target — exposing it would invite a client to choose one, and the ordering it implies means nothing to a caller.

type AuthorizationVerdict

type AuthorizationVerdict int

AuthorizationVerdict answers the whole authorization question while preserving why it was refused.

Authority and tenancy must never be checkable separately. Refusals still need to distinguish a tenancy denial, which conceals existence, from a role denial within a tenancy the caller may see. The verdict carries both truths without separating the checks.

const (
	// AuthorizationAllowed means both tenancy and role checks passed.
	AuthorizationAllowed AuthorizationVerdict = iota
	// AuthorizationDeniedTenancy conceals a tenancy outside the principal's scope.
	AuthorizationDeniedTenancy
	// AuthorizationDeniedRole means the tenancy is visible but the role is insufficient.
	AuthorizationDeniedRole
)

type BasicAuthIssuer

type BasicAuthIssuer struct {
	// contains filtered or unexported fields
}

BasicAuthIssuer issues tokens after authenticating a Principal secret.

func NewBasicAuthIssuer

func NewBasicAuthIssuer(issuer string, signingKey []byte, ttl time.Duration) (*BasicAuthIssuer, error)

func NewKeyringAuthIssuer

func NewKeyringAuthIssuer(issuer string, keys func() [][]byte, ttl time.Duration) (*BasicAuthIssuer, error)

NewKeyringAuthIssuer reads the current keyring on every issue and verify so rotation takes effect without rebuilding the HTTP planes.

func (*BasicAuthIssuer) Issue

func (i *BasicAuthIssuer) Issue(principal *Principal, credential string) (string, string, error)

func (*BasicAuthIssuer) Reissue

func (i *BasicAuthIssuer) Reissue(principal *Principal, secretID string, authTime time.Time) (string, error)

func (*BasicAuthIssuer) TTL

func (i *BasicAuthIssuer) TTL() time.Duration

TTL is the lifetime of tokens this issuer mints.

Exposed so the token endpoint can report expires_in from the same value that produced the token's exp claim, rather than holding a second copy that could drift.

func (*BasicAuthIssuer) Verify

func (i *BasicAuthIssuer) Verify(signed string) (Verified, error)

func (*BasicAuthIssuer) VerifyExpired

func (i *BasicAuthIssuer) VerifyExpired(signed string) (Verified, error)

VerifyExpired performs the same signature and claim checks as Verify while tolerating an expired exp claim. It exists only for the cookie-session renewal path; bearer middleware must continue to call Verify.

type Claims

type Claims struct {
	jwt.RegisteredClaims
	AuthTime *jwt.NumericDate `json:"auth_time,omitempty"`

	OrganizationID string `json:"organization_id,omitempty"`
	ProjectID      string `json:"project_id,omitempty"`
	BucketID       string `json:"bucket_id,omitempty"`
	// SecretID is the credential this token was minted from. Not a secret
	// itself — an opaque identifier — and it is what makes revocation take
	// effect immediately rather than at expiry.
	SecretID string `json:"sid,omitempty"`
	// Scope is a deliberately reserved seam for authorization bindings
	// (duf-vgn). It is emitted as an empty array until bindings exist, and
	// token tests assert that emptiness.
	Scope []string `json:"scope"`
	// Grants is a deliberately reserved seam for authorization bindings
	// (duf-vgn). It is emitted as an empty array until bindings exist, and
	// token tests assert that emptiness.
	Grants []Grant `json:"grants"`
}

Claims is the JWT contract shared by every issuance path.

type Grant

type Grant struct {
	Resource string   `json:"resource"`
	Actions  []string `json:"actions"`
}

Grant is a resource-scoped set of actions carried by a token.

type IdentityKind

type IdentityKind string

IdentityKind says how the actor proved, or failed to prove, its identity.

const (
	IdentityKindAnonymous        IdentityKind = "anonymous"
	IdentityKindServicePrincipal IdentityKind = "service_principal"
	IdentityKindSystem           IdentityKind = "system"
	IdentityKindUnknown          IdentityKind = "unknown"
)

type Principal

type Principal struct {
	ID        string
	Name      string
	ClientID  string
	Scope     Scope
	Role      Role
	CreatedAt time.Time
	// contains filtered or unexported fields
}

Principal is a service identity: a client id, a scope, a role, and its secrets.

func NewPrincipal

func NewPrincipal(
	id, name, clientID string, scope Scope, role Role, at time.Time,
) (*Principal, error)

NewPrincipal creates a principal holding NO secrets.

Creation and issuance are separate actions (duf-4ac): a principal that exists but cannot yet authenticate is an ordinary state, reached deliberately from the console and left behind whenever a non-root principal's last secret is revoked (ADR-0004, amended 2026-08-02). Call IssueSecret to give it a credential.

func RestorePrincipal

func RestorePrincipal(
	id, name, clientID string, scope Scope, role Role, createdAt time.Time, secrets []Secret,
) (*Principal, error)

RestorePrincipal rebuilds a principal from storage.

func (*Principal) Authenticate

func (p *Principal) Authenticate(plaintext string, now time.Time) (string, bool)

Authenticate reports whether plaintext matches any active secret.

Every usable slot is checked even after a match, and missing slots are padded with a dummy verification, so the work does not reveal which one succeeded or whether the principal holds zero, one, or two usable secrets. Returns WHICH secret matched, so a token can name the credential that minted it and stop working when that credential is revoked (review finding 14).

Expired secrets are skipped entirely rather than checked-then-rejected, then their missing usable slots are padded. Presenting a correct-but-expired secret therefore takes the same verification path as garbage. Exactly maxActiveSecrets verifications also stays within MaxVerificationMemoryBytes, whose admission budget already assumes that work (duf-2rw).

func (*Principal) Authorize

func (p *Principal) Authorize(
	required Role, organizationID, projectID uuid.UUID,
) AuthorizationVerdict

Authorize reports whether this principal may perform an action requiring a role on a project or organization tenancy.

func (*Principal) AuthorizeVisibility

func (p *Principal) AuthorizeVisibility(
	required Role, organizationID uuid.UUID,
) AuthorizationVerdict

AuthorizeVisibility reports whether this principal may read an organization or a filtered collection within it. A project binding is visible within its containing organization without granting organization-level authority.

func (*Principal) HasActiveSecret

func (p *Principal) HasActiveSecret(id string, now time.Time) bool

HasActiveSecret reports whether a secret is still current on this principal.

ADR-0019 resolves authority per request so revocation is immediate; expiry gets the same immediacy — a token minted from a secret that has since expired stops working now, not at the token's own expiry.

func (*Principal) IssueSecret

func (p *Principal) IssueSecret(secretID string, expiresAt *time.Time, at time.Time) (string, error)

IssueSecret mints an additional secret, returning the plaintext exactly once.

expiresAt is nil for a credential that never expires. The cap counts USABLE secrets: an expired one grants nothing, so requiring its revocation before a replacement can be issued would add a step at exactly the moment someone is fixing an outage (duf-2rw).

func (*Principal) RevokeSecret

func (p *Principal) RevokeSecret(secretID string, now time.Time) error

RevokeSecret removes a secret, refusing to remove a root's last one.

A principal with no secrets cannot authenticate and cannot mint a replacement FOR ITSELF — but it does not have to. A maintainer issues a replacement for any tenancy-scoped principal, so leaving one secretless is an ordinary, recoverable state.

Root is the exception, and the only one (ADR-0004, amended 2026-08-02). Nothing sits above it to re-issue on its behalf, so a root left secretless can only be recovered by direct database access — the same failure the first-run wizard warns about.

Expiry extends the same rule (duf-2rw): a root left holding only EXPIRING secrets is the same lockout on a timer, so a root must always keep at least one usable, never-expiring secret.

func (*Principal) Secrets

func (p *Principal) Secrets() []Secret

Secrets exposes secret metadata. The plaintext is not recoverable from these.

type Role

type Role string

Role is what an identity may do within a scope it is entitled to (ADR-0019).

The tiers are strictly nested, which is what lets "no role more permissive than your own" be an ordering rather than a comparison of capability sets.

const (
	// RoleReader sees buckets, versions, builds and unrestricted channels.
	RoleReader Role = "reader"
	// RoleBuilder additionally creates buckets, versions and builds, and consumes
	// restricted channels. This is what a Packer CLI principal holds — the
	// smallest role that completes a build.
	RoleBuilder Role = "builder"
	// RolePublisher additionally assigns channels. Separate from builder because
	// declaring a channel is asking to promote.
	RolePublisher Role = "publisher"
	// RoleMaintainer additionally manages restricted channels, principals and role
	// bindings within its scope. Named to avoid OpenShift's meaning of "operator".
	RoleMaintainer Role = "maintainer"
	// RoleRoot may do anything, including creating organisations and configuring
	// authentication and audit. Platform scope only.
	RoleRoot Role = "root"
)

func ParseRole

func ParseRole(value string) (Role, error)

ParseRole converts external input, refusing anything unrecognised.

func (Role) AtLeast

func (r Role) AtLeast(required Role) bool

AtLeast reports whether this role includes the authority of another.

An unrecognised role on either side answers false: a value that is not a role cannot satisfy a requirement, and cannot be satisfied by one (ADR-0017).

func (Role) MayGrant

func (r Role) MayGrant(granted Role) bool

MayGrant reports whether a holder of this role may grant another.

No identity may grant a role more permissive than its own — the ordinary escalation defence. Equality is permitted: a maintainer may appoint another maintainer.

This answers the LEVEL question only. The caller must separately establish that the grantor holds this role IN THE SCOPE being granted, because holding maintainer in one organisation confers nothing in another (ADR-0019).

func (Role) MayModifyHolderOf

func (r Role) MayModifyHolderOf(theirs Role) bool

MayModifyHolderOf reports whether a holder of this role may modify or delete an identity holding another.

MayGrant alone stops upward granting but not downward attack: an identity able to delete a holder of a higher role has escalated by removal, since taking the higher tier out leaves it at the top. Same rationale, separate rule.

func (Role) PlatformOnly

func (r Role) PlatformOnly() bool

PlatformOnly reports whether a role may only be held at platform scope.

type Scope

type Scope struct {
	OrganizationID uuid.UUID
	ProjectID      uuid.UUID
	BucketID       string
}

Scope is the tenancy a principal may act within.

Project is optional: with an organization set, the zero project means organization-scoped, seeing every project in that organization. Bucket is optional too; until bucket-aware enforcement lands, it records a narrower binding without changing the project-level authorization predicates. Both project and organization scope exist because the Packer CLI distinguishes them — a project-scoped principal gets 403 from ProjectService_List, which the CLI turns into a set-HCP_PROJECT_ID message, while an organization-scoped one seeing several projects makes it warn and select the oldest (ADR-0016).

The zero value is the MOST privileged one

Scope{} is platform scope: above every tenancy, and the store answers it with every organisation and every principal on the instance. That is a deny-by- default inversion, so it carries a rule — never construct a Scope literally and hand it to anything that authorizes. Pass a scope that came from a restored Principal, whose construction enforced the binding (duf-ueq).

func (Scope) OrganizationScoped

func (s Scope) OrganizationScoped() bool

OrganizationScoped reports whether this scope spans every project in its organization.

func (Scope) Permits

func (s Scope) Permits(organizationID, projectID uuid.UUID) bool

Permits reports whether the principal may act on the named tenancy.

This is the authorization primitive the compatibility plane needs: it derives its tenant from the request path, so the path must be checked against the caller rather than trusted (ADR-0017). Deny by default — an unset scope permits nothing.

func (Scope) PermitsOrganization

func (s Scope) PermitsOrganization(organizationID uuid.UUID) bool

PermitsOrganization reports whether this scope may act at ORGANISATION level — on the organisation itself, or on the set of its projects.

Distinct from Permits, which asks about a specific project and therefore requires one. An operation like "create a project in this organisation" has no project yet, so Permits would refuse it for want of an argument that cannot exist.

func (Scope) PlatformScoped

func (s Scope) PlatformScoped() bool

PlatformScoped reports whether this scope sits above every tenancy.

Only `root` may be held here (ADR-0019). Permits deliberately still answers false for a platform scope: it asks whether a caller is entitled to a TENANCY, and a platform identity is not entitled to one — it outranks the question. Principal.May handles that, so the tenancy check keeps a single meaning.

func (Scope) WithinOrganization

func (s Scope) WithinOrganization(organizationID uuid.UUID) bool

WithinOrganization reports whether this scope is bound to the organization, either directly or through one of its projects.

This is a membership and visibility question only. It does not grant authority to act at organization level; PermitsOrganization remains the predicate for that stricter question.

type Secret

type Secret struct {
	ID         string
	CreatedAt  time.Time
	LastUsedAt *time.Time
	// ExpiresAt, when set, is the moment this credential stops granting
	// anything. Expiry lives on the SECRET, not the principal — that is what
	// makes overlap possible: the outgoing secret expires soon, the incoming
	// one later (duf-2rw).
	ExpiresAt *time.Time
	// contains filtered or unexported fields
}

Secret is one credential belonging to a principal.

The plaintext exists only in the response that issues it. What is retained is an argon2id hash and metadata — nothing here can reconstruct the secret.

func RestoreSecret

func RestoreSecret(id, encoded string, createdAt time.Time, lastUsedAt, expiresAt *time.Time) (Secret, error)

RestoreSecret rebuilds a Secret from storage.

func (Secret) Encoded

func (s Secret) Encoded() string

Encoded exposes the stored hash for persistence.

func (Secret) Usable

func (s Secret) Usable(now time.Time) bool

Usable reports whether this credential still grants anything. An expired secret stays stored and visible, but it authenticates nothing and does not count against the cap.

type TokenIssuer

type TokenIssuer interface {
	Issue(principal *Principal, credential string) (token string, secretID string, err error)
	Reissue(principal *Principal, secretID string, authTime time.Time) (string, error)
	Verify(token string) (Verified, error)
	VerifyExpired(token string) (Verified, error)
}

TokenIssuer authenticates a credential, issues a token, and verifies tokens carrying the resulting principal scope.

type Verified

type Verified struct {
	PrincipalID string
	Scope       Scope
	// SecretID names the credential that minted this token, so a caller
	// resolving the principal can refuse a token whose credential has since
	// been revoked (review finding 14).
	SecretID  string
	AuthTime  time.Time
	ExpiresAt time.Time
}

Verified is the authenticated identity a token carries.

The principal is returned alongside the scope because authorizing a request and recording who made it are different needs, and a not-found answer at a tenancy boundary must carry enough server-side detail to diagnose (ADR-0017). Scope alone cannot say which principal acted.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL