Help us shape the future of Go! Take the 2026 Go Developer Survey

principal

package
v1.9.0 Latest Latest
Warning

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

Go to latest
Published: Nov 2, 2026 License: MIT Imports: 6 Imported by: 0

Documentation

Overview

Package principal defines the caller abstraction shared by human users, AI agents and workloads. It holds value types only: no store access, no HTTP, no engine. Everything above it in the import graph (session, store, middleware, plugin) can depend on this package without cycles.

Index

Constants

This section is empty.

Variables

View Source
var ErrNotFound = errors.New("principal: not found")

ErrNotFound is returned when a principal or delegation does not exist.

Functions

func NewActorsContext

func NewActorsContext(ctx context.Context, c Chain) context.Context

NewActorsContext returns ctx carrying the actor chain.

func NewContext

func NewContext(ctx context.Context, p *Principal) context.Context

NewContext returns ctx carrying p as the resolved caller.

Types

type AuthAttempt

type AuthAttempt struct {
	Subject Ref
	Actors  Chain

	AppID id.AppID
	EnvID id.EnvironmentID
	OrgID id.OrgID

	// CredentialKind is how the caller authenticated: "api_key",
	// "token_exchange" or "jwt".
	CredentialKind string
	// CredentialID identifies the specific credential, so a verdict can be
	// cached against it and a compromised one can be traced.
	CredentialID string

	IPAddress string
	UserAgent string

	// Ephemeral is true when the subject was minted by another principal
	// rather than registered.
	Ephemeral bool

	At time.Time
}

AuthAttempt describes a credential being turned into a session.

It is the payload the risk plugins receive for machine callers. Sign-in carries an account.SignInRequest, which has an email and a password on it and means nothing for an agent. This is the machine-side equivalent: enough for a risk contributor to score, with nothing on it that only a human has.

type Chain

type Chain []Ref

Chain is an actor chain, ordered nearest-caller-first.

It follows RFC 8693. The session's subject is who the request is for, and the chain is who is doing the acting on their behalf. Element 0 is the immediate caller. A chain of length two is an ephemeral child acting through its registered parent. An empty chain means the subject is calling directly, which is the ordinary sign-in case.

func ActorsFromContext

func ActorsFromContext(ctx context.Context) (Chain, bool)

ActorsFromContext returns the actor chain, if the caller is acting for somebody else.

func (Chain) Actor

func (c Chain) Actor() (Ref, bool)

Actor returns the immediate caller.

func (Chain) Contains

func (c Chain) Contains(r Ref) bool

Contains reports whether r appears anywhere in the chain.

func (Chain) Depth

func (c Chain) Depth() int

Depth returns how many actors stand between the caller and the subject.

func (Chain) Prepend

func (c Chain) Prepend(r Ref) Chain

Prepend returns a chain with r as the new immediate caller. The receiver is not modified, so a chain read off a session can be extended without the extension leaking back into the session.

func (Chain) Root

func (c Chain) Root() (Ref, bool)

Root returns the outermost hop, the principal furthest from the subject.

type Delegation

type Delegation struct {
	ID    id.DelegationID `json:"id"`
	AppID id.AppID        `json:"app_id"`
	OrgID id.OrgID        `json:"org_id,omitempty"`

	Actor   Ref `json:"actor"`
	Subject Ref `json:"subject"`

	GrantKind GrantKind `json:"grant_kind"`
	// Scopes narrows what the actor may do while acting. An empty list places
	// no restriction of its own, leaving the actor's own scopes as the only
	// limit.
	Scopes []string `json:"scopes,omitempty"`
	// GrantedBy is who consented. For a delegation that is normally the
	// subject; for an impersonation it is the admin who initiated it.
	GrantedBy Ref `json:"granted_by"`

	ExpiresAt *time.Time `json:"expires_at,omitempty"`
	RevokedAt *time.Time `json:"revoked_at,omitempty"`
	CreatedAt time.Time  `json:"created_at"`
	UpdatedAt time.Time  `json:"updated_at"`
}

Delegation records that Actor may act on behalf of Subject.

It is the durable, revocable, auditable half of the actor chain. The chain on a session says who is acting. The grant says they were allowed to.

func (*Delegation) AllowsScope

func (d *Delegation) AllowsScope(scope string) bool

AllowsScope reports whether scope falls inside d's scope filter.

func (*Delegation) IsActive

func (d *Delegation) IsActive(at time.Time) bool

IsActive reports whether d may be exercised at time at.

type DelegationQuery

type DelegationQuery struct {
	AppID      id.AppID
	OrgID      id.OrgID
	Actor      *Ref
	Subject    *Ref
	GrantKind  GrantKind
	ActiveOnly bool
	ActiveAsOf time.Time
	Limit      int
}

DelegationQuery filters a delegation listing.

type GrantKind

type GrantKind string

GrantKind distinguishes the two ways one principal comes to act for another.

