auth

package
v2.7.0-dev.5 Latest Latest
Warning

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

Go to latest
Published: Jul 20, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

Documentation

Overview

Package auth defines the per-caller identity primitive used by the multi-session attach layer. See docs/multi-session-design.md for the full design.

The package is intentionally substrate-only: it defines Caller, Authenticator, the authorization matrix, and the static user table loader. Wiring into the attach server, per-session sub-gates, and audit-log threading are layered on in later phases (see issue #162).

Backward compatibility: when no Authenticator is configured, callers should default to AnonymousAuth — every request resolves to the same "anon" identity and the multi-session features behave as if disabled.

Index

Constants

View Source
const HeaderAssertedCaller = "X-Asserted-Caller"

HeaderAssertedCaller is the conventional header name a proxy Caller uses to assert the effective identity. The actual header name is operator-configurable (attach.multi_session.asserted_caller_header) but this constant is the default.

View Source
const UsersFileSchemaVersion = 1

UsersFileSchemaVersion is the schema version this loader understands. Bump when the on-disk shape changes in a way that breaks older loaders; LoadUsersFile rejects unknown versions so operators don't silently lose new fields.

Variables

View Source
var Anonymous = Caller{Identity: "anon"}

Anonymous is the conventional Caller for unauthenticated requests when AnonymousAuth is in effect. Callers of AnonymousAuth may override the identity via configuration (attach.multi_session.default_identity) but "anon" is the documented default.

View Source
var ErrAssertedCallerForbidden = errors.New("auth: caller is not permitted to assert identities")

ErrAssertedCallerForbidden is returned when a non-proxy Caller attempts to use the X-Asserted-Caller header. Callers map this to a 401 response and should log the attempt — it indicates either misconfiguration or a credential that should not be assertable.

View Source
var ErrAssertedCallerUnknown = errors.New("auth: asserted identity is not provisioned")

ErrAssertedCallerUnknown is returned when a proxy Caller asserts an identity that is not present in the configured user table. Callers map this to a 401 response.

View Source
var ErrUnauthenticated = errors.New("auth: unauthenticated")

ErrUnauthenticated is returned by Authenticator.Authenticate when no valid credential is present on the request. Callers map this to a 401 response.

Functions

func Authorize

func Authorize(c Caller, a Action, acl SessionACL) bool

Authorize reports whether c may perform a against the given session ACL. The matrix follows docs/multi-session-design.md §"Authorization rules":

| Action          | Admin | Owner | Viewers | Contributors |
|-----------------|-------|-------|---------|--------------|
| SessionList     |   ✓   |   ✓   |    ✓    |       ✓      |
| SessionRead     |   ✓   |   ✓   |    ✓    |       ✓      |
| SessionWrite    |   ✓   |   ✓   |         |       ✓      |
| SessionAdmin    |   ✓   |   ✓   |         |              |
| DaemonAdmin     |   ✓   |       |         |              |

SessionList is always permitted at the API layer; handlers filter results per-Caller (hiding the existence of unauthorized sessions prevents leaking activity patterns).

func ProxyByFromContext

func ProxyByFromContext(ctx context.Context) (by string, ok bool)

ProxyByFromContext returns the proxying identity previously stored on ctx by WithProxyBy. ok is false (and the string empty) when the request did not go through the proxy path.

func WithCaller

func WithCaller(ctx context.Context, c Caller) context.Context

WithCaller returns a new context carrying c. Use in middleware that has resolved the request's Caller; downstream code reads it via CallerFromContext.

func WithProxyBy

func WithProxyBy(ctx context.Context, by string) context.Context

WithProxyBy returns a new context carrying the proxying identity alongside the effective Caller. Set when the request was routed via the proxy path (X-Asserted-Caller header) so audit logs can capture BOTH the effective Caller and the credential that asserted it (e.g., effective="alice@", proxy_by="sa:slack-bot").

Pair with WithCaller (which carries the effective identity); downstream code reads via ProxyByFromContext.

Types

type Action

type Action int

Action enumerates the authorization decisions the multi-session attach layer makes. The matrix is intentionally small in α.1; finer scoping (per-tool, per-MCP-server, per-session-sub-area) is deferred per docs/multi-session-design.md §"Out of scope".

Handlers don't enforce these in α.1 — enforcement wiring is α.2. The function is here so the type surface is settled before the handlers wrap on top.

const (
	// ActionSessionList is the GET /sessions endpoint. Always
	// permitted; the handler filters results to sessions the Caller
	// can actually read.
	ActionSessionList Action = iota
	// ActionSessionRead covers reading session state — events
	// stream, status, tools, memory, permissions, etc.
	ActionSessionRead
	// ActionSessionWrite covers mutating endpoints — inject,
	// wake, interrupt, slash commands.
	ActionSessionWrite
	// ActionSessionAdmin covers ACL / metadata mutations on the
	// session — modifying SessionACL, deleting the session.
	ActionSessionAdmin
	// ActionDaemonAdmin covers daemon-scoped read/write — peer
	// registry, global metrics, anything not bound to a single
	// session. Admin-only.
	ActionDaemonAdmin
)

func (Action) String

func (a Action) String() string

String returns the action name for diagnostics / audit logs.

type AnonymousAuth

type AnonymousAuth struct {
	Caller Caller
}

AnonymousAuth resolves every request to the same Caller. It is the default Authenticator wired into pkg/attach when multi-session is disabled — every request is "anon" (or whatever default identity the operator configured) and downstream code sees a Caller-on-context just like in multi-session deployments.

Zero value resolves to the package-level Anonymous Caller. Set Caller explicitly to override the identity (the operator-facing knob is attach.multi_session.default_identity).

func (AnonymousAuth) Authenticate

func (a AnonymousAuth) Authenticate(_ *http.Request) (Caller, error)

Authenticate ignores the request and returns the configured Caller. Never returns an error.

type Authenticator

type Authenticator interface {
	Authenticate(r *http.Request) (Caller, error)
}

Authenticator extracts a Caller from an inbound HTTP request, or returns ErrUnauthenticated when no valid credential is present.

Implementations shipped in α.1:

  • AnonymousAuth — single fixed Caller; the default for single-user deployments and any deployment with multi-session disabled.
  • BearerTokenAuth — token → Caller lookup against a static table loaded from users.json (see LoadUsersFile).

Future implementations (designed but not in α.1): OIDC/JWT, mTLS (subject DN → Caller), K8s ServiceAccount (TokenReview).

type AuthenticatorWithProxy

type AuthenticatorWithProxy interface {
	Authenticator
	CanProxyAs(c Caller) bool
}

AuthenticatorWithProxy is the optional extension implemented by authenticators that support the proxy pattern (a Caller authorized to assert other Callers via the X-Asserted-Caller header).

The bot integration use case: a Slack/GChat bot authenticates as itself, then asserts the human user's identity per request so audit logs and per-caller MCP credentials attribute to the human, not the bot. CanProxyAs reports whether the resolved Caller is on the configured proxy allowlist.

Authenticators that don't implement this interface implicitly deny all proxy assertions.

type BearerTokenAuth

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

BearerTokenAuth validates the request's bearer token against a static table loaded from users.json. Returns the matched Caller, or ErrUnauthenticated when no token is presented or the token is unknown.

Token comparison is constant-time (subtle.ConstantTimeCompare) to avoid leaking match prefixes through response timing. The table is indexed by token for O(1) lookup; identities are not exposed by the lookup path.

Accepted headers, in order:

  1. X-Attach-Token (matches the existing daemon-level side-channel header used when an identity gateway owns Authorization)
  2. Authorization: Bearer <token>

Proxy semantics: a Caller resolved here is permitted to assert other identities via X-Asserted-Caller only if it appears in the ProxyIdentities allowlist. See CanProxyAs.

func NewBearerTokenAuth

func NewBearerTokenAuth(users []User, adminIdentities, proxyIdentities []string) *BearerTokenAuth

NewBearerTokenAuth builds an authenticator from a parsed user table (typically the result of LoadUsersFile). adminIdentities marks the listed identities as Admin Callers; proxyIdentities marks them as permitted to use X-Asserted-Caller.

Empty tokens in the users slice are skipped — a misconfigured row shouldn't authenticate every credential-less request. Duplicate tokens are last-write-wins; the loader should reject duplicates upstream but the authenticator is defensive.

func (*BearerTokenAuth) Authenticate

func (b *BearerTokenAuth) Authenticate(r *http.Request) (Caller, error)

Authenticate resolves the request's bearer token against the table. Returns ErrUnauthenticated when no token is presented or the token is not in the table.

func (*BearerTokenAuth) CanProxyAs

func (b *BearerTokenAuth) CanProxyAs(c Caller) bool

CanProxyAs reports whether c is on the operator-configured proxy allowlist. Returns false for callers not in the allowlist and for the zero-value Caller (defense against accidental authorization).

func (*BearerTokenAuth) HasIdentity

func (b *BearerTokenAuth) HasIdentity(identity string) bool

HasIdentity reports whether the named identity exists in the user table. Used by the proxy path: a bot can only assert identities the operator has provisioned (see ErrAssertedCallerUnknown).

func (*BearerTokenAuth) LookupIdentity

func (b *BearerTokenAuth) LookupIdentity(identity string) (Caller, bool)

LookupIdentity returns the Caller registered for the given identity, or zero-Caller + false when the identity is not in the table. Used by the proxy path to materialize the asserted Caller (preserving Labels and Admin flag from the user table entry).

type Caller

type Caller struct {
	Identity string
	Labels   map[string]string
	Admin    bool
}

Caller is the opaque identity attached to every authenticated request entering the daemon. Subsequent code uses it for authorization (see Authorize) and audit (see eventlog metadata, layered in α.2).

Identity is a stable opaque ID — typically an email ("alice@example.com"), a service-account marker ("sa:platform-agent"), or "anon" for unauthenticated requests when anonymous access is allowed.

Labels carry free-form metadata from the auth source (e.g., {"team": "platform", "issuer": "https://oidc.example.com"}). Available to authorization logic and audit consumers; not part of the Identity equality check.

Admin grants the see-everything role; set per configuration (attach.multi_session.admin_identities). Admin Callers pass every Authorize check.

func CallerFromContext

func CallerFromContext(ctx context.Context) (c Caller, ok bool)

CallerFromContext returns the Caller previously stored on ctx by WithCaller. ok is false when no Caller is present (e.g., code paths reached before the authenticator middleware, or in tests).

type SessionACL

type SessionACL struct {
	Owner        string
	Viewers      []string
	Contributors []string
}

SessionACL captures who can do what to a session. Owner is the creator (full access). Viewers can read; Contributors can read + write but not modify the ACL itself.

Owner may be a synthetic identity like "channel:#incident-response" for shared-session deployments where the session belongs to a group rather than a single user.

Zero value (empty Owner, nil slices) grants nothing to any non-Admin Caller — this is the safe default when a session was created before multi-session was enabled (legacy sessions become admin-only-accessible until the operator assigns an Owner).

type User

type User struct {
	Identity string            `json:"identity"`
	Token    string            `json:"token"`
	Labels   map[string]string `json:"labels,omitempty"`
}

User is one row in users.json. Identity is the stable opaque ID the daemon stamps onto audit log entries; Token is the bearer credential clients present. Labels are free-form metadata available to downstream authorization / observability.

Identity and Token are both required; rows missing either are rejected at load time (silently skipping them would hide misconfiguration from the operator).

type UsersFile

type UsersFile struct {
	Version int    `json:"version"`
	Users   []User `json:"users"`
}

UsersFile is the on-disk shape of attach.multi_session.auth.table_file. Operators populate this directly today; an OIDC / IDP-backed loader is layered in later (see docs/multi-session-design.md §"Migration story").

func LoadUsersFile

func LoadUsersFile(path string) (*UsersFile, error)

LoadUsersFile reads + validates a users.json file from disk.

Validation:

  • File mode must be 0600 or stricter on POSIX (group/other bits must be zero). The file holds bearer secrets; world- or group-readable permissions are a configuration error, not a tolerable laxity. Skipped on Windows where Unix mode bits don't map cleanly.
  • Schema version must match UsersFileSchemaVersion.
  • Every row must carry both identity and token.
  • Token values must be unique across rows (duplicate tokens would produce nondeterministic identity resolution).
  • Identity values must be unique across rows.

Jump to

Keyboard shortcuts

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