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 ¶
var ErrNotFound = errors.New("principal: not found")
ErrNotFound is returned when a principal or delegation does not exist.
Functions ¶
func NewActorsContext ¶
NewActorsContext returns ctx carrying the actor chain.
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 ¶
ActorsFromContext returns the actor chain, if the caller is acting for somebody else.
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.
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" )
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 ¶
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) IsEphemeral ¶
IsEphemeral reports whether p was minted by another principal.
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 ¶
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 ¶
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.
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.