Skip to content

Commit 95b96ff

Browse files
committed
Bootstrap registry v1 docs
Signed-off-by: Radoslav Dimitrov <radoslav@stacklok.com>
1 parent 202f5f2 commit 95b96ff

10 files changed

Lines changed: 634 additions & 101 deletions

File tree

docs/toolhive/guides-registry/authentication.mdx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ OAuth mode to protect your registry. You can configure authentication to fit
1010
different deployment scenarios, from development environments to production
1111
deployments with enterprise identity providers.
1212

13+
:::tip[Looking for authorization?]
14+
15+
This page covers **authentication** (verifying caller identity). For
16+
**authorization** (controlling what callers can do), including role-based access
17+
control and claims-based scoping, see [Authorization](./authorization.mdx).
18+
19+
:::
20+
1321
## Authentication modes
1422

1523
The server supports two authentication modes configured via the required `auth`
@@ -547,6 +555,8 @@ If tokens from some providers work but others don't:
547555

548556
## Next steps
549557

558+
- [Configure authorization](./authorization.mdx) to set up role-based access
559+
control and claims-based scoping
550560
- [Set up the database](./database.mdx) for production storage and migrations
551561
- [Configure telemetry](./telemetry-metrics.mdx) for distributed tracing and
552562
metrics collection
Lines changed: 357 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,357 @@
1+
---
2+
title: Authorization
3+
description:
4+
How to configure role-based access control and claims-based authorization for
5+
the Registry Server
6+
---
7+
8+
The Registry server provides a claims-based authorization model that controls
9+
who can manage sources, registries, and entries. Authorization builds on top of
10+
[authentication](./authentication.mdx) — you need OAuth authentication enabled
11+
before configuring authorization.
12+
13+
## How authorization works
14+
15+
Authorization in the Registry server operates at two levels:
16+
17+
1. **Role-based access control (RBAC)**: Determines which admin operations a
18+
caller can perform (manage sources, manage registries, publish/delete
19+
entries).
20+
2. **Claims-based scoping**: Limits visibility and access to specific sources,
21+
registries, and entries based on key-value claims attached to both resources
22+
and callers.
23+
24+
When a caller makes an API request, the server:
25+
26+
1. Extracts the caller's claims from their JWT token
27+
2. Resolves the caller's roles based on those claims and the `authz`
28+
configuration
29+
3. Checks whether the caller's role permits the operation
30+
4. Checks whether the caller's claims satisfy the resource's claims
31+
32+
```mermaid
33+
flowchart LR
34+
JWT["JWT token"] --> Claims["Extract claims"]
35+
Claims --> Roles["Resolve roles"]
36+
Roles --> RoleCheck{"Role\npermitted?"}
37+
RoleCheck -->|Yes| ClaimCheck{"Claims\nsatisfied?"}
38+
RoleCheck -->|No| Deny403["403 Forbidden"]
39+
ClaimCheck -->|Yes| Allow["Allow"]
40+
ClaimCheck -->|No| Deny403
41+
```
42+
43+
## Configure roles
44+
45+
Define roles in the `auth.authz.roles` section of your configuration file. Each
46+
role maps to a list of claim rules — if a caller's JWT claims match any rule in
47+
the list, they are granted that role.
48+
49+
```yaml title="config-authz.yaml"
50+
auth:
51+
mode: oauth
52+
oauth:
53+
resourceUrl: https://registry.example.com
54+
providers:
55+
- name: keycloak
56+
issuerUrl: https://keycloak.example.com/realms/mcp
57+
audience: registry-api
58+
# highlight-start
59+
authz:
60+
roles:
61+
superAdmin:
62+
- role: 'super-admin'
63+
manageSources:
64+
- org: 'acme'
65+
role: 'admin'
66+
manageRegistries:
67+
- org: 'acme'
68+
role: 'admin'
69+
manageEntries:
70+
- role: 'writer'
71+
# highlight-end
72+
```
73+
74+
### Available roles
75+
76+
| Role | Grants access to |
77+
| ------------------ | ------------------------------------------------------------- |
78+
| `superAdmin` | All operations; bypasses all claim checks |
79+
| `manageSources` | Create, update, delete, and list sources via the admin API |
80+
| `manageRegistries` | Create, update, delete, and list registries via the admin API |
81+
| `manageEntries` | Publish and delete MCP server versions and skills |
82+
83+
### Role rule matching
84+
85+
Each role is defined as a list of claim maps. A caller is granted the role if
86+
their JWT claims match **any** map in the list (OR logic). Within a single map,
87+
**all** key-value pairs must match (AND logic).
88+
89+
```yaml title="Example: grant manageSources to org admins OR platform leads"
90+
authz:
91+
roles:
92+
manageSources:
93+
# Rule 1: any admin in the acme org
94+
- org: 'acme'
95+
role: 'admin'
96+
# Rule 2: anyone with the platform-lead role
97+
- role: 'platform-lead'
98+
```
99+
100+
Claim values can be strings or arrays. When a JWT claim is an array (for
101+
example, `roles: ["admin", "writer"]`), the server checks whether any element
102+
matches the required value. A rule with `role: "admin"` would match this JWT
103+
because `"admin"` is one of the array elements.
104+
105+
### Super-admin role
106+
107+
The `superAdmin` role bypasses **all** claim checks across the entire server. A
108+
super-admin can:
109+
110+
- Access any registry regardless of its claims
111+
- See all entries regardless of source or entry claims
112+
- Manage any source or registry, even those with claims outside their JWT
113+
- Publish and delete entries without claim validation
114+
115+
Use this role sparingly and only for platform operators who need unrestricted
116+
access.
117+
118+
## Configure claims on sources and registries
119+
120+
Claims are key-value pairs attached to sources and registries in your
121+
configuration file. They act as access boundaries — only callers whose JWT
122+
claims satisfy the resource's claims can access it.
123+
124+
### Source claims
125+
126+
Claims on a source are **inherited by all entries** during sync. This means
127+
every MCP server or skill ingested from that source carries the source's claims.
128+
129+
```yaml title="config-source-claims.yaml"
130+
sources:
131+
- name: platform-tools
132+
format: toolhive
133+
git:
134+
repository: https://github.com/acme/platform-tools.git
135+
branch: main
136+
path: registry.json
137+
syncPolicy:
138+
interval: '30m'
139+
# highlight-start
140+
claims:
141+
org: 'acme'
142+
team: 'platform'
143+
# highlight-end
144+
145+
- name: data-tools
146+
format: toolhive
147+
git:
148+
repository: https://github.com/acme/data-tools.git
149+
branch: main
150+
path: registry.json
151+
syncPolicy:
152+
interval: '30m'
153+
claims:
154+
org: 'acme'
155+
team: 'data'
156+
```
157+
158+
With this configuration:
159+
160+
- Entries from `platform-tools` are visible only to callers with `org: "acme"`
161+
**and** `team: "platform"` in their JWT
162+
- Entries from `data-tools` are visible only to callers with `org: "acme"`
163+
**and** `team: "data"` in their JWT
164+
- A super-admin sees all entries regardless of claims
165+
166+
### Registry claims
167+
168+
Claims on a registry act as an **access gate** for the consumer API. Before
169+
returning any data from a registry's endpoints, the server checks that the
170+
caller's JWT claims satisfy the registry's claims.
171+
172+
```yaml title="config-registry-claims.yaml"
173+
registries:
174+
- name: platform
175+
sources: ['platform-tools']
176+
# highlight-start
177+
claims:
178+
org: 'acme'
179+
team: 'platform'
180+
# highlight-end
181+
182+
- name: public
183+
sources: ['community-tools']
184+
# No claims — accessible to all authenticated users
185+
```
186+
187+
A caller who requests `GET /platform/v0.1/servers` must have JWT claims that
188+
include `org: "acme"` and `team: "platform"`. Otherwise, the server returns
189+
`403 Forbidden`.
190+
191+
### Claim containment
192+
193+
The server uses **containment** (superset check) for claim validation: the
194+
caller's claims must be a superset of the resource's claims. For example:
195+
196+
| Resource claims | Caller JWT claims | Result |
197+
| --------------------------------- | --------------------------------- | ------- |
198+
| `{org: "acme"}` | `{org: "acme", team: "platform"}` | Allowed |
199+
| `{org: "acme", team: "platform"}` | `{org: "acme"}` | Denied |
200+
| `{}` (no claims) | `{org: "acme"}` | Allowed |
201+
| `{org: "acme"}` | `{org: "contoso"}` | Denied |
202+
203+
Resources with no claims are accessible to all authenticated callers.
204+
205+
## Claims on published entries
206+
207+
When you publish an MCP server version or skill to a managed source, you can
208+
attach claims to the entry. The server enforces two rules:
209+
210+
1. **Publish claims must be a subset of the publisher's JWT claims.** You cannot
211+
publish entries with broader visibility than your own identity allows. For
212+
example, if your JWT has `{org: "acme", team: "platform"}`, you can publish
213+
entries with `{org: "acme", team: "platform"}` but not with `{org: "acme"}`
214+
alone (which would be visible to all teams).
215+
216+
2. **Subsequent versions must have the same claims as the first.** Once you
217+
publish the first version of an entry with specific claims, all future
218+
versions must carry the exact same claims. This prevents accidentally
219+
narrowing or broadening visibility across versions.
220+
221+
```bash title="Publish a server with claims"
222+
curl -X POST \
223+
https://registry.example.com/default/v0.1/publish \
224+
-H "Authorization: Bearer $TOKEN" \
225+
-H "Content-Type: application/json" \
226+
-d '{
227+
"name": "my-server",
228+
"version": "1.0.0",
229+
"url": "https://mcp.example.com/my-server",
230+
"description": "Team-scoped MCP server",
231+
"claims": {
232+
"org": "acme",
233+
"team": "platform"
234+
}
235+
}'
236+
```
237+
238+
## Admin API claim scoping
239+
240+
When authorization is enabled, the admin API endpoints for managing sources and
241+
registries are also scoped by claims:
242+
243+
- **List sources/registries**: Only returns resources whose claims the caller's
244+
JWT satisfies.
245+
- **Get source/registry by name**: Returns `404 Not Found` (not `403`) when the
246+
caller's claims don't match — this prevents information disclosure about
247+
resources the caller cannot access.
248+
- **Create source/registry**: The request claims must be a subset of the
249+
caller's JWT claims.
250+
- **Update/delete source/registry**: The caller's JWT claims must satisfy the
251+
existing resource's claims.
252+
253+
## Anonymous mode
254+
255+
When authentication is set to `anonymous`, all authorization checks are
256+
bypassed. There are no JWT claims to validate, so all sources, registries, and
257+
entries are accessible without restriction. This is suitable for development and
258+
testing environments only.
259+
260+
## Complete example
261+
262+
This example shows a multi-team setup with full RBAC and claims-based scoping:
263+
264+
```yaml title="config-multi-tenant.yaml"
265+
sources:
266+
- name: platform-tools
267+
format: toolhive
268+
git:
269+
repository: https://github.com/acme/platform-tools.git
270+
branch: main
271+
path: registry.json
272+
syncPolicy:
273+
interval: '30m'
274+
claims:
275+
org: 'acme'
276+
team: 'platform'
277+
278+
- name: data-tools
279+
format: toolhive
280+
git:
281+
repository: https://github.com/acme/data-tools.git
282+
branch: main
283+
path: registry.json
284+
syncPolicy:
285+
interval: '30m'
286+
claims:
287+
org: 'acme'
288+
team: 'data'
289+
290+
- name: shared
291+
managed: {}
292+
293+
registries:
294+
- name: platform
295+
sources: ['platform-tools', 'shared']
296+
claims:
297+
org: 'acme'
298+
team: 'platform'
299+
300+
- name: data
301+
sources: ['data-tools', 'shared']
302+
claims:
303+
org: 'acme'
304+
team: 'data'
305+
306+
auth:
307+
mode: oauth
308+
oauth:
309+
resourceUrl: https://registry.example.com
310+
providers:
311+
- name: keycloak
312+
issuerUrl: https://keycloak.example.com/realms/mcp
313+
audience: registry-api
314+
authz:
315+
roles:
316+
superAdmin:
317+
- role: 'super-admin'
318+
manageSources:
319+
- org: 'acme'
320+
role: 'admin'
321+
manageRegistries:
322+
- org: 'acme'
323+
role: 'admin'
324+
manageEntries:
325+
- role: 'writer'
326+
```
327+
328+
With this configuration:
329+
330+
- **Platform team members** (JWT with `org: "acme"`, `team: "platform"`) can
331+
access the `platform` registry and see entries from `platform-tools` and
332+
`shared`.
333+
- **Data team members** (JWT with `org: "acme"`, `team: "data"`) can access the
334+
`data` registry and see entries from `data-tools` and `shared`.
335+
- **Writers** (JWT with `role: "writer"`) can publish to the `shared` managed
336+
source.
337+
- **Admins** (JWT with `org: "acme"`, `role: "admin"`) can manage sources and
338+
registries within the `acme` org.
339+
- **Super-admins** (JWT with `role: "super-admin"`) can access and manage
340+
everything.
341+
342+
Entries published to the `shared` source without claims are visible through any
343+
registry that includes it, subject only to the registry-level claims gate. To
344+
restrict visibility further, attach claims when
345+
[publishing entries](#claims-on-published-entries).
346+
347+
## Next steps
348+
349+
- [Configure authentication](./authentication.mdx) to set up OAuth providers
350+
- [Configure sources and registries](./configuration.mdx) to set up your data
351+
sources
352+
- [Manage skills](./skills.mdx) to publish and discover reusable skills
353+
354+
## Related information
355+
356+
- [Registry server introduction](./intro.mdx) - architecture and features
357+
overview

0 commit comments

Comments
 (0)