#36937 #36938: feat(roles): add role-membership endpoints (grant user, bulk remove users) - #37077
Merged
Merged
Conversation
…eparent * New v1 REST endpoint replacing DWR RoleAjax#updateRole for the Angular Roles & Tools portlet (epic #36909): updates name, key, description, can-grant flags and parent; null parentRoleId reparents to root (DWR parity) * Guards: 404 missing role/parent, 403 system/locked roles, 400 invalid name and reparent cycles (net-new corruption guard), 409 duplicate key/name * Auth: backend user + roles portlet + CMS admin, shared gate extracted and reused by POST /v1/roles (behavior unchanged, regression-tested) * RoleHelper promoted to @ApplicationScoped CDI with the update logic under @WrapInTransaction; response reuses RoleView/ResponseEntityRoleDetailView (same shape as GET /v1/roles/{roleid}) * 15 integration tests (TDD red->green), registered in MainSuite3a * Regenerated openapi.yaml
…th a test * OpenAPI description now spells out that PUT overwrites every field: omitted booleans reset to false, omitted roleKey/description are cleared, omitted parentRoleId reparents to root (DWR parity) * New IT testUpdateRole_fullReplace_omittedFieldsAreReset pins the contract so drift to merge/PATCH semantics is a deliberate, test-breaking change * Addresses claude[bot] review finding on PR #37012
… saves cannot poison the role cache Review fixes for PR #37012 (fabrizzio-dotCMS): * loadRoleById returns the cache-resident Role instance; the update previously mutated it in place before validation, so a rejected save (duplicate key/name, invalid name, cycle, missing parent) left phantom values in the local cache. Now: validate first, then copy the role (BeanUtils.copyProperties — the same mechanism RoleFactoryImpl.save uses) and save the copy * Rejection tests now pre-warm the cache (production-warm case: entries cached inside the failing transaction are rollback-evicted by CommitListenerCacheWrapper and mask the bug) and assert post-rejection that name/key/description survived — all five failed before the fix, 16/16 after * Replace hand-rolled ancestor walk with roleAPI.isParentRole * Drop dead CDI annotations on RoleHelper (nothing injects it; transactions are ByteBuddy-woven, not CDI-intercepted) * Align path template with sibling GET: {roleId} -> {roleid}; regenerated openapi * MainSuite3a: import instead of fully-qualified suite entry
Cascading delete (legacy RoleAPIImpl.delete parity): removes the role from all users, strips its permissions, detaches its layouts. Blocks only where legacy blocks: children (409), workflow-action Assign To references (409, pre-checked because delete() re-wraps the check into a generic failure), system/locked roles (403). Response reports usersAffected as the cascade blast radius via a new typed RoleDeletionView.
…ant user Grants a role to a user as a direct membership, replacing the DWR RoleAjax#addUserToRole path for the Angular Roles & Tools portlet (epic #36909). - Idempotent: granting an already-held role (direct or inherited) returns 200 and changes nothing (legacy RoleAPIImpl.addRoleToUser parity) - Already-holds check runs BEFORE the editUsers gate (legacy order), so re-grants stay 200 no-ops even after the role's membership is frozen (editUsers flipped to false) - Single grant gate: role's editUsers flag false -> 403 (the only failure legacy has; the issue's hierarchy-409 case does not exist in code) - Inherited-grant quirk documented in OpenAPI: membership inherits DOWN the role tree, so granting a role the user already inherits via a parent role is a no-op that still returns 200 - 404 via DoesNotExistException for missing role/user; auth via shared roles-portlet + CMS-admin gate - Path template {roleid} matching the convention set by the sibling GET/PUT endpoints - Typed immutable views (RoleUserGrantView with minimal user payload) returned directly per v2 REST patterns - Truthful audit logging: success lines only on actual grant, error lines on failure (deviation from DWR, which always logged success)
…ber removal Bulk-removes direct role memberships, replacing the DWR RoleAjax#removeUsersFromRole path for the Angular Roles & Tools portlet (epic #36909). DELETE with JSON body {userIds: [...]} — same pattern as the bulk category delete endpoint. - Partial-success semantics: once the role resolves the batch never fails as a whole; response reports removedUserIds plus skipped entries with reasons not_found / inherited / error (constants on the view, single source of truth with the OpenAPI enum) - skipped 'inherited' reports what legacy silently no-ops (0-row membership DELETE; the Dojo grid only offered checkboxes on direct rows) - userIds entries validated non-null/non-blank -> 400 (Jackson accepts null elements in a Set<String>; unchecked they would 500 mid-batch) - Direct-membership check per user via the role cache (loadRolesForUser) — scales with batch size, not role membership size, and reads at removal time - Deliberately no batch-level transaction: partial success requires per-user commits (each roleAPI.removeRoleFromUser is already transactional); documented in the helper javadoc - jdkOnly immutables so the view's list properties inline in the OpenAPI schema instead of binding to the shared ImmutableListString component (which would clobber other endpoints' docs) - Path template {roleid} matching the convention set by the sibling GET/PUT endpoints - Per-removal ActivityLogger/AdminLogger/SecurityLogger lines; error-path audit lines; missing role -> 404; auth via shared roles-portlet + CMS-admin gate
…-role-update # Conflicts: # dotcms-integration/src/test/java/com/dotcms/MainSuite3a.java
9 tasks
hassandotcms
added a commit
that referenced
this pull request
Aug 17, 2026
…dpoint
Aligns GET /{roleid}/users with the existing GET /{roleid}, PUT
/{roleid} (PR #37012, where review feedback settled on lowercase) and
the membership endpoints POST /{roleid}/users/{userId} and DELETE
/{roleid}/users (PR #37077). Without this, the regenerated openapi.yaml
would end up with /v1/roles/{roleId}/users and /v1/roles/{roleid}/users
as two entries for the same real path once both branches merge, which
OpenAPI treats as ambiguous.
Template rename only: real URLs, clients and tests are unaffected.
openapi.yaml regenerated.
hassandotcms
marked this pull request as ready for review
August 17, 2026 16:57
hassandotcms
changed the base branch from
main
to
36936-roles-api-put-role-update
August 17, 2026 19:32
…e' into 36937-36938-roles-api-role-membership
Contributor
|
Claude finished @hassandotcms's task in 2m 29s —— View job Code Review — role-membership endpointsReviewed
New Issues
Nothing blocking — the two prior review threads ( |
…pi-role-membership # Conflicts: # dotCMS/src/main/java/com/dotcms/rest/api/v1/system/role/RoleHelper.java # dotCMS/src/main/java/com/dotcms/rest/api/v1/system/role/RoleResource.java # dotcms-integration/src/test/java/com/dotcms/rest/api/v1/system/role/RoleResourceIntegrationTest.java
6 tasks
jcastro-dotcms
approved these changes
Aug 18, 2026
… convention
PUT and GET on the same segment already use {roleid} (aligned during PR #37012 review); this makes DELETE consistent so all /v1/roles/{roleid} operations share one path entry in the OpenAPI spec. URL shape is unchanged - template name only. Regenerated openapi.yaml.
hassandotcms
enabled auto-merge
August 18, 2026 21:22
7 tasks
This was referenced Aug 20, 2026
Open
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds the two role-membership mutations for the Angular Roles & Tools portlet migration (epic #36909), replacing DWR
RoleAjax#addUserToRole/#removeUsersFromRole.Closes #36937, closes #36938. #36939
POST /v1/roles/{roleid}/users/{userId} — grant user to role
Grants a direct membership. Returns
{granted, roleId, user: {userId, email, fullName}}.editUsers=falserole → 403 (covers workflow/system roles). Missing role/user → 404.DELETE /v1/roles/{roleid}/users — bulk remove members
JSON body
{userIds: [...]}(same pattern as bulk category delete).{removedUserIds, skipped: [{userId, reason: not_found|inherited|error}]}.inheritedreports what legacy silently no-ops.userIds→ 400. Missing role → 404.Both
Testing
22 new integration tests in
RoleResourceIntegrationTest(MainSuite3a), TDD red→green.Security review: no critical/high findings.
This PR fixes: #36937