Skip to content

SSO role sync strips existing users' roles when a mapped role has "Can edit users" off (SAML and OIDC) #38050

Open

Description

@swicken

Problem Statement

When an IdP group is mapped to a dotCMS role whose "Can edit users" flag (editUsers) is off, the role assignment throws during SSO login and the whole login fails. Because both SSO flows remove all of the user's roles before re-adding them, a user who already exists is left with their roles stripped. That includes their personal role, which is where directly granted permissions hang off. This happens with both OIDC (dotAuth OAuth) and SAML.

Impact:

  • Any admin can trigger it. Unticking "Can edit users" on a role that a group mapping points to silently strips the roles of every existing SSO user the next time each of them logs in. Under SAML they lose all roles. Under OIDC with "Sync all" they keep only the system and default roles. The roles come back only after the role is fixed and the user logs in again.
  • No useful error for the user. OIDC redirects to /?error=oauth+callback+failed, which renders a 404 page. SAML returns HTTP 400 and shows raw JSON in the browser: {"message":"Cannot alter users on this role. Name:<role>, id:<role id>"}.
  • New users are half-provisioned. The user row is created before the role step fails, and the user cannot log in until the role is fixed.
  • Hard to diagnose. Nothing reaches the error log for the existing-user case. It is recorded only as an INFO line from the security logger (DotBadRequestExceptionMapper : Cannot alter users on this role, or OAuthWebInterceptor - OAuth callback failed for OIDC).

Root cause:

  • The throwing call. RoleAPIImpl.addRoleToUser throws DotStateException("Cannot alter users on this role…") when editUsers=false.
  • OIDC path. OAuthHelper.applyBuildRolesStrategy calls removeAllRolesFromUser, then applySystemRoles, applyExtraRoles and applyProviderGroups, which leads to addRoleByKey and then addRoleToUser. Nothing catches the exception, so the callback fails after the removal.
  • SAML path. SAMLHelper.addRoles calls removeAllRolesFromUser, then handleRoles, addRolesFromIDP and addIDPRoles, which leads to UserHelper.addRole and then addRoleToUser. addRoles only catches DotDataException, and DotSamlResource has no handler, so the exception reaches the generic DotStateException mapper (400). SAML adds the IdP roles first, so it fails before re-adding anything.

Steps to Reproduce

Reproduced locally on dotcms/dotcms:26.10.12-01 with Keycloak 26.6.1 as both the OIDC and SAML IdP.

  1. In dotCMS, create a role (for example key keycloak-editors) with Can edit users ON.
  2. Configure dotAuth SSO on SYSTEM_HOST (OIDC or SAML) with role behavior Sync all, and a group mapping from an IdP group (for example dotcms-editors) to that role key. For SAML the role key must equal the group name, because SAML's default strategy maps group name to role key.
  3. Log in through SSO as an IdP user in that group. This works; the user gets DOTCMS_BACK_END_USER, the mapped role and their personal role.
  4. Edit the role and turn Can edit users OFF.
  5. Log in again as the same user.

Actual:

Flow Browser User's roles afterwards
OIDC, existing user /?error=oauth+callback+failed (404 page) only DOTCMS_BACK_END_USER
SAML, existing user HTTP 400, raw JSON "Cannot alter users on this role…" none (GET /api/v1/roles/users/<email> returns [])
SAML, new user HTTP 400, raw JSON user row created, no roles

Expected: the login succeeds, and the unassignable role is skipped with a WARN that names the role and the reason. The user keeps every role that could be assigned.

Acceptance Criteria

  • An IdP-mapped role that cannot be assigned (editUsers=false) is skipped with a WARN naming the role key and the reason, and the login completes, for both OIDC and SAML.
  • A failure while re-applying roles never leaves an existing user with fewer roles than the strategy would assign. Either the new role set is computed before anything is removed, or the remove and re-add run atomically.
  • The user's personal role (role key = user ID) is never removed by either flow's role sync.
  • A new user whose role assignment fails is not left half-provisioned: either the user is created with the roles that could be assigned, or creation is rolled back.
  • SAML login errors show a user-facing error page, not raw JSON from the ACS endpoint.
  • Tests cover OIDC and SAML with an existing user, a new user, and a mapped role with editUsers=false, under "Sync all" and "IdP groups only".

dotCMS Version

Reproduced on dotcms/dotcms:26.10.12-01 (Evergreen). The same code paths are present on main as of 2026-10-15.

Severity

High - Major functionality broken

Links

NA

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions