You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* feat(auth): portal_delegate key scope + single-route fence (ent#163)
The OSS half of delegated portal identity: the scope, its containment, and the
admin-only mint. The endpoint that decides *whether* an email may be delegated
stays in the entitled module — core owns the enforcement primitive only, the
same split as `PORTAL_SESSION_SCOPE` (#78) and `users.suspended_at` (#995).
Why a new scope rather than reusing `scope='user'`: the capability lets its
holder act as another person and read their conversations. Riding it on an
ordinary user key would silently turn EVERY user key into a fleet-wide
impersonation key. It is admin-issued, listed and revocable like any other key,
and revoking it stops delegation on the next request.
The fence is the load-bearing part. A delegate key resolves to the KEY OWNER,
exactly like every other MCP key, so unfenced it would simply BE an admin's
credential handed to a third party. `get_current_user` confines it to one
(method, path) — the exchange route — at the single auth entry point, mirroring
the connector fence (ent#46): centrally, not in the portal router, because the
many endpoints doing inline access checks resolve this principal to the owner
and would otherwise treat it as that human.
Deliberately ONE route, not a prefix: the minted portal session — not this key —
drives the portal surface afterwards, so the key never needs breadth, and a
prefix would silently grant every portal endpoint added in future.
Minting is admin AND human-only. `assert_admin` alone is insufficient: it
rejects connector principals but not agent-scoped ones, and an agent key
resolves to its owner carrying the owner's role, which on a default admin-owned
install passes a bare role check (trinity-ops-agent#232). The db layer
independently refuses any scope outside `{user, portal_delegate}` so
agent/connector/system keys — which carry an agent binding and are minted by
their own paths — can never be forged through this endpoint.
Incidental fix in the same function: `create_mcp_api_key` now sets `is_active=1`
explicitly instead of relying on a column default that exists in only one of the
two schema sources — `schema.py` declares `is_active INTEGER DEFAULT 1` but
`db/tables.py` (the Core/Alembic source) declares a bare `Column(...)`. A table
built from the metadata yields NULL, and `validate_mcp_api_key` treats falsy
`is_active` as revoked, so every minted key would be born invalid. Harmless on
both live schema paths; found because it broke a test built from the metadata.
Tests: `test_163_portal_delegate_scope.py` — structural (the fence is one route,
the model defaults to False, the db creatable-set excludes agent scopes) plus
behavioural, driving the real `get_current_user`: a delegate key is accepted on
the exchange route and 403s on the fleet, the user list, key minting, the portal
surface itself, and the right path with the wrong method. Verified the
behavioural half fails (5 tests) with the fence disabled.
103 passed across the auth-surface suites; 692 passed over the wider
key/auth/portal selection. Enterprise-docs guard clean.
Related to trinity-enterprise#163
* feat(ui): surface portal_delegate keys in Settings → MCP Keys (ent#163)
The issue's AC asks for the scope to be "admin-issued, listed and revocable in
Settings → MCP Keys". Listing and revoking already worked — but a
`portal_delegate` key rendered with NO badge, falling through the
`agent`/`system` v-if chain and looking exactly like an ordinary user key. For a
credential that can act as any end user with portal access, that is misleading
in precisely the place an admin audits keys. And there was no way to create one
from the UI at all, so "admin-issued" meant "curl only".
Two small additions:
* an amber **Portal Delegate** badge, deliberately distinct from the purple
Agent / red System badges, with a title explaining what the key can do
* an admin-only checkbox on the create modal (`v-if="isAdmin"`), opt-in rather
than a default — minting a key that acts as other people should be a
conscious choice, never a stray click. The scope is omitted entirely (not sent
as null) for an ordinary key, so nothing changes for existing callers.
Verified live: badge renders on both delegate keys; the checkbox appears for an
admin and mints a working `scope=portal_delegate` key. The end-to-end flow was
driven against local dev — mint → fenced (403 on /api/agents, /api/users,
/api/mcp/keys) → exchange for two different end users from ONE key → disjoint
rosters (bob→[evt-boss], carol→[evt-worker]) → unauthorized email 403 → revoke →
401.
Related to trinity-enterprise#163
|`agent`| Explicit permission list (`agent_permissions`) | Resolves to owner user; ownership/sharing checks only |
1984
1984
|`system`|**Bypasses all checks**| Resolves to owner user (system agent owner) |
1985
+
|`portal_delegate`| n/a (not an MCP tool principal) |**Fenced to a single route** — may only exchange an asserted end-user email for a portal session; every other path 403s (ent#163) |
0 commit comments