Files
DocsGPT/docs/content/Deploying/Access-Control.mdx
T
arc53-machine da75845fcf Document sharing agents and whose access what they use runs with
A new "Sharing agents and what they use" section covers viewers and
editors, whose access each tool, source and prompt runs with as the share
dialog labels it, sponsors, stopped resources, what API, widget and
public-link users can't do without the write allowlist, the wiki switch
and research steps. The sharing rules now mention the editor switches and
member-mode tools, the guardrails page no longer says editors can't
change guardrails, the connector guide uses the new tool share labels,
and related pages link to the section.
2026-09-29 17:35:17 +01:00

258 lines
21 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, unless they turn on **Editors can share** in the share dialog's **Access settings**.
- 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 unless the owner turns on the matching switch.
- Shared tools run server-side with the **owner's** credentials, or with each member's own account when a connected tool is shared that way; 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)).
### Sharing agents and what they use
Sharing an agent lets people use it; it doesn't share its tools, sources or prompt. Those stay yours, and people reach them only through the agent.
- **Viewers** chat with the agent. They don't see its configuration, and they never open its share dialog.
- **Editors** open its edit page and change it: its instructions, model, tools, sources and prompt, and its [guardrails](/Agents/guardrails) and limits. They can share it or delete it only when you turn on **Editors can share** or **Editors can delete**.
Every tool, source and prompt on the agent runs with someone's access, the same for everyone who uses the agent. The agent's share dialog lists them under **What this agent uses**, for you and its editors. For a workflow agent, the list also includes the tools and sources on its nodes.
| The item | Runs with | The list says |
| --- | --- | --- |
| Yours, or shared with you | Your access | **Your access** (an editor sees **The owner's access**) |
| Added by an editor, and you can't use it | The editor's access; they are its [sponsor](#resources-an-editor-adds-to-someone-elses-agent) | **dana@example.com's access** |
| A connected tool shared as **Your account** | That account on the service | **Your Notion account**, or **dana@example.com's Notion account** for a teammate's tool |
| A connected tool shared as **Each person's own** | The account of whoever uses the agent; they connect it the first time | **Each person's own Notion account** |
An editor becomes a sponsor only after confirming who will reach the resource through the agent. A resource whose sponsor or owner loses access stops running and is marked **Stopped** in the list, with the reason (see [When a resource stops working](#when-a-resource-stops-working)).
Some runs are limited, because nobody can approve an action for you there:
- **API key, website widget and public link.** A write action on your connected accounts or saved credentials runs only if you allow it under **Access details > Actions API, widget and public-link users can take as you**. Only the owner can change that list. The share dialog marks tools with actions that aren't allowed as **No API changes**. On a tool shared as **Each person's own**, public-link users act with their own account and approve their own writes. See [Agents used through an API key](/Guides/Connectors#agents-used-through-an-api-key).
- **Wikis.** API and widget users edit a wiki only when its owner turns on **Let API and widget users edit this wiki** in its **Wiki settings**. Public-link users edit only wikis they can edit themselves, and approve each edit (see [Wiki sources](/Sources/Wiki-sources#edits-from-the-api-widget-and-public-links)).
- **Research agents.** A research step can't stop to ask, so it skips any action that would need approval or a connection, and any write the caller may not make on your accounts. The step is told why and carries on (see [Research Agent](/Agents/basics#3-research-agent)).
### 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 of a connected tool), `can_confirm` and `can_reconnect`. A running tool also has `credential_mode` (`owner` or `member` for a connected tool, else `null`), `account` (whose account an `owner`-mode connection acts as) and `owner_credential_writes` (its write actions on credentials its owner stored, which the API write allowlist covers). 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.