authz

package
v0.4.0-rc.21 Latest Latest
Warning

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

Go to latest
Published: Jul 6, 2026 License: AGPL-3.0 Imports: 8 Imported by: 0

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

View Source
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

View Source
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 (seeded in migration 002). 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

func AccessAtLeast(a, min agentsdk.Access) bool

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

func Authorize(ctx context.Context, q *dbq.Queries, p Principal, a Action, agentID uuid.UUID) error

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 MinAccess

func MinAccess(a, b agentsdk.Access) agentsdk.Access

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

func RequiredTenantRole(a Action) auth.Role

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).

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)

	// 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"

	// 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
	TenantSelfPasskeyManage   Action = "tenant.self.passkey.manage" // register / list / rename / delete the caller's own passkeys + set/remove own password: user+
	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

func GrantedTenantActions(role auth.Role) []Action

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. The two axes are independent (see airlock/AGENTS.md "Permission Model"): agent access comes from agent_grants, tenant role from the user record/JWT.

const (
	AxisAgent  Axis = iota // requires a per-agent access level
	AxisTenant             // requires a tenant role
)

type Grant

type Grant struct {
	GranteeID    uuid.UUID
	Capabilities []string
}

Grant is a resource_grants row reduced to what a capability check needs.

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
)

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
}

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 TriggerPrincipal

func TriggerPrincipal() Principal

TriggerPrincipal builds a non-human trigger principal (cron/webhook).

func UserPrincipal

func UserPrincipal(id uuid.UUID, role auth.Role) Principal

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

func (p Principal) GranteeSet() []uuid.UUID

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

func (p Principal) IsAuthenticatedUser() bool

IsAuthenticatedUser reports whether the principal is a registered user with a real UserID — the precondition for tenant-axis actions and for initiating A2A.

type Requirement

type Requirement struct {
	Axis   Axis
	Agent  agentsdk.Access // AxisAgent
	Tenant auth.Role       // AxisTenant
}

Requirement is the minimum access an action needs. Exactly one of Agent/Tenant is meaningful, per Axis.

Jump to

Keyboard shortcuts

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