const (
	// GrantDelegation is an agent or workload acting for a user who granted
	// it that authority. Both parties are checked, so the decision narrows.
	GrantDelegation GrantKind = "delegation"
	// GrantImpersonation is an admin acting as a user. The actor is not
	// independently checked, because impersonating somebody is precisely the
	// request to evaluate as them. The gate sits on the Impersonate call.
	GrantImpersonation GrantKind = "impersonation"
)

type Kind

type Kind string

Kind is the sort of caller a principal is.

const (
	// KindUser is a human with an account.
	KindUser Kind = "user"
	// KindAgent is an AI agent or MCP client acting under a registration.
	KindAgent Kind = "agent"
	// KindWorkload is a machine caller with no human behind it: a CI job, a
	// cron, a service calling another service.
	KindWorkload Kind = "workload"
	// KindService is the kind sessions written before agents and workloads
	// existed carry. Retained so those rows keep resolving.
	KindService Kind = "service_account"
)

func (Kind) IsHuman

func (k Kind) IsHuman() bool

IsHuman reports whether k denotes a person.

func (Kind) Valid

func (k Kind) Valid() bool

Valid reports whether k is a kind this package knows.

type Principal

type Principal struct {
	Ref

	AppID  id.AppID
	OrgID  id.OrgID
	EnvID  id.EnvironmentID
	Name   string
	Scopes []string
	Roles  []string

	// Owner is the principal answerable for this one. Nil for users, who are
	// answerable for themselves.
	Owner *Ref
	// Parent is the registered principal that minted this one. Set only on
	// ephemeral children.
	Parent *Ref
	// ExpiresAt is a hard cutoff. Nil means durable.
	ExpiresAt *time.Time
	Disabled  bool
}

Principal is a resolved caller carrying everything an authorization decision needs, so callers do not go back to the store mid-decision.

func FromContext

func FromContext(ctx context.Context) (*Principal, bool)

FromContext returns the resolved caller.

This lives here rather than only in middleware so a plugin can read the caller without importing middleware, which would pull in forge and the whole HTTP surface.

func (*Principal) IsActive

func (p *Principal) IsActive(at time.Time) bool

IsActive reports whether p may authenticate at time at.

func (*Principal) IsEphemeral

func (p *Principal) IsEphemeral() bool

IsEphemeral reports whether p was minted by another principal.

func (*Principal) IsExpired

func (p *Principal) IsExpired(at time.Time) bool

IsExpired reports whether p has passed its cutoff at time at.

type Query

type Query struct {
	AppID      id.AppID
	Kind       Kind
	OwnerUser  *id.UserID
	Parent     *Ref
	ActiveOnly bool
	ActiveAsOf time.Time
	Limit      int
}

Query filters a principal listing.

type Ref

type Ref struct {
	Kind Kind   `json:"kind"`
	ID   string `json:"id"`
}

Ref is the addressable identity of a principal. It is comparable, so it can be a map key and can be compared with ==, and it is cheap enough to put on a context and to serialize into a token claim.

func ParseRef

func ParseRef(s string) (Ref, error)

ParseRef parses the "kind:id" form produced by Ref.String.

It splits on the first colon only. A TypeID does not contain one today, but refs are compared to make authorization decisions, and splitting on the last colon would let an id with a colon in it address a different principal than the one that was written.

func UserRef

func UserRef(userID id.UserID) Ref

UserRef builds a ref for a human user.

func (Ref) IsZero

func (r Ref) IsZero() bool

IsZero reports whether r addresses nothing.

func (Ref) String

func (r Ref) String() string

String renders the ref as "kind:id".

type Store

type Store interface {
	// GetPrincipal resolves any principal by ref. Users are assembled from
	// the user table; every other kind comes from the service accounts table.
	GetPrincipal(ctx context.Context, ref Ref) (*Principal, error)
	// ListPrincipals returns principals matching q.
	ListPrincipals(ctx context.Context, q *Query) ([]*Principal, error)

	// CreateDelegation stores a new grant.
	CreateDelegation(ctx context.Context, d *Delegation) error
	// GetDelegation returns a grant by ID.
	GetDelegation(ctx context.Context, delID id.DelegationID) (*Delegation, error)
	// FindActiveDelegation returns the live grant letting actor act for
	// subject under grantKind, or ErrNotFound.
	FindActiveDelegation(ctx context.Context, appID id.AppID, actor, subject Ref, grantKind GrantKind) (*Delegation, error)
	// ListDelegations returns grants matching q.
	ListDelegations(ctx context.Context, q *DelegationQuery) ([]*Delegation, error)
	// RevokeDelegation marks a grant revoked at the given time. Revoking an
	// already-revoked grant is not an error.
	RevokeDelegation(ctx context.Context, delID id.DelegationID, at time.Time) error
}

Store is the persistence interface for principals and their delegations.

The principal read side reads the same rows the serviceaccount store writes. It is separate because callers resolving a caller mid-request want a Principal and not a ServiceAccount, and because a user principal is assembled from a different table entirely.

Jump to

Keyboard shortcuts

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