Skip to content

Admin Auth and Security

This reference documents the current Core management authorization model in 0.0.1. It is intentionally explicit because this layer protects the control plane.

Plystra Core has no admin token. Every non-public management route must be authorized by one of these credentials:

CredentialHeaderPrincipalIntended use
User access tokenAuthorization: Bearer <access_token>A User with active AdminGrant rows.Human admin consoles, operators, user-driven management flows.
API keyX-Plystra-API-Key: <api_key> or Authorization: Bearer ply_ak_...A scoped ApiKey row with explicit permission keys.Server-to-server automation and authorization checks.

Public routes are limited to health, ready, version, protected registration, login, refresh, logout, actor context, actor switch-member, and the disabled/protected metrics handler. Sensitive APIs are not intended to be anonymous.

LevelScope fieldsWhat it can reachNotes
instance_super_adminno space_id, no group_idEntire instance and all permission keys.Only user sessions are considered super admin. API keys are never super admins.
instance_adminno space_id, no group_idEntire instance for matching permission_key.Cannot create or revoke instance-level grants unless the same user also has super admin.
space_adminspace_id requiredMatching permission key inside exactly that Space.Cannot see or mutate instance-level AdminGrants or instance API keys.
group_admingroup_id required; space_id is resolved from the groupMatching permission key inside the group subtree.Does not cover sibling groups or whole-space list operations.

An AdminGrant is active only when:

  • status = active
  • deleted_at IS NULL
  • revoked_at IS NULL
  • expires_at IS NULL OR expires_at > now

The associated User must also remain active.

LevelScope fieldsWhat it can reach
instanceno space_id, no group_idMatching permission key across the instance.
spacespace_id requiredMatching permission key inside exactly that Space.
groupgroup_id required; space_id is resolved from the groupMatching permission key inside the group subtree.

An API key is active only when:

  • status = active
  • deleted_at IS NULL
  • revoked_at IS NULL
  • expires_at IS NULL OR expires_at > now
  • submitted plaintext token HMAC-matches the stored key_hash

Core stores only HMAC hashes for API keys. The plaintext key is returned only once when the key is created.

Permission keys use lowercase domain:action.

Grant keyRequired key it matches
*Any key.
exact key, such as users:readThe same exact key.
domain:*Any action in that domain.
domain:manageAny action in that domain, including read, create, revoke, and other domain-specific actions.

Invalid keys are rejected:

*:read
Users:read
users
users:
users:read:extra
users:read/write

The middleware resolves the required permission from method and path before the handler runs.

Route groupRequired permission
GET /api/v1/console/overviewinstance:read
POST /api/v1/authz/checkauthz:check
POST /api/v1/authz/explainauthz:check
/metrics when enabled without metrics tokenmetrics:read
GET /api/v1/admin/meinstance:read
GET /api/v1/admin/grantsadmin_grants:read
GET /api/v1/admin/grants/{id}admin_grants:read, resolved to that grant’s scope
POST /api/v1/admin/grantsadmin_grants:manage
POST /api/v1/admin/grants/{id}/revokeadmin_grants:manage, resolved to that grant’s scope
GET /api/v1/api-keysapi_keys:read
GET /api/v1/api-keys/{id}api_keys:read, resolved to that key’s scope
POST /api/v1/api-keysapi_keys:create
POST /api/v1/api-keys/{id}/revokeapi_keys:revoke, resolved to that key’s scope
GET /api/v1/audit/logsaudit:read, optional space_id query scope
GET /api/v1/audit/logs/{id}audit:read, resolved to the audit log’s Space
GET /api/v1/users*users:read
mutating /api/v1/users*users:manage
GET /api/v1/spaces*spaces:read
mutating /api/v1/spaces*spaces:manage
GET /api/v1/spaces/{space_id}/groups*groups:read in the Space or resolved Group
mutating /api/v1/spaces/{space_id}/groups*groups:manage in the Space or resolved Group
GET /api/v1/spaces/{space_id}/members*members:read in the Space
mutating /api/v1/spaces/{space_id}/members*members:manage in the Space
GET /api/v1/spaces/{space_id}/user-members*user_members:read in the Space
mutating /api/v1/spaces/{space_id}/user-members*user_members:manage in the Space
GET /api/v1/spaces/{space_id}/roles*roles:read in the Space
mutating role and member-role routesroles:manage in the Space
GET /api/v1/permissions* and GET /api/v1/role-permissions*permissions:read
mutating permission and role-permission routespermissions:manage
GET /api/v1/resource-types*registry:read
mutating resource type, action, and mapping routesregistry:manage
GET /api/v1/resourcesresources:read, optionally scoped by space_id
POST /api/v1/resourcesresources:manage, target scope resolved in handler
GET /api/v1/resources/{type}/{id}resources:read, resolved to resource Space and Group
nested Space resource routesresources:read or resources:manage in the Space or resolved Group
data preview routesdata:read or data:manage; feature disabled by default
plugin routesplugins:read or plugins:manage
template routestemplates:read or templates:manage

Handlers re-check target scope for routes where the middleware cannot know the target body yet, especially API key creation, AdminGrant creation, authorization checks, and resource mutations.

