Files
DocsGPT/docs/content/Deploying/Access-Control.mdx
T

217 lines
15 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Access Control, Roles & Teams
description: How DocsGPT models permissions — global admin/user roles (RBAC), the admin dashboard, teams with per-team roles, and sharing agents, sources, prompts, and tools.
---
import { Callout } from 'nextra/components'
# Access Control, Roles & Teams
DocsGPT has two independent permission planes:
- **Global RBAC** — every user holds the `admin` or `user` role for the whole instance. Admins can manage other users and see instance-wide usage and audit data.
- **Team roles** — within a team a user is a `team_admin` or `team_member`, and resources can be shared to teams or individual members.
The two planes never mix: a global `admin` is a superuser over *all* teams, but a `team_admin` is **not** a global admin.
<Callout type="info" emoji="🔒">
Roles are resolved **server-side on every request** and are never trusted from the JWT. The frontend route guards are cosmetic — the server-side decorators are the real boundary. Persisted roles apply under `AUTH_TYPE=oidc`; token-only modes (`simple_jwt`, `session_jwt`) can never hold the admin role.
</Callout>
## Global roles (RBAC)
| Role | Capabilities |
| --- | --- |
| `user` | The default. Owns their own conversations, sources, agents, prompts, and tools. |
| `admin` | Everything a user can do, plus the [admin dashboard](#admin-dashboard): user management, force-logout, role management, and instance-wide usage/audit. |
To see the roles the current request resolves to, call:
```text
GET /api/user/me → { "user_id": "...", "roles": ["user"], "email": "...", "name": "...", "picture": "..." }
```
This is the canonical way to check a caller's effective roles (distinct from `/api/config`, which only reports the instance `auth_type`).
## Granting admin
There are four ways an account becomes an admin:
1. **The `grant_admin` script** — run `python scripts/grant_admin.py <user_id>` on the server to grant (or `--revoke` / `--list`) the admin role directly. This is the canonical bootstrap for the first admin.
2. **OIDC group mapping** — `OIDC_ADMIN_GROUPS` auto-grants admin to members of the listed IdP groups. See [below](#admin-via-oidc-groups).
3. **Local no-auth mode** — `LOCAL_MODE_ADMIN=true` grants admin when `AUTH_TYPE=None` (self-host, no authentication).
4. **Grant by an existing admin** — through the admin API/dashboard.
| Setting | Default | Description |
| --- | --- | --- |
| `OIDC_ADMIN_GROUPS` | — | Comma-separated IdP groups whose members are granted the global `admin` role. Re-checked at every login and silent renewal. Unset = no OIDC admin mapping. |
| `LOCAL_MODE_ADMIN` | `false` | Grants admin in no-auth mode only (`AUTH_TYPE=None`). |
<Callout type="warning" emoji="⚠️">
**Never set `LOCAL_MODE_ADMIN=true` on a networked deployment.** It only makes sense for a single-user local install with `AUTH_TYPE=None`, where there is no identity to check.
</Callout>
### Bootstrapping the first admin
Granting admin through the dashboard itself requires *already being* an admin, which creates a chicken-and-egg problem on a fresh deployment. Break it one of these ways:
- **Run the script** on the server: `python scripts/grant_admin.py <user_id>` (the `user_id` is the OIDC `sub`). The grant is written to `user_roles` with `source='manual'` and takes effect on the user's next request. Use `--list` to see current admins and `--revoke` to remove a manual grant.
- Set `OIDC_ADMIN_GROUPS` to a group you belong to, and sign in — you become an admin automatically.
- For a no-auth local install, use `LOCAL_MODE_ADMIN=true`.
### Admin via OIDC groups
When `OIDC_ADMIN_GROUPS` is set, group membership is mapped to the admin role at every sign-in *and* every [silent renewal](/Deploying/OIDC-SSO#silent-session-renewal) — so removing a user from the admin group revokes their admin at the next renewal, just like the [sign-in allowlist](/Deploying/OIDC-SSO#restricting-sign-in-by-group). It is independent of `OIDC_ALLOWED_GROUPS` (which controls *whether* a user may sign in at all). Leaving `OIDC_ADMIN_GROUPS` unset never mass-revokes admin.
```env
OIDC_ADMIN_GROUPS=platform-admins
# OIDC_GROUPS_CLAIM=groups # only if your IdP uses a different claim name
```
If your IdP only exposes groups via the userinfo endpoint, DocsGPT backfills them from there during reconciliation, the same as the allowlist.
## Admin dashboard
Admins get a dashboard backed by a REST surface under `/api/admin` (every endpoint requires the admin role). All mutating actions are written to the `auth_events` audit log with the acting admin recorded as the event's `actor_id`.
| Method | Path | Description |
| --- | --- | --- |
| `GET` | `/api/admin/overview` | Instance overview counts. |
| `GET` | `/api/admin/users` | List users (paginated; supports a `user_id` filter). |
| `GET` | `/api/admin/users/<id>` | Drill into a single user. |
| `PATCH` | `/api/admin/users/<id>` | Activate / deactivate a user (deactivation revokes their sessions). |
| `POST` | `/api/admin/users/<id>/role` | Grant the admin role (guards against removing the last admin). |
| `DELETE` | `/api/admin/users/<id>/role` | Revoke the admin role. |
| `POST` | `/api/admin/users/<id>/revoke-sessions` | Force-logout a user. |
| `GET` | `/api/admin/admins` | List current admins. |
| `GET` | `/api/admin/usage` | Usage series with tokens and spend, per-model split, latency percentiles, top users. Takes `days`, `bucket` and `group_by=model\|agent\|source`. |
| `GET` | `/api/admin/users/<id>/usage` | One user's spend: daily series plus a split by model and by flow. |
| `GET` | `/api/admin/activity` | [Merged activity feed](#activity-feed) across all three audit journals. |
| `GET` | `/api/admin/activity/events` | The `(event, category)` pairs this instance has recorded, for the filter UI. |
| `GET` | `/api/admin/activity/export` | The filtered feed as `format=csv` or `format=ndjson`. |
| `GET` | `/api/admin/audit` | Authentication/admin audit feed (`auth_events` only). |
| `GET` | `/api/admin/devices/audit` | Remote-device audit feed. |
| `GET` | `/api/admin/teams` | Instance-wide oversight of all teams. |
| `GET` `PUT` `DELETE` | `/api/admin/quotas/...` | [Usage quotas](/Deploying/Usage-Quotas) for the instance, teams and users. |
<Callout type="info" emoji="ℹ️">
Deactivating a user via the dashboard works for any auth type, while OIDC deployments can also offboard through [SCIM](/Deploying/OIDC-SSO#scim-user-provisioning). Both revoke live sessions immediately.
</Callout>
## Teams
Teams let a group of users collaborate and share resources. Teams are self-serve — any user can create one and manage its membership.
### Team roles
| Role | Capabilities |
| --- | --- |
| `team_member` | Belongs to the team; can use resources shared with the team. |
| `team_admin` | Manages membership and team settings. Implies `team_member`. |
The team **owner** is a distinct concept from `team_admin`: ownership can be transferred, and a global `admin` is treated as a superuser over every team.
### Managing a team
| Method | Path | Description |
| --- | --- | --- |
| `GET` / `POST` | `/api/teams` | List your teams / create a team. |
| `GET` / `PATCH` / `DELETE` | `/api/teams/<id>` | Read, update, or delete a team. |
| `GET` / `POST` | `/api/teams/<id>/members` | List members / add a member. |
| `PATCH` / `DELETE` | `/api/teams/<id>/members/<member_id>` | Change a member's role / remove (or leave). |
| `POST` | `/api/teams/<id>/transfer_owner` | Transfer ownership. |
Membership and removal are guarded so a team can never be left without an admin (the last-admin guard).
<Callout type="info" emoji="ℹ️">
Members are added by email (or subject id). The invitee **must have signed in at least once** so DocsGPT can resolve them to a user account.
</Callout>
## Sharing resources with a team
Four resource types can be shared: **agents**, **sources**, **prompts**, and **tools**. Sharing is **additive** — it grants access to others without changing ownership.
| Method | Path | Description |
| --- | --- | --- |
| `GET` / `POST` / `DELETE` | `/api/teams/<id>/grants` | List, create, or revoke a share within a team. |
| `GET` | `/api/resource_shares` | List shares visible to the caller. |
Sharing rules:
- Only the **owner** of a resource can share it.
- A share targets either the **whole team** or a **single member**.
- Each share carries an access level: **`viewer`** (read-only) or **`editor`** (read and modify).
- `editor` is not the same as owner — an editor can change a resource but cannot delete it or re-share it.
- Shared tools run server-side with the **owner's** credentials; a grantee never sees the owner's secrets.
- A wiki's editors can edit its pages, but only its owner decides whether API and widget users can edit it through an agent (see [Wiki sources](/Sources/Wiki-sources#edits-from-the-api-widget-and-public-links)).
### Resources an editor adds to someone else's agent
An agent (and its workflow) runs as its owner, so its tools, sources and prompts are checked against the owner's access. When a team editor adds one the owner can't use, it runs with the **editor's** access instead, for everyone who uses the agent: members of the teams it is shared with, anyone with its API key or website widget, its public link and its webhook. The editor becomes that resource's **sponsor**.
- Only someone who **owns** the resource, or has **`editor`** access to it through any team, can sponsor it. `viewer` access lets you use a resource in your own agents, but not extend it to another agent's users: the save is refused with `403` and `code: "sponsor_not_allowed"`.
- Sponsoring is never implied. A save that would make you a new sponsor is refused with `409` until you confirm it. In DocsGPT, a dialog names each resource and who will reach it through the agent. Through the API, send the save again with `confirm_sponsor` listing every resource from the response as `"<type>:<id>"`. A `confirm_sponsor` entry for anything the save doesn't ask you to sponsor is refused with `400`.
- A sponsored resource stops running when its sponsor can no longer edit the agent, or no longer owns or edits the resource. It stays on the agent but does nothing, and it doesn't pass to whoever saves the agent next. The agent's edit page says why it stopped. An editor who may sponsor it can choose **Run … with my access** there, and it runs with their access from their next save. Through the API, include its key in `confirm_sponsor` on any save. Otherwise, someone removes it.
- A resource that is removed and later added again needs a new confirmation, even if its old sponsor could still sponsor it.
- Editors can remove any tool, source or prompt from the agent or its workflow nodes, including the owner's private ones they can't open.
- Workflow nodes follow the same rules, through `PUT /api/workflows/<id>`.
- The confirmation covers the audience the agent has at that moment. If the owner later shares the agent with more teams, or turns on a public link, sponsored resources reach those people too, and their sponsors aren't asked again.
- `audience.teams` lists every team the agent is shared with, including teams where only some members were given access.
A save that needs confirmation returns:
```json
{
"success": false,
"code": "sponsor_confirmation_required",
"message": "These resources would run with your access for everyone who uses this agent. Confirm to add them.",
"resources": [{ "key": "tool:<id>", "type": "tool", "id": "<id>", "name": "Jira" }],
"audience": { "teams": ["Support"], "api_key": true, "public_link": false, "webhook": false }
}
```
Owners and editors see sponsored resources on the agent's edit page and in `resource_sponsors` from `GET /api/get_agent` (and `GET /api/workflows/<id>`). Viewers get an empty list. Each entry has the resource (`key`, `type`, `id`, `name`), the sponsor (`user_id`, `label`), `state` (`active` or `inactive`), `reason` when inactive (`sponsor_cannot_edit_agent` or `sponsor_cannot_edit_resource`), and `can_confirm`, which says whether you may take an inactive one over.
<Callout type="warning" emoji="⚠️">
After upgrading, resources sponsored by someone with only `viewer` access to them stop running. An editor who owns or can edit such a resource can choose **Run … with my access** on the agent's edit page to start it again.
</Callout>
## Audit log
Access-control actions are appended to the `auth_events` table alongside the [authentication events](/Deploying/OIDC-SSO#login-auditing). This includes admin actions — `admin_user_activated` / `admin_user_deactivated`, `admin_sessions_revoked`, `role_granted` / `role_revoked` (with `metadata.source` = `manual` or `oidc_group`), `quota_policy_set` / `quota_policy_deleted` — and team events (`team.create`, `team.member_add`, `team.member_role`, `team.member_remove`, `team.share`, `team.unshare`, `team.transfer_owner`, `team.delete`).
Data-plane actions are recorded too: `source.created` / `source.deleted` / `source.reingested` / `source.wiki_settings_updated`, `agent.created` / `agent.updated` / `agent.deleted` / `agent.key_regenerated`, and `conversation.deleted` / `conversation.deleted_all`.
Every row carries `actor_id` (who did it) and `target_id` (the user it was done to, or `NULL` when the event is not about a user), so "everything this admin did" is a single query:
```sql
SELECT created_at, event, target_id, metadata
FROM auth_events
WHERE actor_id = 'the-admin'
ORDER BY created_at DESC;
```
### Activity feed
The **Admin → Activity** tab merges three append-only journals into one timeline:
| Journal | Category | What it records |
| --- | --- | --- |
| `auth_events` | `identity`, `access`, `config`, `data` | Sign-ins, provisioning, role and account changes, quota changes, resource mutations. |
| `device_audit_log` | `device` | Every remote-device command dispatch and its verdict. |
| `guardrail_events` | `safety` | Every guardrail decision on a request. |
Filter by category, event name, actor, affected user, a time window, or a free-text search that reaches into each row's detail payload, then export the filtered feed as CSV or NDJSON. Exports stream and are capped at 100,000 rows — take a database dump for a full history.
An unknown filter value is refused with a 400 rather than dropped. Dropping it would leave that facet unfiltered, and no filter means every row, so a typo or a stale bookmark would silently widen an audit view instead of narrowing it.
The per-user panel in **Admin → Users** deliberately lists identity and access events only. Data-plane events are filed under the user who performed them, so on an active account routine activity would push a denied login or a role grant out of the window; use the Activity tab filtered by that user to see everything.
Two guardrail columns are deliberately never projected into the feed or the export: `api_key` (a raw agent key) and `matched_value` (unredacted source text). Admin-gating is not a reason to widen what a list response carries.
## Related
- [SSO with OIDC](/Deploying/OIDC-SSO) — sign-in, group allowlists, and the `auth_events` table.
- [Usage Quotas](/Deploying/Usage-Quotas) — token and cost limits per user and per team.
- [App Configuration](/Deploying/DocsGPT-Settings) — the full settings reference.