Skip to content

Service principals and keys

A service principal is a tenant's non-human member: an integration, a CI job, an MCP client with no person behind it. It is granted roles like a person, and acts with a key.

Its id

A service principal's id is a PrincipalId: not empty, and with no :, ~ or whitespace. The libraries' own actors take ids with a :, such as relayr's rules (reflexr:<rule>), so no principal a tenant makes, and no key it issues, can pass for one (ADR-0028). An id outside that form is refused wherever a principal is made, and the admin API answers it with 422.

Issuing a key

from grantr import Resource, ServicePrincipal, User

ALICE = User(id="alice")
ci = ServicePrincipal(id="ci")

await grants.add_member("acme", ci, by=ALICE)
await grants.grant("acme", "viewer", on=Resource(kind="workspace", workspace="main"), to=ci, by=ALICE)
key, entry = await grants.issue_key("acme", ci, by=ALICE)

A key is grantr_sk_<id>_<secret>, shown once: grantr stores its public id, its tenant, its principal and a digest of its secret, never the secret. Issuing or revoking one needs grantr:issue_key or grantr:revoke_key, and every action the principal's grants hold, since a key acts with them. revoke_key takes the key's id, and removing the principal takes its keys with it.

Authenticating a key

found = await grants.authenticate(key)  # ("acme", ServicePrincipal(id="ci")), or None

authenticate finds the key by its id and compares digests in constant time. It answers with the key's one tenant and its principal, so a key reaches only its own tenant; give that tenant to the admin API's Caller(principal, tenant), and to the libraries' resolvers. A key for the application's own principal is never answered.

stackr's template reads a key only from the Authorization header, where a token would be, so a key never lands in a URL's logs. When an external agent has no person behind it, the template checks it as the key's principal.