authz

package
v0.9.0 Latest Latest
Warning

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

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

Documentation

Overview

Package authz is Wardyn's authorization kernel: the one Decision every door refuses with, and the registry of refusal reasons that is the audit contract a SIEM rule is written against. Pure Go — no HTTP, no store — so the same Decision can later cross a wire (schema authz/v1) unchanged.

Index

Constants

View Source
const AuditAction = "authz.denied"

AuditAction is the audit action every audited refusal is recorded under.

View Source
const Schema = "authz/v1"

Schema names the wire shape of Decision. Changes inside v1 are additive and readers ignore unknown fields; a removal or rename is a new schema.

Variables

This section is empty.

Functions

func Datum

func Datum(d Decision, p Principal, method string) map[string]any

Datum is the data of d's audit row, refused to p over method (empty when no request carried it). A detail never stands in for a reserved key, so it can never forge the reason or a marker. user_view is a marker present only when true (renamed in 0.8 from member_mode); device_channel is a sibling of the ingest marker device_origin, never that key.

Types

type Decision

type Decision struct {
	Schema string `json:"schema"`
	Effect Effect `json:"effect"`
	Status int    `json:"status"`
	Reason Reason `json:"reason,omitempty"`
	// Target is what was refused: a request field (runs.image), a route, or a
	// resource id.
	Target string     `json:"target,omitempty"`
	RunID  *uuid.UUID `json:"run_id,omitempty"`
	// Sentence is the refusal body. Empty means the reason's own sentence.
	Sentence string `json:"sentence,omitempty"`
	// WireReason overrides the reason an HTTP response carries, when set
	// (#656 slice 3's refuse fix). d.Reason itself is never overridden — the
	// audit row's own "reason" field (authz.Datum) always uses the TRUE
	// cause — this only changes what the CALLER sees. Its one use today is a
	// Hidden-effect decision (EffectHidden: "byte-identical to a missing
	// row"): the caller must see the SAME reason a truly-missing resource's
	// 404 carries, never the internal "not_owner"/"attach_ticket_foreign_run"
	// that would tell a prober the resource exists. See Decision.AsIf.
	WireReason Reason `json:"wire_reason,omitempty"`
	// Policy names the policy whose ceiling caused the refusal and how to ask for
	// a change. Set only by a ceiling door, and written to the wire by the HTTP
	// refusal emitter, never into the audit datum (Datum reads none of it). The
	// emitter leaves it off a decision with a WireReason and off an EffectHidden
	// one: a hidden door must stay byte-identical to a missing resource.
	Policy      *policyref.Ref    `json:"policy,omitempty"`
	Detail      map[string]string `json:"detail,omitempty"`
	Obligations []Obligation      `json:"obligations,omitempty"`
	Trace       []Step            `json:"trace,omitempty"`
}

Decision is one authorization answer. Every field is data (no funcs), so it serializes as-is.

func Deny

func Deny(reason Reason, target, sentence string) Decision

Deny is the refusal of target for reason, answered with sentence. The effect and status come from the registry, never from the door; an unregistered reason still refuses (403) and is caught where it is emitted.

func Drop

func Drop(reason Reason, target string, dropped []string) Decision

Drop is the refusal of the listed entries of target for reason: they are removed and the request proceeds without them.

func (Decision) AsIf

func (d Decision) AsIf(reason Reason) Decision

