Manual role changes now blocked at API level when provisioning is active
The n8n API previously allowed role changes to bypass UI restrictions, creating a security gap when expression-based mapping or SSO provisioning was enabled. A single API call could override what the interface locked down.
When n8n instance or project roles are being managed automatically—either by an SSO provider via SCIM or by expression-based role mapping rules—the UI correctly prevented admins from manually changing roles. The API did not.
Anyone with API access could send a PATCH request to change a user or project role regardless of what the UI showed. This was a security gap where the frontend enforcement could be bypassed entirely with a single curl command.
The fix adds guards to both API endpoints: PATCH /users/:id/role and PATCH /projects/:projectId/users/:userId. When provisioning is active (either SSO or expression mapping), these endpoints now return 403 Forbidden with a clear message explaining that roles are managed automatically. Two new helper methods—isInstanceRoleManaged() and isProjectRoleManaged()—check both provisioning modes in a single call, keeping the logic consistent across the codebase.
The frontend updates differentiate between SSO provisioning and expression mapping with distinct banners, ensuring admins understand why controls are disabled. The remove-member action is also hidden when provisioning is active.
This closes a gap between what the UI enforced and what the API permitted.
View Original GitHub Description
Summary
Prevents manual role changes when instance or project roles are being managed automatically — either by an SSO provider via SCIM/provisioning, or by expression-based role mapping rules.
Before: The UI disabled role editing in both cases, but the underlying API endpoints (PATCH /users/:id/role and PATCH /projects/:projectId/users/:userId) accepted requests regardless, meaning anyone with API access could bypass the restriction.
After: Both endpoints return 403 Forbidden when the relevant provisioning mode is active. The UI and API are now consistent.
Changes
provisioning.service.ee.ts— Two new public methods (isInstanceRoleManaged,isProjectRoleManaged) that combine the SSO provisioning and expression-mapping flags into a single guard checkusers.controller.ts— Guard onPATCH /users/:id/rolethat throwsForbiddenErrorwhen instance roles are managedproject.controller.ts— Guard onPATCH /projects/:projectId/users/:userIdthat throwsForbiddenErrorwhen project roles are managed- Frontend —
SettingsUsersView.vueandProjectSettings.vueupdated to show distinct info banners for expression mapping vs SSO provider, and to disable editing for both; new i18n strings added provisioning.ts(rest-api-client) —scopesUseExpressionMappingadded toProvisioningConfiginterface
Related Linear tickets
https://linear.app/n8n/issue/IAM-501
Testing
Prerequisites
- An n8n instance with the
N8N_ENV_FEAT_ROLE_MAPPING_STRATEGY=trueenv var set (for expression mapping tests) - An owner or admin account to obtain a valid auth cookie/token
jqinstalled for pretty-printing JSON responses
UI Tests
1. Expression-based mapping — Instance roles (Settings > Users)
- Set
N8N_ENV_FEAT_ROLE_MAPPING_STRATEGY=trueand ensurescopesUseExpressionMappingis enabled viaPATCH /sso/provisioning/config - Navigate to Settings > Users
- Expected: A blue info banner reads "Instance roles are managed automatically by expression-based role mapping rules. Manual role changes are disabled."
- Expected: No role dropdown is visible or interactable next to any user row
- Disable expression mapping, reload — role dropdowns should reappear
2. Expression-based mapping — Project roles (Project > Settings)
- With expression mapping still active, open any team project and navigate to Settings > Members
- Expected: A blue info banner reads "Project roles are managed automatically by expression-based role mapping rules. Manual role changes are disabled."
- Expected: The member search input is disabled and no role dropdowns are editable
- Expected: The "Add member" control is not functional
3. SSO provisioning — Instance roles (Settings > Users)
- Disable expression mapping; enable SSO (SAML/OIDC) and set
scopesProvisionInstanceRole: trueviaPATCH /sso/provisioning/config - Navigate to Settings > Users
- Expected: Banner reads "User management and instance roles are controlled by your SSO provider..." (the existing SSO copy, not the expression mapping copy)
- Expected: Role dropdowns are disabled
4. SSO provisioning — Project roles (Project > Settings)
- With
scopesProvisionProjectRoles: true, open a team project's Settings - Expected: Banner reads "User management and project roles are controlled by your SSO provider..."
- Expected: Member role dropdowns and the add-member input are disabled
5. No provisioning active (baseline)
- Ensure both expression mapping and SSO provisioning are disabled
- Navigate to Settings > Users and a team project's Settings
- Expected: No info banners shown, all role controls are interactive
API Tests (cURL)
Replace the placeholders:
N8N_HOST— your instance URL, e.g.http://localhost:5678AUTH_COOKIE— value of then8n-authcookie from a logged-in owner session (grab from browser DevTools > Application > Cookies)TARGET_USER_ID— ID of any non-owner user (from Settings > Users URL orGET /users)PROJECT_ID— ID of a team project (from the project URL)PROJECT_MEMBER_ID— ID of a user who is a member of that project
Setup: enable expression-based mapping
curl -s -X PATCH "$N8N_HOST/rest/sso/provisioning/config" \
-H "Content-Type: application/json" \
-H "Cookie: n8n-auth=$AUTH_COOKIE" \
-d '{
"scopesUseExpressionMapping": true,
"scopesProvisionInstanceRole": false,
"scopesProvisionProjectRoles": false,
"scopesName": "",
"scopesInstanceRoleClaimName": "",
"scopesProjectsRolesClaimName": ""
}' | jq .
Test A: Instance role change blocked (expression mapping)
curl -s -X PATCH "$N8N_HOST/rest/users/$TARGET_USER_ID/role" \
-H "Content-Type: application/json" \
-H "Cookie: n8n-auth=$AUTH_COOKIE" \
-d '{"newRoleName": "global:admin"}' | jq .
Expected response (403):
{
"status": "error",
"message": "Instance roles are managed automatically and cannot be changed manually"
}
Test B: Project role change blocked (expression mapping)
curl -s -X PATCH "$N8N_HOST/rest/projects/$PROJECT_ID/users/$PROJECT_MEMBER_ID" \
-H "Content-Type: application/json" \
-H "Cookie: n8n-auth=$AUTH_COOKIE" \
-d '{"role": "project:editor"}' | jq .
Expected response (403):
{
"status": "error",
"message": "Project roles are managed automatically and cannot be changed manually"
}
Setup: switch to SSO provisioning mode
curl -s -X PATCH "$N8N_HOST/rest/sso/provisioning/config" \
-H "Content-Type: application/json" \
-H "Cookie: n8n-auth=$AUTH_COOKIE" \
-d '{
"scopesUseExpressionMapping": false,
"scopesProvisionInstanceRole": true,
"scopesProvisionProjectRoles": true,
"scopesName": "",
"scopesInstanceRoleClaimName": "",
"scopesProjectsRolesClaimName": ""
}' | jq .
Test C: Instance role change blocked (SSO provisioning)
curl -s -X PATCH "$N8N_HOST/rest/users/$TARGET_USER_ID/role" \
-H "Content-Type: application/json" \
-H "Cookie: n8n-auth=$AUTH_COOKIE" \
-d '{"newRoleName": "global:admin"}' | jq .
Expected response (403):
{
"status": "error",
"message": "Instance roles are managed automatically and cannot be changed manually"
}
Test D: Project role change blocked (SSO provisioning)
curl -s -X PATCH "$N8N_HOST/rest/projects/$PROJECT_ID/users/$PROJECT_MEMBER_ID" \
-H "Content-Type: application/json" \
-H "Cookie: n8n-auth=$AUTH_COOKIE" \
-d '{"role": "project:editor"}' | jq .
Expected response (403):
{
"status": "error",
"message": "Project roles are managed automatically and cannot be changed manually"
}
Test E: Role changes allowed when provisioning is disabled (baseline)
# Disable all provisioning
curl -s -X PATCH "$N8N_HOST/rest/sso/provisioning/config" \
-H "Content-Type: application/json" \
-H "Cookie: n8n-auth=$AUTH_COOKIE" \
-d '{
"scopesUseExpressionMapping": false,
"scopesProvisionInstanceRole": false,
"scopesProvisionProjectRoles": false,
"scopesName": "",
"scopesInstanceRoleClaimName": "",
"scopesProjectsRolesClaimName": ""
}' | jq .
# Instance role change — should succeed
curl -s -X PATCH "$N8N_HOST/rest/users/$TARGET_USER_ID/role" \
-H "Content-Type: application/json" \
-H "Cookie: n8n-auth=$AUTH_COOKIE" \
-d '{"newRoleName": "global:admin"}' | jq .
Expected response (200):
{
"data": { "success": true }
}
Review / Merge checklist
- PR title and summary are descriptive. (conventions)
- Docs updated or follow-up ticket created.
- Tests included.
- PR labeled with
Backport to Beta,Backport to Stable, orBackport to v1if urgent.