Documentation
¶
Overview ¶
Package bodyscope is the Harbor Protocol's single body-identity gate.
The problem it owns ¶
Every Protocol method carries an identity scope in its request body. That scope is caller-supplied, so it is an input, not an authority: the authority is the verified identity the transport established at the request edge (identity.FromVerified). Reconcile is the one place the two meet.
The contract ¶
For a request body scope B and the ctx-verified identity V:
- B's User and Session equal V's unconditionally. No scope claim widens them; a surface that wants a broader read expresses it in a filter field, never by renaming the caller.
- An entirely empty B is backfilled from V. Echoing the verified triple into the body is a client convenience, not a requirement.
- B's Tenant equals V's, EXCEPT on a surface whose registered policy declares that component admin-scoped. There, a differing value is permitted only when the caller holds auth.ScopeAdmin or auth.ScopeConsoleFleet, and every permitted crossing publishes an `audit.admin_scope_used` event naming the ctx-verified actor. Without the claim the request fails with CodeScopeMismatch (or the surface's declared deny code, for a surface whose deny path must not confirm that the named scope exists).
- When ctx carries NO verified identity there is nothing to reconcile against, so the request fails closed with CodeIdentityRequired. Trusting the body in that case would make a transport that forgot its auth middleware indistinguishable from one that ran it.
Why the policy is a registry key, not a parameter ¶
Reconcile takes a Surface, not a Policy value: a caller cannot invent a posture at the call site, only name one the registry already declares. The registry is closed and lockstep-tested — every canonical Protocol request type that carries a body identity scope must join to a registered Surface, so a new surface is either registered or fails `go test`. See registry.go.
Why the audit sink is a parameter ¶
A surface whose policy permits a tenant crossing MUST pass a non-nil Auditor. The permission and the accountability are one argument list apart, so the two cannot drift: a tenant-permissive policy with no audit sink is refused at run time with CodeRuntimeError, the same fail-closed posture the impersonation gate holds. A surface whose policy pins the tenant needs no Auditor and may pass nil.
Index ¶
- Constants
- func Elevated(ctx context.Context) bool
- func ExemptScopeCarriers() map[string]string
- func JoinedRequestTypes() []string
- func Reconcile(ctx context.Context, ref ScopeRef, surface Surface, aud Auditor) (context.Context, *protoerrors.Error)
- func RegisteredPolicies() map[Surface]Policy
- type AdminScopeUsedPayload
- type Auditor
- type BusAuditor
- type ComponentRule
- type Elevation
- type Policy
- type ScopeRef
- type Surface
- type Violation
Constants ¶
const ( // KindUnregisteredRequest — a canonical Protocol request type carries // an identity scope but no surface governs it. KindUnregisteredRequest = "unregistered_request_type" // KindUnregisteredCarrier — a canonical wire type carries an identity // scope, is not a request, and is not a reviewed exempt carrier. KindUnregisteredCarrier = "unregistered_scope_carrier" // KindStaleRegistration — a registry row names a wire type the // canonical packages no longer declare. KindStaleRegistration = "stale_registration" // KindUnknownSurface — a registry row names a surface with no policy. KindUnknownSurface = "unknown_surface" // KindUnusedSurface — a surface declares a policy no request type // joins to. KindUnusedSurface = "unused_surface" // KindHandRolledGate — a body-identity comparison outside the shared // reconciler. KindHandRolledGate = "hand_rolled_body_scope_gate" // KindUnregisteredElevation — a call to a verified-identity or // elevation writer from a site that is not on the reviewed list. KindUnregisteredElevation = "unregistered_elevation_site" )
Violation kinds.
Variables ¶
This section is empty.
Functions ¶
func Elevated ¶
Elevated reports whether ctx carries an audited tenant crossing minted by Reconcile. Surfaces consult it to widen a read that would otherwise be pinned to the verified tenant.
func ExemptScopeCarriers ¶
ExemptScopeCarriers returns the non-request scope-carrying wire types and the reason each is exempt from the join.
func JoinedRequestTypes ¶
func JoinedRequestTypes() []string
JoinedRequestTypes returns every canonical request type with a registered surface, sorted.
func Reconcile ¶
func Reconcile(ctx context.Context, ref ScopeRef, surface Surface, aud Auditor) (context.Context, *protoerrors.Error)
Reconcile is the single body-identity gate. It reconciles ref against the ctx-verified identity under the policy registered for surface, writes the reconciled triple back through ref, and returns the ctx the caller must use for everything downstream.
The returned ctx carries an audited tenant elevation when — and only when — the policy permitted a tenant crossing and aud recorded it. A later Reconcile on the same request (a surface re-running the gate behind its transport) reads that marker and does not re-record the same crossing.
aud MUST be non-nil when the registered policy permits an identity crossing; a nil sink on such a surface is refused with CodeRuntimeError rather than silently accepted.
A nil *protoerrors.Error return means the request is reconciled and may proceed. Any non-nil return is terminal: the caller writes it and does not dispatch.
func RegisteredPolicies ¶
RegisteredPolicies returns a copy of the closed policy table.
Types ¶
type AdminScopeUsedPayload ¶
type AdminScopeUsedPayload struct {
events.SafeSealed
// Actor is the verified identity at the Protocol edge.
Actor identity.Identity
// Surface is the Protocol surface that granted the crossing.
Surface string
// TargetTenant / TargetUser are the identity components the granted
// request reaches. TargetSession is omitted: a crossing is a tenant
// or user reach, and the session is the caller's own.
TargetTenant string
TargetUser string
// Reason names the surface and the entitlement.
Reason string
}
AdminScopeUsedPayload is the typed SafePayload published on the canonical `audit.admin_scope_used` event when the body-scope gate grants an identity crossing.
SafePayload by construction: every field is a bounded identity component, a registry key, or a surface-authored reason string. No caller-supplied bytes reach the bus — the reason is built from the policy's own Wire name, never from the request.
The payload is distinct from the per-subsystem admin payloads (the Tools, Tasks, Agents and impersonation shapes). All ride the same canonical event type; a subscriber type-switches on the payload.
type Auditor ¶
type Auditor interface {
// AdminScopeUsed records one permitted crossing. Implementations
// never fail the request: the crossing has already passed its scope
// check, and an emit failure is logged loudly rather than converted
// into a client error.
AdminScopeUsed(ctx context.Context, e Elevation)
}
Auditor records a permitted identity crossing. Reconcile calls it BEFORE granting the crossing, so a record exists for every request that was allowed to reach outside the verified identity.
A surface whose registered policy permits a crossing MUST be given a non-nil Auditor; Reconcile refuses such a surface with a nil sink rather than granting an unrecorded crossing.
type BusAuditor ¶
type BusAuditor struct {
// contains filtered or unexported fields
}
BusAuditor publishes each granted crossing as an `audit.admin_scope_used` event on the wired event bus. It is a compiled artifact: every field is set at construction and never mutated, so one instance is safe to share across concurrent requests.
func NewBusAuditor ¶
NewBusAuditor builds an Auditor over the supplied bus.
bus may be nil (an embedder that wired no bus): the crossing is then logged at Info instead of published. It is never fully silent — a granted crossing that left no trace anywhere is the failure mode this whole gate exists to close.
redactor may be nil; when supplied, the payload is run through it before publishing and a redaction failure suppresses the publish in favour of a loud log, never an unredacted event.
logger may be nil; slog.Default() is used.
func (*BusAuditor) AdminScopeUsed ¶
func (a *BusAuditor) AdminScopeUsed(ctx context.Context, e Elevation)
AdminScopeUsed implements Auditor.
type ComponentRule ¶
type ComponentRule uint8
ComponentRule declares how one component of a request body's identity triple is reconciled against the ctx-verified identity.
const ( // Pinned — the component MUST equal the verified identity's. An // empty component does not equal a populated verified one, so an // empty component on a partially-populated body is a mismatch. This // is the default and the posture of every component on every surface // that has no cross-identity read. Pinned ComponentRule = iota // PinnedOrEmpty — a populated component MUST equal the verified // identity's; an empty one is a wildcard the surface interprets // downstream (an artifacts list scoped to a whole tenant, a filter // that spans a user's sessions). The wildcard is left empty rather // than backfilled, so the surface's own scoping reads it as written. PinnedOrEmpty // AdminScoped — a component that differs from the verified // identity's is permitted ONLY when the caller holds // auth.ScopeAdmin or auth.ScopeConsoleFleet, and every permitted // divergence is recorded on the audit bus before it is granted. An // empty component is a wildcard, as with PinnedOrEmpty. AdminScoped )
func (ComponentRule) String ¶
func (r ComponentRule) String() string
String renders a ComponentRule for test failures and error detail.
type Elevation ¶
type Elevation struct {
// Surface is the registry key of the surface that granted the
// crossing.
Surface Surface
// Actor is the ctx-verified identity — the triple the transport
// established for the request. It is the audit anchor: the answer to
// "who did this".
Actor identity.Identity
// Target is the identity the reconciled request runs under.
Target identity.Identity
// Reason names the surface and the entitlement that permitted the
// crossing.
Reason string
}
Elevation is the record of one permitted identity crossing: which surface granted it, which verified actor asked for it, which identity the request now runs under, and why.
type Policy ¶
type Policy struct {
// Surface is the registry key.
Surface Surface
// Wire is the operator-facing surface name that appears in the
// Protocol error message. It names the surface, never the caller's
// identity or the tenant it asked for.
Wire string
// Tenant, User, Session declare the per-component reconciliation
// rule. The overwhelmingly common shape is all three Pinned.
Tenant ComponentRule
User ComponentRule
Session ComponentRule
// Grants names the claims that permit a crossing on this surface.
// Empty means the admin-tier pair (`admin` + `console:fleet`) — the
// common case, and the one the fleet-observation claim was minted
// for. A surface whose crossing is strictly an administrative act
// names `admin` alone, so a read-only fleet token cannot take it.
Grants []auth.Scope
// ScopeDeniedCode overrides the Protocol code returned when an
// AdminScoped component diverges and the caller holds no admin-tier
// claim. Empty means CodeScopeMismatch. A surface whose deny path
// must not confirm that the named scope exists sets its own code.
ScopeDeniedCode protoerrors.Code
// PinnedDeniedCode overrides the Protocol code returned when a Pinned
// component diverges — the flat refusal, which no claim reopens.
// Empty means CodeIdentityRequired.
//
// A surface sets this when its flat refusal already has a published
// answer that differs. The gate runs EARLIER than the surface checks
// it replaces, and moving a refusal earlier must not change the code a
// client branches on.
PinnedDeniedCode protoerrors.Code
// Reason documents why the surface holds this posture. It is the
// answer a reviewer needs when a policy row looks surprising, and it
// is the thing that used to live in a copied code comment.
Reason string
}
Policy is one Protocol surface's declared body-identity posture. It is a value in a closed registry, never constructed at a call site: the whole point of the registry is that a surface's posture is declared in one readable table rather than re-derived by whoever writes the next handler.
func PolicyFor ¶
PolicyFor returns the registered Policy for surface and a presence bool. Absence is a construction bug — the registry is closed — so Reconcile turns it into a loud CodeRuntimeError rather than a default.
func (Policy) Granted ¶
Granted reports whether ctx carries a claim this surface accepts for a crossing. Scopes are write-once at the request edge and absent reads as denied, so this is fail-closed by construction.
func (Policy) PermitsCrossIdentity ¶
PermitsCrossIdentity reports whether the policy can grant a request that names an identity component other than the verified one. A policy that can MUST be supplied an Auditor at every Reconcile call site — the permission and the accountability travel together.
type ScopeRef ¶
type ScopeRef interface {
// Triple returns the body's (tenant, user, session).
Triple() (tenant, user, session string)
// SetTriple writes the reconciled (tenant, user, session) back into
// the body so everything downstream reads the reconciled value.
SetTriple(tenant, user, session string)
}
ScopeRef is a mutable handle on a request body's identity triple. Wire bodies carry the triple in more than one shape (types.IdentityScope on most requests, types.ArtifactScope on the artifacts cluster), so Reconcile reads and writes through this handle rather than switching on the concrete request type.
Adapters for the canonical shapes are ForIdentityScope and ForArtifactScope. A surface with a bespoke body shape supplies its own; the handle is three strings, so an adapter is mechanical.
func ForArtifactScope ¶
func ForArtifactScope(s *types.ArtifactScope) ScopeRef
ForArtifactScope returns a ScopeRef over a request body's types.ArtifactScope. The pointer must be non-nil.
func ForIdentityScope ¶
func ForIdentityScope(s *types.IdentityScope) ScopeRef
ForIdentityScope returns a ScopeRef over a request body's types.IdentityScope. The pointer must be non-nil.
type Surface ¶
type Surface string
Surface is the registry key for a Protocol surface that decodes an identity scope from a request body. One surface owns one body-identity posture; every method the surface serves reconciles under it.
const ( SurfaceAgentConfig Surface = "agent_config" SurfaceAgents Surface = "agents" SurfaceApps Surface = "mcp.apps" SurfaceArtifacts Surface = "artifacts" SurfaceArtifactsPut Surface = "artifacts.upload" SurfaceArtifactsDelete Surface = "artifacts.removal" SurfaceArtifactsRef Surface = "artifacts.ref" SurfaceAuth Surface = "auth" SurfaceControlTask Surface = "task" SurfaceEvents Surface = "events" SurfaceFlows Surface = "flows" SurfaceGovernance Surface = "governance" SurfaceMCP Surface = "mcp.servers" SurfaceMemory Surface = "memory" SurfacePause Surface = "pause_page" SurfacePosture Surface = "runtime" SurfaceRuns Surface = "runs" SurfaceSessions Surface = "sessions" SurfaceStateHistory Surface = "state_history" SurfaceTasks Surface = "tasks" SurfaceTools Surface = "tools" SurfaceTopology Surface = "topology" )
The closed set of body-identity surfaces. A Protocol request type that carries an identity scope belongs to exactly one of them — the join is requestSurfaces below, and a lockstep test fails when a canonical request type has no row.
Two keys are deliberately not the surface's method prefix. SurfacePause and SurfaceStateHistory would otherwise read `pause` and `state.history`, which are canonical Protocol method names — and a method name is single-sourced in internal/protocol/methods, with a build-gating scan that rejects the literal anywhere else. The keys carry an underscore so the registry does not become a second place a method string is written. Operator-facing refusals use the Wire field, not the key.
func RegisteredSurfaces ¶
func RegisteredSurfaces() []Surface
RegisteredSurfaces returns every registered Surface, sorted. Used by the lockstep test to pin the closed set and by operator tooling that renders the posture table.
func SurfaceForRequest ¶
SurfaceForRequest returns the Surface governing the named canonical request type and a presence bool.
type Violation ¶
Violation is one breach found by a gate scan: the offending file (relative to the scanned root), the 1-based line, the kind of breach, and a detail that names the fix rather than only the mismatch.
func ScanElevationSites ¶
ScanElevationSites is the MINTING half of the gate. It walks the Go source under root and flags every call to a writer of the verified identity or of an audited tenant crossing that is not on the reviewed list.
allow maps a repo-relative file path to the reason that file is permitted to mint. The list is short by construction: seating a verified identity is a request-edge act, and crossing a tenant is an authorization act.
Returns the violations and the number of Go files read.
func ScanHandRolledGates ¶
ScanHandRolledGates is the ENFORCEMENT half of the gate. It walks the Go source under root and flags every comparison that reconciles a request body's identity component against a verified identity — the shape a hand-written body-scope helper always takes:
body.Tenant != verified.TenantID // wire component vs identity component body.Session != verified.Session // wire component vs wire component
allow maps a repo-relative file path to the reason it is exempt. An exemption is a reviewed decision, so it carries prose, not a bare path; an empty reason is itself a violation.
Stated limits ¶
The scan reads through a ONE-level hoist and follows an aliased `strings` import, which covers the refactors a contributor performs by habit. It does NOT resolve a two-level hoist (`b := a; b != x`), a `strings.ToLower(a) != strings.ToLower(b)` pair, or a comparison assembled through a function value. Those are deliberate evasions rather than accidents, and this scan is a copy-paste tripwire, not the control: the control is checkTenantMove, which runs at every write and cannot be evaded by any spelling. Widen the scan if one of these ever appears honestly.
Returns the violations and the number of Go files read.
func ScanWireTypes ¶
ScanWireTypes is the COVERAGE half of the gate. It parses the canonical wire-type tree at typesDir and reconciles what it finds against the surface registry, in both directions.
What it recognises, and what it does not ¶
A type is in scope when it carries a DIRECTLY-NAMED field of one of the canonical scope shapes (IdentityScope / ArtifactScope). Two shapes are deliberately NOT recognised, because Harbor's wire types do not use them today:
- a POINTER-typed scope field (*IdentityScope). The canonical bodies carry the scope by value; the one pointer use is IdentityScope's own Actor / Requester / Impersonating triplet, which the impersonation gate owns rather than this one.
- a FLAT tenant/user/session triple declared inline on the request instead of a named scope struct.
A wire type introducing either shape would pass this scan without a registry row. If one lands, widen structCarriesScope in the same change — the boundary is stated here so the next author meets it as a documented limit rather than a silent hole.
Returns the violations and the number of Go files read.