AsIf makes d's HTTP response carry reason instead of d.Reason, for a Hidden-effect decision whose wire body must match a specific entity's genuinely-missing twin byte for byte (#656 slice 3) — d.Reason (and so the audit row) is untouched; only WireReason changes.

func (Decision) OnRun

func (d Decision) OnRun(id uuid.UUID) Decision

OnRun is d about run id.

func (Decision) With

func (d Decision) With(key, value string) Decision

With is d with one more detail. The map is copied, so a Decision built from d is never changed by it.

func (Decision) WithPolicy added in v0.8.6

func (d Decision) WithPolicy(ref *policyref.Ref) Decision

WithPolicy is d naming ref as the policy that bound the person. A nil ref leaves d unchanged.

type Effect

type Effect string

Effect is what a Decision does to the request. Append-only: it is part of the wire contract (testdata/wire.golden).

const (
	EffectAllow Effect = "allow"
	EffectDeny  Effect = "deny"
	// EffectHidden is the 404 twin: byte-identical to a missing row, so a
	// refusal is never an existence oracle.
	EffectHidden        Effect = "hidden"
	EffectUnprocessable Effect = "unprocessable"
	// EffectConflict is a refusal answered 409: the request is well-formed and
	// the caller could otherwise make it, but their own session state (a
	// deleted user-view type) conflicts with it.
	EffectConflict Effect = "conflict"
	// EffectUnavailable is a refusal answered 503: the caller is authorized
	// and the request well-formed, but this server cannot prove a safety
	// property the answer depends on (a run's masking corpus), so it refuses
	// instead of passing bytes through.
	EffectUnavailable Effect = "unavailable"
	// EffectUnauthenticated is a refusal answered 401: the credential itself is
	// no longer acceptable (its role stamp is too old), and signing in again is
	// the remedy.
	EffectUnauthenticated Effect = "unauthenticated"
	// EffectBadRequest is a refusal answered 400: the request combines parameters the door does not
	// accept together. Input shape, not a caller's rights.
	EffectBadRequest Effect = "bad_request"
)

func (Effect) Status

func (e Effect) Status() int

Status is the HTTP status a refusal with this effect answers with.

type Obligation

type Obligation struct {
	Kind   ObligationKind `json:"kind"`
	Detail string         `json:"detail"`
}

Obligation is one thing a Decision does to one part of the request.

type ObligationKind

type ObligationKind string

ObligationKind is what an allow still does to the request. Append-only, like Effect.

const ObligationDrop ObligationKind = "drop"

ObligationDrop removes one entry from the request and lets the rest proceed.

type Origin

type Origin struct {
	DeviceID  *uuid.UUID `json:"device_id,omitempty"`
	Placement string     `json:"placement,omitempty"`
}

Origin is where a request reached the deciding control plane from. Zero in 0.8. Only the org sets DeviceID, from the device channel it authenticated — never from anything a laptop asserts.

type Principal

type Principal struct {
	Subject string `json:"subject"`
	// MemberView: an admin exercising "view as member"; only marks the row,
	// the tier the kernel saw is already clamped.
	MemberView bool `json:"member_view,omitempty"`
	// UserType is the type a member view looks through; read only with MemberView.
	UserType string `json:"user_type,omitempty"`
	Origin   Origin `json:"origin"`
}

Principal is who a decision is about.

type Reason

type Reason string

Reason is a refusal's stable code: the `reason` of its authz.denied row and of its Decision. The set is the registry below and nothing else.

Append-only once 0.8.0 is tagged (testdata/wire.golden): a SIEM rule is written against these strings, so from then on a removal or a rename is a major version.

const (
	ReasonAdminSurface                   Reason = "admin_surface"
	ReasonSecurityAdminSurface           Reason = "security_admin_surface"
	ReasonNotOwner                       Reason = "not_owner"
	ReasonAttachTicketForeignRun         Reason = "attach_ticket_foreign_run"
	ReasonBYOIUser                       Reason = "byoi_user"
	ReasonCapabilityAgent                Reason = "capability_agent"
	ReasonCapabilityEgressHost           Reason = "capability_egress_host"
	ReasonCapabilityFeature              Reason = "capability_feature"
	ReasonCapabilityPolicy               Reason = "capability_policy"
	ReasonCapabilitySecret               Reason = "capability_secret"
	ReasonCapabilityWorkspace            Reason = "capability_workspace"
	ReasonCapabilityWorkspaceProvider    Reason = "capability_workspace_provider"
	ReasonCapabilityModelProvider        Reason = "capability_model_provider"
	ReasonCapabilityComponent            Reason = "capability_component"
	ReasonGovernanceProfile              Reason = "governance_profile"
	ReasonPlacementComponentSelfDefined  Reason = "placement_component_self_defined"
	ReasonGovernanceOverlayUnsatisfiable Reason = "governance_overlay_unsatisfiable"
	ReasonGrantPairingNotEligible        Reason = "grant_pairing_not_eligible"
	ReasonGroupsSnapshotStale            Reason = "groups_snapshot_stale"
	ReasonRunKept                        Reason = "run_kept"
	ReasonRunNotFound                    Reason = "run_not_found"
	ReasonRunTerminal                    Reason = "run_terminal"
	ReasonSecondHumanRequired            Reason = "second_human_required"
	ReasonRunQuota                       Reason = "run_quota"
	ReasonUserTypeUnknown                Reason = "user_type_unknown"
	ReasonUserViewTypeDeleted            Reason = "user_view_type_deleted"
	// ReasonModelProviderUnavailable: a run's model provider cannot credential
	// it — none by that name, off, not serving the agent, none chosen among
	// several, no usable credential of the caller's for it, or a policy grant
	// that would set a model credential beside it (#987).
	ReasonModelProviderUnavailable Reason = "model_provider_unavailable"
	// ReasonAdminView: an admin in the user view launched a run after the type
	// the view looks through was deleted. Not audited on its own — the cause
	// row is ReasonUserViewTypeDeleted, which the launch response answered.
	ReasonAdminView Reason = "admin_view"
	// ReasonUserViewPreview: with WARDYN_GOVERN_ADMIN_RUNS on, an admin whose
	// User view looks through a type other than their own sent a write; the
	// view is a read-only preview. 409, audited with the viewed and stamped types.
	ReasonUserViewPreview Reason = "user_view_preview"
	// ReasonDelegationScope: a portal's delegated token asked for a route
	// outside the delegation allow-list (#1142).
	ReasonDelegationScope Reason = "delegation_scope"
	// ReasonServiceScope: a service identity's token asked for a route outside
	// its fixed allow-list (#1508).
	ReasonServiceScope Reason = "service_scope"
	// ReasonEventStreamCap: the caller already holds the most concurrent run
	// event streams one principal may (#1407). A limit, not a denial: not
	// audited, like ReasonRunQuota.
	ReasonEventStreamCap Reason = "event_stream_cap"
	// ReasonRunOwnerOnly: interactive entry (terminal, UI app, SSH, take-over)
	// needs the run's owner; a super admin is refused unless the run has no
	// personal owner (#1476). Not hidden: the admin can already see the run.
	ReasonRunOwnerOnly Reason = "run_owner_only"
	// ReasonMaskStateUnavailable: a door that relays or persists a run's output
	// (recording upload, live attach, SSH shell, exec relay, live output read)
	// cannot prove this run's masking corpus complete here, so it refuses with
	// 503 instead of passing bytes through unmasked (ha-l2.0).
	ReasonMaskStateUnavailable Reason = "mask_state_unavailable"
	// ReasonDriveNotFound, ReasonDriveTargetConflict and ReasonDriveNotInPool:
	// a run's drives[] names a drive the caller's own grants do not reach, two
	// drives that derive one path, or a drive the run's pool cannot mount. 422,
	// audited; the drive_not_found row never says whether the id exists.
	ReasonDriveNotFound       Reason = "drive_not_found"
	ReasonDriveTargetConflict Reason = "drive_target_conflict"
	ReasonDriveNotInPool      Reason = "drive_not_in_pool"
	// ReasonRoleStampStale: an API token's role and group stamp is older than
	// WARDYN_ROLE_STAMP_TTL. Its owner signs in again to re-stamp it.
	ReasonRoleStampStale Reason = "role_stamp_stale"
	// ReasonRecordingGoverned: Record Mode is refused for an admin whose runs are
	// governed (WARDYN_GOVERN_ADMIN_RUNS) unless the deployment exempts recording.
	ReasonRecordingGoverned Reason = "recording_governed"
	// ReasonAuditExportPartitionFilter: a partition export (GET /audit/export?partition=) was asked
	// to narrow the partition with another filter, so the digest in its footer would not cover the
	// whole partition. Input shape, not a denial: not audited.
	ReasonAuditExportPartitionFilter Reason = "audit_export_partition_filter"
	// ReasonKeyDomainUnknown: a key-domain assignment named a domain the
	// deployment's key domains file does not declare.
	ReasonKeyDomainUnknown Reason = "key_domain_unknown"
	// ReasonKeyDomainAmbiguous: a group assignment would leave people whose
	// groups are assigned to different domains, with no user assignment of
	// their own, or while someone whose last sign-in lost groups has none, so
	// their next principal key would be refused; or the delete of a user
	// assignment would leave that person so.
	ReasonKeyDomainAmbiguous Reason = "key_domain_ambiguous_membership"
	// The five refusals of POST /audit/retention/drop, one per rule audit_retention_drop (migration
	// 0123) enforces. State conflicts of an authorized security operator, audited so an attempt that
	// was refused is on the record.
	ReasonAuditRetentionNotOldest      Reason = "audit_retention_not_oldest"
	ReasonAuditRetentionNotClosed      Reason = "audit_retention_not_closed"
	ReasonAuditRetentionInsideWindow   Reason = "audit_retention_inside_window"
	ReasonAuditRetentionLiveRun        Reason = "audit_retention_live_run"
	ReasonAuditRetentionDigestMismatch Reason = "audit_retention_digest_mismatch"
	// ReasonAuditRetentionActorReserved: the drop's caller is named "wardynd", the name the database
	// reads as the sweeper (a system, unattested drop), so no request may carry it.
	ReasonAuditRetentionActorReserved Reason = "audit_retention_actor_reserved"
	// ReasonComponentAutonomy: the organisation's autonomy cap on runs that
	// carry a self-defined component (site config components.autonomy_cap)
	// alone bound the run's level, and the request asks for more than it
	// permits. A tie with a governance profile's rubric refuses as
	// ReasonGovernanceProfile instead.
	ReasonComponentAutonomy Reason = "component_autonomy"
)

func Reasons

func Reasons() []Reason

Reasons is every registered reason, sorted.

type Refusal

type Refusal struct {
	Effect Effect
	// Audit: the refusal writes an authz.denied row. False only for a caller
	// who IS authorized and is at a limit — a row per quota hit would make a
	// busy user look like an attacker.
	Audit bool
	// Sentence is the reason's own body, used when the door supplies none;
	// set only where every door refusing with the reason says the same thing.
	Sentence string
}

Refusal is one reason's registry row.

func Lookup

func Lookup(reason Reason) (Refusal, bool)

Lookup returns reason's registry row; false for a reason nobody registered, which no door may emit.

type Step

type Step struct {
	Rule    string `json:"rule"`
	Tier    string `json:"tier,omitempty"`
	RowID   string `json:"row_id,omitempty"`
	Outcome string `json:"outcome"`
}

Step is one rule a decision passed through, for an explanation. Strings only, so a Decision round-trips through JSON unchanged.

Jump to

Keyboard shortcuts

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