Access policy and default role
The Access tab decides what a brand-new user gets the first time they log in to use your MCP server. Plus an exceptions table for the people who need more (or less) than the default.
What the UI does
Two sections:
- Default access. A checkbox to enable the policy, and a dropdown to pick the role granted to first-time users. Trigger is fixed at
first_successful_login. - Manual exceptions. A table of user-to-role assignments specific to this resource server. Each row carries a
SOURCEtag —MANUAL_ADMINfor ones an admin added by hand,DEFAULT_POLICYfor ones the policy created automatically.
The right-hand Policy preview panel shows what the role will grant: tool counts, scope counts, default role grants, viewer scopes.
Data model
Two tables back this:
resource_server_access_policies (one row per RS, unique on resource_server_id):
| Column | What it does |
|---|---|
enabled | If false, the policy is dormant. New users get nothing. |
default_role_id | FK to the role granted on first login. |
assignment_trigger | Always first_successful_login today. |
assignment_source | default_policy — distinguishes auto-grants from manual ones. |
role_bindings (the join table where the actual user-role assignments live):
| Column | What it does |
|---|---|
user_id, role_id, resource_server_id | The triple that identifies a grant. |
assignment_source | default_policy if created by the access policy on first login; manual if added by an admin. |
assignment_metadata | JSONB. Stores who created the row, when, and any audit notes. |
The assignment_source distinction matters because it tells you which bindings will survive policy changes (manual ones) versus which will be re-created on each first login (default_policy ones).
How the trigger fires
When a user authenticates against the AuthSec backend for the first time, the backend checks every resource_server_access_policies row with enabled = true. For each one, it creates a role_bindings row with assignment_source = 'default_policy'. The user is now permanently bound to that role for that RS — even if the policy is later disabled, the binding persists.
In other words: the policy is a creation rule, not an ongoing membership check. Disable the policy and existing users keep their bindings; new users start getting nothing.
Adding a manual exception
Use the Manual exceptions table → Add. Pick a user and a role. The row goes into role_bindings with assignment_source = 'manual'.
Manual rows take precedence over default policy: if a user has both a manual binding and a default-policy binding, the union of their scopes is what the SDK sees.
Removing a binding
Click the trash icon on a row. The row is hard-deleted from role_bindings. If you delete a default-policy binding for a user who logs in again, the policy will re-create it (assuming the policy is still enabled). To prevent re-creation, either disable the policy or create a manual binding with a no-scope role.
Roles and scopes — where they connect
Roles are containers for scopes. The viewer role auto-created on registration starts empty — you add scopes to it from the role editor. Scopes themselves come from the Define scopes step.
The runtime flow:
- User logs in → access policy grants them
viewerrole. - User authorizes an OAuth client, consents to scope
rs-xyz:tools:read. - The issued token's
scopeclaim is the intersection of (what the role grants) and (what the user consented to). - The Go SDK validates the token, sees
scope=rs-xyz:tools:read, allows tools mapped to that scope.
This means the role gates what the user can request; consent gates what they actually got.
What's not here today
The plan calls these out explicitly — they're roadmap, not bugs:
- Per-scenario simulation. "If user X uses client Y to call tool Z, what happens?" The Access tab can preview policy state, but not interactively simulate per-request. The backend endpoint for this isn't built yet.
- Bulk import for manual exceptions. Adding 50 users from a CSV — one at a time today.
- Time-bound exceptions. Exceptions don't expire. If you want to grant temporary access, you have to remember to delete the row.
Auth scheme
| Endpoint | Auth |
|---|---|
GET /authsec/resource-servers/:id/access-policy | Admin JWT |
PUT /authsec/resource-servers/:id/access-policy | Admin JWT |
POST /authsec/resource-servers/:id/role-bindings (manual) | Admin JWT |
DELETE /authsec/resource-servers/:id/role-bindings/:user_id | Admin JWT |
None of these are SDK-facing.
Related
- Map tools to scopes — the previous step
- Pre-register OAuth clients — the next step
- Monitor drift and runtime —
default_role_disableddrift events