Skip to content

Users, Groups & API Keys

Brutor has two distinct populations of people, plus machine credentials:

Identity Who Authenticates against Used for
Admin users Platform / tenant operators Control Plane (:5050) Admin UI, Admin API, provisioning scripts
End users People consuming AI (portal, chat clients) Core Proxy (:8100, /v1/portal/auth/login) User Portal, runtime traffic
API keys Agents, services, scripts Core Proxy (:8100) Programmatic LLM/MCP/Skill traffic

Admin users come in two tiers:

  • System admins sit above the tenant hierarchy (no tenant_id) — they manage tenants themselves. Login: POST /v1/admin-users/system/login.
  • Tenant admins belong to one tenant and carry a role (role_id) whose permissions gate every Admin API call. Login: POST /v1/admin-users/tenant/login with {username, password, tenant_id?}{access_token, ...}. Both admin types support TOTP 2FA — check requires_2fa in the login response before scripting against the API.

Admin users list in the Admin UI

Roles and their permission sets are managed under Admin Users → Roles in the Admin UI:

Tenant admin roles

End users are the people whose requests the gateway governs. Create them in the Admin UI or via the API:

Terminal window
curl -X POST http://localhost:5050/v1/admin/end-users \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"username": "ada",
"password": "S3cure-Passw0rd!",
"email": "ada@example.com",
"display_name": "Ada Lovelace",
"send_welcome_email": false
}'
{
"user": {
"id": 42,
"username": "ada",
"email": "ada@example.com",
"is_active": true,
"source": "manual"
}
}

Every end user carries a source recording how the account came to exist: manual (created by an admin), saml (JIT-provisioned at SSO login), scim (pushed by your IdP), or import.

Other useful endpoints: PATCH /v1/admin/end-users/{id}/status {"is_active": false} to deactivate, GET /v1/admin/end-users?search=ada, and CSV/JSON export under /v1/admin/end-users/export/csv|json.

End-user groups are how people get into resource groups. You never bind individual users to guardrails or models — you put users in an end-user group, then bind the group to one or more resource groups.

Terminal window
# Create a group with initial members
curl -X POST http://localhost:5050/v1/admin/end-user-groups \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "data-science",
"display_name": "Data Science",
"description": "DS team members",
"initial_member_ids": [42, 43]
}'
# Bind it to a resource group
curl -X POST http://localhost:5050/v1/admin/resource-groups/{rg_id}/end-user-groups \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"end_user_group_ids": ["eug-01H..."]}'

Membership management: POST /v1/admin/end-user-groups/{id}/members {"end_user_ids": [...]} adds, DELETE .../members removes, and PUT .../members sets the authoritative full list (useful for sync scripts).

Groups also carry a source (manual, saml, scim):

  • SAML — groups asserted by your IdP at login are matched by source_external_id and users are JIT-added.
  • SCIM — your IdP pushes users and groups to /scim/v2/Users and /scim/v2/Groups on the Control Plane, authenticated with a SCIM bearer token minted under Admin → SCIM tokens (/v1/admin/scim-tokens).

Multi-group users and the active workspace

Section titled “Multi-group users and the active workspace”

A user can be a member of many resource groups (directly, or via end-user groups). Two rules govern how that resolves at runtime:

  1. Access checks consider ALL groups — allow-wins. If any of the user’s groups grants a model, MCP server, or tool capability, the user has it. A capability disabled in one workspace can still be reachable through a less-restricted sibling group.

  2. One group is the active group per request — it determines usage attribution, limits, and workspace context. Selection order:

    • X-Resource-Group header, if present and the user really is a member of that group (client-supplied values are validated against actual memberships);
    • otherwise the user’s primary group;
    • otherwise the first membership.

The primary group is the user’s default workspace:

Terminal window
curl -X PATCH http://localhost:5050/v1/admin/end-users/42/primary-group \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"group_id": "9f2c..."}'

The user must already be a member of the target group.

API keys give agents, services, and scripts governed access to the runtime proxy without a human login. Keys are bound to a resource group — they inherit that group’s models, tools, guardrails, and limits. Keys are not inherited down the resource-group tree.

Format: sk_brutor_api_ followed by 43 URL-safe characters. Sent as Authorization: Bearer sk_brutor_api_... or X-API-Key: sk_brutor_api_....

Storage: only the SHA-256 hash is stored. The plaintext key appears exactly once — in the creation response. Capture it then or re-issue.

Scopes:

  • access_mode: "shared" → stored scope global: a shared key usable by the whole group — the standard choice for agents and services.
  • access_mode: "user" → scope user: tied to one end user (user_id required); requests attribute to that user.
Terminal window
curl -X POST http://localhost:5050/v1/admin/resource-groups/{rg_id}/api-keys \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "revenue-sentinel-agent",
"description": "Shared key for the revenue agent",
"access_mode": "shared",
"expires_at": "2027-01-01T00:00:00Z"
}'
{
"id": "apikey-8c1e...",
"key_value": "sk_brutor_api_M81FDppPA5VW6rA5SNpYXhbmkqSSN574W9i-fxYajGw",
"key_prefix": "sk_brutor_api_M81FDp...",
"name": "revenue-sentinel-agent",
"scope": "global",
"access_mode": "shared",
"group_id": "9f2c...",
"enabled": true,
"expires_at": "2027-01-01T00:00:00Z"
}
Operation Endpoint Effect
Disable POST .../api-keys/{key_id}/disable Temporary — key rejects until re-enabled
Enable POST .../api-keys/{key_id}/enable Undo disable
Revoke POST .../api-keys/{key_id}/revoke Permanent — records who/when/why
Expiry expires_at on the key null = never expires; expired keys fail auth

At runtime the proxy accepts a key only when enabled AND NOT revoked AND (expires_at IS NULL OR expires_at > now()).

A key can optionally be restricted to specific agent skills via allowed_skill_ids (a list of skill IDs; null means all skills the group grants) and throttled with skill_rate_limit_daily. Keys can also be bound to an agent identity (agent_id) for per-agent attribution and runtime authorization.

Usage per key is visible at GET /v1/admin/resource-groups/{rg_id}/api-keys/usage and in Mission Control.