mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-04 14:12:58 +00:00
234 lines
18 KiB
Plaintext
234 lines
18 KiB
Plaintext
---
|
||
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; after they confirm in the same dialog that names who reaches the agent, 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 owner's own saves are checked too: a node can't name a tool or source its owner can't use.
|
||
- 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>
|
||
|
||
### When a resource stops working
|
||
|
||
Every run checks each tool, source and prompt on the agent (and each tool and source on its workflow nodes) against the owner's access, or the sponsor's. One that no longer passes is left out of the run; a prompt falls back to the default prompt. The agent's edit page, and the workflow builder for node resources, lists each one that stopped with the reason and what you can do:
|
||
|
||
| Reason | What happened | What you can do |
|
||
| --- | --- | --- |
|
||
| `deleted` | The tool or source was deleted. | Remove it. |
|
||
| `owner_lost_access` | The owner can no longer use it, for example a team stopped sharing it with them. | Ask whoever shares it to share it again, choose **Run … with my access** if you may sponsor it, or remove it. |
|
||
| `sponsor_cannot_edit_agent`, `sponsor_cannot_edit_resource` | Its sponsor lost access (see above). | Choose **Run … with my access** if you may sponsor it, or remove it. |
|
||
| `connection_needs_reconnect` | The account a connected tool uses was disconnected or needs signing in again. | If it is your account, choose **Reconnect**; otherwise ask the tool's owner. |
|
||
| `connection_removed` | The connection was removed but the tool was kept. | Ask the tool's owner to connect the account again, or remove it. |
|
||
| `connector_disabled` | An admin turned the service off. | Ask an admin to turn it back on, or remove it. |
|
||
|
||
**Remove** takes the item off the form; save to store it. Through the API, `GET /api/get_agent` and `GET /api/workflows/<id>` return `resource_states` to owners and editors (an empty list to everyone else): one entry per attached resource with `key`, `type`, `id`, `name`, `state` (`active` or `stopped`), `reason`, `sponsor` and `contact` (`{user_id, label}`: the recorded sponsor, and someone else who can fix it), `connection` (the service, for a connection reason), `can_confirm` and `can_reconnect`. When anything can be taken over, the response also carries `sponsor_audience`, the same shape as `audience` above. A name is shown only for a resource that runs, that someone sponsored, that you can see yourself, or that is on an agent (agent saves check every reference).
|
||
|
||
The check is the one the run uses, so the page never shows a resource as running when the run leaves it out. Each resource a run leaves out is logged as `resource_stopped` with the agent or workflow, the resource's type and id, and the reason.
|
||
|
||
## 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.
|