Documentation
¶
Overview ¶
Package authz is the single authorization layer every surface gates through. It models the caller as a Principal (registered user, anonymous human, or non-human trigger), resolves that principal's effective per-agent access off agent_grants, and checks it against a central action→required-level policy. HTTP handlers, bridges, A2A, and the system agent all build a Principal and call Authorize — there is no second place that decides "what level does this action need".
It is a low-level package (deps: agentsdk, auth, dbq, apperr, uuid, pgtype) so both service and trigger can import it without a cycle.
Index ¶
- Constants
- Variables
- func AccessAtLeast(a, min agentsdk.Access) bool
- func Authorize(ctx context.Context, q *dbq.Queries, p Principal, a Action, agentID uuid.UUID) error
- func AuthorizeOwnedResource(ctx context.Context, q *dbq.Queries, p Principal, ownerID uuid.UUID, ...) error
- func AuthorizeResource(ctx context.Context, q *dbq.Queries, p Principal, action Action, ...) error
- func LockResource(ctx context.Context, q *dbq.Queries, resourceType string, resourceID uuid.UUID) error
- func MinAccess(a, b agentsdk.Access) agentsdk.Access
- func RequiredTenantRole(a Action) auth.Role
- func ResourceCapabilities(ctx context.Context, q *dbq.Queries, p Principal, resourceType string, ...) ([]string, error)
- type Action
- type Axis
- type Grant
- type Kind
- type Principal
- func (p Principal) EffectiveAgentAccess(ctx context.Context, q *dbq.Queries, agentID uuid.UUID) agentsdk.Access
- func (p Principal) EffectiveAgentAccessGranted(ctx context.Context, q *dbq.Queries, agentID uuid.UUID) (access agentsdk.Access, granted bool)
- func (p Principal) GranteeSet() []uuid.UUID
- func (p Principal) HasResourceCapability(ownerPrincipalID uuid.UUID, grants []Grant, capability string) bool
- func (p Principal) IsAuthenticatedUser() bool
- type Requirement
Constants ¶
const ( CapView = "view" CapBind = "bind" CapManage = "manage" )
Resource capabilities (the management plane). Runtime "use" of a bound resource is intrinsic to the agent's code and is not gated here; these gate who may see, attach (bind), and reconfigure a resource.
Variables ¶
var ( GroupAdmin = uuid.MustParse("00000000-0000-0000-0000-0000000000a1") GroupManager = uuid.MustParse("00000000-0000-0000-0000-0000000000a2") GroupUser = uuid.MustParse("00000000-0000-0000-0000-0000000000a3") )
Built-in group principal ids in the schema baseline. They let a grant target a tenant role ("all managers") without a stored-membership table: the resolver expands a caller into the role-groups it belongs to. admin ⊇ manager ⊇ user, so a higher role inherits the lower groups' grants.
Functions ¶
func AccessAtLeast ¶
AccessAtLeast reports whether a ranks at or above min on the per-agent access ladder. The single comparator for that ladder — chat slash gates, the service-layer Authorize gate, and the MCP path all rank through here so the ordering can't drift between surfaces.
func Authorize ¶
Authorize is the single gate. It looks the action up in the policy table and checks the principal against the required level on the action's axis. Returns:
- apperr.ErrUnauthorized when there is no authenticated identity (a registered-user principal with a zero UserID — i.e. no JWT).
- apperr.ErrForbidden when the principal is known but ranks below the requirement.
- nil when allowed.
Panics on an action missing from the policy table (fail loud — a new action must not silently default to allowed). agentID is ignored for tenant-axis actions.
func AuthorizeOwnedResource ¶
func AuthorizeOwnedResource(ctx context.Context, q *dbq.Queries, p Principal, ownerID uuid.UUID, adminAction Action) error
AuthorizeOwnedResource gates on "the caller owns the resource, OR the caller's tenant role satisfies adminAction." Use this anywhere a row has a single owner_id (bridges, platform_identities, OAuth grants) and an admin escape exists for cross-user moderation.
ownerID is the UserID stored on the resource. adminAction must be a tenant-axis Action — the policy table is the single source of truth for who can act on someone else's resource.
Returns nil if owner, otherwise the result of Authorize(adminAction). Anonymous / trigger principals fall through to Authorize, which rejects them with 401/403 as appropriate.
func AuthorizeResource ¶
func AuthorizeResource(ctx context.Context, q *dbq.Queries, p Principal, action Action, resourceType string, resourceID uuid.UUID) error
AuthorizeResource applies the central authenticated-user policy and then the requested capability to a concrete management-plane resource.
func LockResource ¶
func LockResource(ctx context.Context, q *dbq.Queries, resourceType string, resourceID uuid.UUID) error
LockResource serializes capability changes and credential replacement on a concrete resource. Callers lock need rows first when an operation has both.
func MinAccess ¶
MinAccess returns the lower of two access levels on the agent ladder. It composes the A2A delegation caps: a sibling agent acting on a user's behalf can never exceed the access its own owner holds on the target, nor the per-edge max_access the operator set on the address-book entry. The effective access is the minimum across (driving user, acting-agent owner, edge max_access).
func RequiredTenantRole ¶
RequiredTenantRole returns the minimum tenant role for a tenant-axis action, so the router's RequireTenantRole middleware can source its level from the same policy table rather than a literal. Panics if the action is unknown or not tenant-axis (fail loud).
func ResourceCapabilities ¶
func ResourceCapabilities(ctx context.Context, q *dbq.Queries, p Principal, resourceType string, resourceID uuid.UUID) ([]string, error)
ResourceCapabilities returns the caller's complete capability set after requiring bind access. It is used by the need picker, where bind-only resources remain visible while manage controls shared authorization changes.
Types ¶
type Action ¶
type Action string
Action names a gated operation. The policy map below is the single source of truth for "what level does this action require" — every surface authorizes by Action, never by a hardcoded level at the call site, so the two routes that can reach the same action can't drift.
const ( // Agent axis — member (AccessUser) suffices. AgentGet Action = "agent.get" AgentUpdate Action = "agent.update" AgentLifecycle Action = "agent.lifecycle" // stop / start / suspend AgentGit Action = "agent.git" // connect / disconnect / read git binding AgentRunView Action = "agent.run.view" // runs list / get / logs (admin: runs span all users) AgentMembersView Action = "agent.members.view" AgentToolsView Action = "agent.tools.view" AgentBuildsView Action = "agent.builds.view" // builds list / get / log stream (admin) AgentConversation Action = "agent.conversation" // create / list web conversations AgentModelsView Action = "agent.models.view" AgentClone Action = "agent.clone" // fork this agent's code into a new agent (member of source; also needs TenantAgentClone) AgentFileResolve Action = "agent.file.resolve" // Agent axis — owner (AccessAdmin) required. AgentDelete Action = "agent.delete" AgentBuildManage Action = "agent.build.manage" // upgrade / rollback / cancel build AgentMembersManage Action = "agent.members.manage" AgentWebhooksView Action = "agent.webhooks.view" AgentSchedulesView Action = "agent.schedules.view" AgentScheduleFire Action = "agent.schedule.fire" AgentConnections Action = "agent.connections" // credentials / MCP / env-vars AgentExecEndpoints Action = "agent.exec_endpoints" // SSH exec-endpoint config AgentSiblings Action = "agent.siblings" AgentModelsUpdate Action = "agent.models.update" AgentRoutesView Action = "agent.routes.view" AgentManagedBotCreate Action = "agent.managed_bot.create" AgentIntegrationInvoke Action = "agent.integration.invoke" // Tenant axis. TenantCatalogView Action = "tenant.catalog.view" // read providers/models/capabilities catalog: user+ TenantUserView Action = "tenant.user.view" // read tenant user directory (id/email/display_name): user+ TenantAgentCreate Action = "tenant.agent.create" // create an agent: manager+ TenantAgentList Action = "tenant.agent.list" // list agents visible to the caller: user+ TenantAgentListAll Action = "tenant.agent.list_all" // list every agent in the tenant: admin TenantAgentMembersSelfAdd Action = "tenant.agent.members.self_add" // escape: tenant admin adds self to an agent they're not yet a member of: admin TenantAgentLifecycleAny Action = "tenant.agent.lifecycle_any" // stop/start/suspend an agent the caller isn't a member of: admin TenantAgentDeleteAny Action = "tenant.agent.delete_any" // delete an agent the caller isn't a member of: admin TenantAgentClone Action = "tenant.agent.clone" // clone an agent (produces a new agent): manager+ (paired with AgentClone member gate) TenantAgentTransferAny Action = "tenant.agent.transfer_any" // transfer an agent the caller doesn't own: admin TenantBridgeList Action = "tenant.bridge.list" // list bridges visible to the caller: user+ TenantBridgeListAll Action = "tenant.bridge.list_all" // list every bridge in the tenant: admin TenantBridgeCreate Action = "tenant.bridge.create" // any bridge: manager+ TenantBridgeUpdateAny Action = "tenant.bridge.update_any" // edit a bridge the caller doesn't own: admin TenantBridgeDeleteAny Action = "tenant.bridge.delete_any" // delete a bridge the caller doesn't own: admin TenantBridgeSystem Action = "tenant.bridge.system" // system (agent-less) bridge: admin TenantManagerBotConfig Action = "tenant.manager_bot.config" // configure the Telegram-managed-bots manager bot token: admin TenantUserManage Action = "tenant.user.manage" TenantProviderView Action = "tenant.provider.view" // list configured providers (no secrets) for model selection: manager+ TenantProviderManage Action = "tenant.provider.manage" TenantSettingsView Action = "tenant.settings.view" // read system defaults (agent-create prefill): user+ TenantSettingsUpdate Action = "tenant.settings.update" TenantIdentityManage Action = "tenant.identity.manage" // link / list / unlink caller's own platform identities: user+ TenantIdentityManageAll Action = "tenant.identity.manage_all" // list / unlink any user's platform identities: admin TenantSelfProfileUpdate Action = "tenant.self.profile.update" // update the caller's own profile: user+ TenantSelfPasskeyManage Action = "tenant.self.passkey.manage" // register / list / rename / delete the caller's own passkeys + set/remove own password: user+ ResourceInventoryView Action = "resource.inventory.view" // list resources available through ownership or grants: user+ ResourceView Action = "resource.view" // authenticated precondition; resource capability checked by AuthorizeResource ResourceBind Action = "resource.bind" // authenticated precondition; resource capability checked by AuthorizeResource ResourceManage Action = "resource.manage" // authenticated precondition; resource capability checked by AuthorizeResource TenantGroupView Action = "tenant.group.view" // list groups + their grants: admin TenantModelGrantManage Action = "tenant.model_grant.manage" // grant/revoke which (provider, model) a group may use: admin TenantUsageView Action = "tenant.usage.view" // read the LLM spend ledger rollups (billing/usage): admin )
func GrantedTenantActions ¶
GrantedTenantActions returns every tenant-axis Action that `role` satisfies, sorted lexicographically. The frontend consumes this via /api/v1/me to gate UI without duplicating the role ladder — `can(a)` becomes set membership, and adding a new Action surfaces in the UI purely through whatever can() call sites reference it.
Agent-axis actions are deliberately excluded: their requirement depends on per-resource membership, not tenant role, and surface in the agent payload's `your_access` field instead.
type Axis ¶
type Axis int
Axis is the permission axis an action gates on. Agent access and tenant role are independent (see airlock/AGENTS.md "Permission Model"); authenticated actions establish identity before a domain-specific capability check.
type Kind ¶
type Kind int
Kind discriminates how a caller was authenticated. It exists to kill the old userID==uuid.Nil ambiguity, which conflated "anonymous human" with "no human at all".
const ( // KindRegisteredUser — authenticated via a user JWT. UserID and // TenantRole are set. KindRegisteredUser Kind = iota // KindAnonymousUser — a human with no account (bridge public DM). // Resolves to AccessPublic; may do public-reachable actions but no // member/admin ones. KindAnonymousUser // KindTrigger — no human at all (cron/webhook). Cannot delegate as a // user; agent-axis actions and A2A initiation are denied. KindTrigger // KindCodegen is an active agent build using its narrow integration // credential. It has no tenant or ordinary agent standing. KindCodegen )
type Principal ¶
type Principal struct {
Kind Kind
UserID uuid.UUID // RegisteredUser only; uuid.Nil otherwise
TenantRole auth.Role // RegisteredUser only
// OnBehalfOfAgent records the agent whose code is acting (A2A /
// system-agent delegation). Audit/logging only — authorization
// evaluates the delegated principal as the originating user, no more.
OnBehalfOfAgent uuid.UUID
// BuildID and CodegenAgentID are set only for KindCodegen. The integration
// policy verifies both against the active build row on every service call.
BuildID uuid.UUID
CodegenAgentID uuid.UUID
}
Principal is the caller identity threaded through every gated call. Build it once at the surface boundary (handler / bridge / A2A) and pass it down; nothing below invents identity.
func AnonymousPrincipal ¶
func AnonymousPrincipal() Principal
AnonymousPrincipal builds an anonymous-human principal (bridge public DM).
func CodegenPrincipal ¶
CodegenPrincipal builds the narrow principal admitted only by integration policy actions while its build credential remains active.
func TriggerPrincipal ¶
func TriggerPrincipal() Principal
TriggerPrincipal builds a non-human trigger principal (cron/webhook).
func UserPrincipal ¶
UserPrincipal builds a registered-user principal. A uuid.Nil id yields a principal that Authorize treats as unauthenticated (ErrUnauthorized).
func (Principal) EffectiveAgentAccess ¶
func (p Principal) EffectiveAgentAccess(ctx context.Context, q *dbq.Queries, agentID uuid.UUID) agentsdk.Access
EffectiveAgentAccess resolves the principal's access on agentID off agent_grants. A grant may target the principal's own user id (per-user member) or a role-group in its grantee-set (e.g. the built-in `user` group = every registered user, "shared with everyone"); the effective access is the max role across all matching grants. Everyone with no matching grant (anonymous, trigger, non-member registered user) maps to AccessPublic. Surface-specific "is public allowed here" policy (e.g. the agent's allow_public_mcp flag) lives at the surface, not in this ladder.
func (Principal) EffectiveAgentAccessGranted ¶
func (p Principal) EffectiveAgentAccessGranted(ctx context.Context, q *dbq.Queries, agentID uuid.UUID) (access agentsdk.Access, granted bool)
EffectiveAgentAccessGranted is EffectiveAgentAccess plus whether an actual grant matched. The access value alone can't distinguish "AccessPublic because an explicit `public` grant matched" from "AccessPublic because the caller is a non-member at the floor" — both read as AccessPublic. The A2A entitlement gate needs that distinction (an explicit All-Users `public` grant admits the caller; the bare floor does not), so it reads granted.
func (Principal) GranteeSet ¶
GranteeSet returns the principal ids a grant may target to reach p: p's own user id plus the built-in role-groups its tenant role belongs to. A grant to the `user` group reaches everyone; to `manager`, managers + admins. This is the open-source policy resolver — the single seam a higher tier swaps to also consult a stored-membership table. Returns nil for a non-registered principal (anonymous / trigger have no role standing).
func (Principal) HasResourceCapability ¶
func (p Principal) HasResourceCapability(ownerPrincipalID uuid.UUID, grants []Grant, capability string) bool
HasResourceCapability reports whether p holds capability on a resource owned by ownerPrincipalID and carrying grants. True if p (or a role-group in its grantee-set) owns the resource — owners hold view/bind/manage implicitly — or holds a grant carrying capability.
func (Principal) IsAuthenticatedUser ¶
IsAuthenticatedUser reports whether the principal is a registered user with a real UserID — the precondition for tenant-axis actions and for initiating A2A.