These rules are enforced in handlers and covered by tests:

  • A user can create an API key only if the user has api_keys:create for the target scope.
  • A user can put permission keys on a new API key only if the user already holds those permission keys for the target scope.
  • A space admin cannot create an instance API key.
  • A space admin cannot create an API key in another Space.
  • A space admin cannot read instance API keys.
  • A space admin cannot read instance-level AdminGrants.
  • A group admin cannot read sibling group resources.
  • A group admin cannot list whole-space resources through a group grant.
  • Only a user session can create or revoke AdminGrants.
  • API keys cannot create or revoke human AdminGrants, even with admin_grants:manage.
  • Only an instance super admin can create or revoke instance_super_admin or instance_admin grants.
  • Core refuses to revoke the last active instance_super_admin grant.
  • API keys with * are not considered instance super admins.

Some create routes need to pass middleware before the request body is parsed. Middleware can temporarily allow a scoped principal through these domains:

api_keys:*
admin_grants:*
authz:check

That pass-through is not the final authorization. The handler must then resolve the real target space_id, group_id, resource, or grant and deny if the principal is out of scope.

For existing objects, Core resolves the object’s stored scope before deciding visibility. This prevents a Space admin from listing or reading instance-level API keys or AdminGrants.

Registration route:

POST /api/v1/auth/register

Registration is disabled unless an operator explicitly enables it. Ordinary registration requires PLYSTRA_AUTH_REGISTRATION_ENABLED=true and a matching PLYSTRA_AUTH_REGISTRATION_TOKEN; production also requires that token to be at least 32 characters. Ordinary registration is refused until at least one active instance_super_admin grant already exists.

Ordinary registration creates a User, default Member, default UserMember, session, and a Space admin grant inside the single Simple Mode application default Space space_default. It does not create one Space per User and does not create an instance super admin.

Public user-only registration can be enabled with PLYSTRA_AUTH_PUBLIC_USER_REGISTRATION_ENABLED=true; it does not require a registration token and creates only a User, without Member, UserMember binding, admin grant, or session.

First-super-admin bootstrap through registration is separate: set PLYSTRA_BOOTSTRAP_REGISTRATION_ENABLED=true and use PLYSTRA_BOOTSTRAP_REGISTRATION_TOKEN. This path is only available while no active instance_super_admin grant exists, creates the user, the Simple Mode default Space/Member/UserMember, a Space admin grant, and the initial instance_super_admin grant in one transaction, then returns a session token pair.

Login route:

POST /api/v1/auth/login

Body:

{
"email": "[email protected]",
"password": "plystra-demo"
}

Response includes:

{
"access_token": "ply_at_...",
"refresh_token": "ply_rt_...",
"token_type": "Bearer",
"expires_at": "2026-05-12T01:00:00Z",
"refresh_expires_at": "2026-06-11T01:00:00Z",
"user": {},
"actor": {},
"available_members": []
}

Current token behavior:

  • access token TTL is 15 minutes.
  • refresh token TTL is 30 days.
  • refresh rotates both access and refresh tokens.
  • logout revokes by bearer access token or body refresh token.
  • session token hashes use PLYSTRA_SESSION_SECRET.
  • passwords are stored with Argon2id.
  • password changes revoke existing sessions for that User.
  • login is rate-limited by normalized email and source IP.

Create route:

POST /api/v1/api-keys

Body:

{
"name": "billing-service-prod",
"level": "space",
"space_id": "space_acme",
"permission_keys": ["authz:check", "resources:read"],
"expires_at": "2026-12-31T23:59:59Z",
"metadata": {
"owner": "billing-platform"
}
}

Response includes api_key once:

{
"id": "ak_...",
"name": "billing-service-prod",
"key_prefix": "ply_ak_ak_...",
"level": "space",
"space_id": "space_acme",
"permission_keys": ["authz:check", "resources:read"],
"api_key": "ply_ak_ak_....secret"
}

Production handling:

  • Store plaintext api_key in a secret manager.
  • Do not log plaintext API keys.
  • Use PLYSTRA_API_KEY_SECRET for HMAC hashing.
  • During secret rotation, set PLYSTRA_API_KEY_SECRET_PREVIOUS to a comma-separated list of previous secrets.
  • Revoke stale keys with POST /api/v1/api-keys/{id}/revoke.

Bearer session call:

{
"resource_type": "invoice",
"resource_id": "invoice_001",
"action": "approve"
}

Core uses the session active actor.

API key call:

{
"actor": {
"user_id": "user_alice",
"member_id": "member_finance_reviewer",
"user_member_id": "um_alice_finance_reviewer",
"space_id": "space_acme"
},
"resource_type": "invoice",
"resource_id": "invoice_001",
"action": "approve"
}

API keys must include the actor.

At minimum, add tests for:

  • expected allow case.
  • no matching permission deny.
  • out-of-scope group deny.
  • cross-space deny.
  • revoked UserMember deny.
  • inactive User deny.
  • API key revoked deny.
  • API key expired deny.
  • API key cannot create AdminGrant.
  • Space admin cannot access another Space.
  • Group admin cannot access sibling Group.

Core’s own test suite includes these boundaries, but your application should still test the way it maps application users and objects into Plystra ids.