Skip to content

#37070 #37071: feat(roles): add GET /v1/roles/{roleid}/users and childCount/userCount on RoleView - #37078

Open
hassandotcms wants to merge 15 commits into
mainfrom
37070-37071-roles-users-endpoint-and-counts
Open

#37070 #37071: feat(roles): add GET /v1/roles/{roleid}/users and childCount/userCount on RoleView#37078
hassandotcms wants to merge 15 commits into
mainfrom
37070-37071-roles-users-endpoint-and-counts

Conversation

@hassandotcms

Copy link
Copy Markdown
Member

What

Backend for the Angular Roles and Tools portlet (epic #36909):

Prerequisite fix

UserFactoryImpl.appendRoleFilter matched roles by role_key, so a role without a key matched no users. The EXISTS subquery now matches ur.role_id, with a defensive key-to-id fallback for hand-built Role objects from external UserAPI callers. Affects /v1/users/filter?roleKey= and /v1/users/loginAsData: behavior preservation is demonstrated, not assumed. Regression tests were written and green against the previous SQL, then carried unchanged across the change.

Tests

22 new integration tests (TDD, red before each implementation), all green:

  • RoleResourceCountsIntegrationTest (8): counts per endpoint, direct-only semantics, batch aggregate
  • RoleResourceUsersIntegrationTest (8): emails present, keyless role (the headline regression), no inheritance, user-role self-grant, filter, pagination, 404/403
  • role-filter tests folded into UserAPITest and UserResourceIntegrationTest next to existing coverage
  • pre-existing testGetUsersByNameFilteredByRole re-validated against the new SQL

Fixes #37070, Fixes #37071

Adds two int fields to RoleView so the roles tree can render the
folder/leaf icon and the user-count badge without lazy expands:

- childCount derives from the already-loaded Role.roleChildren (null
  treated as 0), zero extra queries
- userCount resolves through the new RoleAPI.countUsersByRoleIds
  aggregate: one grouped users_cms_roles query per response, chunked
  IN lists, direct grants only (inherited grants excluded)
- RoleHelper.toRoleViews collects all role ids (parents plus hydrated
  children), runs the single count query, then builds the views;
  loadRootRoles, loadRoleByRoleId and loadUserRoles now route through
  it
- counts are computed per response and never stored on cached Role
  objects

Covered by RoleResourceCountsIntegrationTest (8 tests, registered in
MainSuite2b).
…e_key (#37070)

UserFactoryImpl.appendRoleFilter bound role.getRoleKey() into a
role_key IN (...) subquery, so a role without a roleKey bound null and
matched no users. This blocked the upcoming GET /v1/roles/{roleId}/users
endpoint for keyless roles.

The EXISTS subquery now matches ur.role_id IN (...) binding the role id
(dropping the cms_role join), shared by getUsersByName and
getCountUsersByName. Ids are resolved defensively: a hand-built Role
carrying only a roleKey is resolved by key so external UserAPI callers
keep working.

Behavior preservation is demonstrated, not assumed: the five regression
tests in UserRoleFilterRegressionIntegrationTest (roleKey filtering on
/v1/users/filter, multi-key union, list/count consistency, the back-end
user role param loginAsData passes, and key-only Role objects) were
written and green against the previous SQL, then carried unchanged
across this change. The keyless-role test was red before and is green
now.
…d users (#37070)

New endpoint returning the paginated list of users directly granted a
role, keyed by role id, using the standard user serialization with
emailAddress. Closes the gap where the Users tab showed empty emails
for roles without a roleKey (the FE had to combine
/v1/users/filter?roleKey= and rolehierarchyanduserroles, which returns
roles instead of users).

- reuses UserPaginator through the non-deprecated
  PaginationUtil.getPageView (ResponseEntityPaginatedDataView)
- direct grants only; inheritance stays a client concern via the
  ancestor walk on RoleView.parent
- user-roles served uniformly (self-grant row)
- gated behind requiredBackendUser plus the roles portlet since the
  response carries user PII
- 404 via DoesNotExistException for missing roles
- openapi.yaml regenerated (additive: new path plus required
  childCount/userCount on RoleView from the previous commit)

Covered by RoleResourceUsersIntegrationTest (8 tests, registered in
MainSuite2b).
…es (#37070)

Dissolves UserRoleFilterRegressionIntegrationTest into the classes that
already own these concerns:

- API-level tests (keyless role, key-only Role object) move next to the
  existing testGetUsersByNameFilteredByRole in UserAPITest, following
  its javadoc and try/finally cleanup style
- REST-level tests (filter by roleKey, multi-key union) move into
  UserResourceIntegrationTest
- two tests are dropped as duplicates of the already existing
  testGetUsersByNameFilteredByRole coverage (roles-param count
  consistency and back-end role matching)

All relocated tests plus the pre-existing role-filter test ran green
against the role_id-based filter.
Two pre-review findings:

- RoleHelper.toRoleViews now skips (with a warning) a child role id that
  no longer resolves instead of passing null into the RoleView
  constructor and throwing an NPE; the same latent NPE existed in the
  old inline loops
- UserFactoryImpl.resolveRoleId now propagates a DotDataException from
  the key-to-id lookup as DotRuntimeException instead of swallowing it;
  a role that simply does not resolve still matches no users, but an
  infrastructure failure no longer masquerades as an empty result

All 23 affected integration tests re-ran green.
…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.
@github-actions github-actions Bot added the Area : Backend PR changes Java/Maven backend code label Aug 17, 2026
@claude

claude Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Claude finished @hassandotcms's task in 2m 48s —— View job


Claude Code Review

  • Read changed production files
  • Analyze for bugs, security, conventions
  • Post review

I reviewed the diff against origin/main, focusing on the count aggregate, the new users endpoint, and the appendRoleFilter SQL change.

New Issues

No issues found. The implementation is clean and well-tested. Notes from verification:

  • RoleFactoryImpl.countUsersByRoleIds SQL is safe — the only concatenated fragments are ? placeholders (map(id -> "?")) and DbConnectionFactory.getDBFalse() (a DB constant, not user input). All values (chunk ids, User.DEFAULT) are bound via addParam. The visibility filters (system/anonymous/companyid <> DEFAULT/delete_in_progress) exactly mirror UserFactoryImpl.getUsersByName, so userCount stays consistent with the users listing total. ✅
  • loadUsersByRoleId direction handlingOrderDirection.valueOf(direction) throwing on a bad value is not a 500: IllegalArgumentExceptionMapper maps it to 400, matching the documented response and the existing /v1/users/filter behavior. ✅
  • Permission gating — the new endpoint requires a backend user + roles portlet before returning PII. 404 on missing role via DoesNotExistException. ✅
  • N+1 avoided for countsRoleHelper.toRoleViews collects all parent+child ids and resolves counts in one aggregated (chunked) query. The old 2-arg RoleView constructor has no remaining callers (build compiles). ✅
  • appendRoleFilter change — now matches ur.role_id with a defensive role_key → id fallback in resolveRoleId; a lookup failure propagates as DotRuntimeException rather than silently returning an empty match. Behavior for unresolvable roles (null → matches nothing) is preserved.

Resolved

  • RoleFactoryImpl.java:599 — Semgrep CUSTOM_INJECTION-2 findings are false positives (confirmed above and by the author); all values are parameter-bound.

Other

  • The reviewer request from @dario-daza (apply class-level cleanup to RoleResourceCountsIntegrationTest, RoleResourceUsersIntegrationTest, UserResourceIntegrationTest) has been addressed — all three now track and remove created roles/users in @AfterClass. ✅

Overall: no blocking issues.
37070-37071-roles-users-endpoint-and-counts

Comment thread dotCMS/src/main/java/com/dotmarketing/business/RoleFactoryImpl.java Outdated
@semgrep-dotcms

Copy link
Copy Markdown
Contributor

Semgrep found 2 CUSTOM_INJECTION-2 findings:

The method identified is susceptible to injection. The input should be validated and properly
escaped.

If this is a critical or high severity finding, please also link this issue in the #security channel in Slack.

PR review found that countUsersByRoleIds counted raw users_cms_roles
rows while GET /v1/roles/{roleid}/users filters through
UserFactoryImpl.getUsersByName, which hides the system and anonymous
users, the default user, and users flagged delete_in_progress. A role
granted to any of those reported a badge count higher than the Users
tab total.

The count query now joins user_ and applies the same visibility rules
as the listing, so RoleView.userCount always equals the listing total.
Covered by a red-first integration test that grants a role to a visible
user, the system user and a delete_in_progress user, asserts the count
is 1, and asserts it equals the endpoint's totalEntries. Javadoc,
Schema description and the regenerated openapi.yaml updated to match.
@semgrep-dotcms

Copy link
Copy Markdown
Contributor

Semgrep found 8 CUSTOM_INJECTION-2 findings:

The method identified is susceptible to injection. The input should be validated and properly
escaped.

If this is a critical or high severity finding, please also link this issue in the #security channel in Slack.

Auditing the SQL surface showed the endpoint's orderBy and direction
parameters were silently ignored: UserPaginator reads ordering from the
FilteringParams keys (orderby, orderdirection), not from the
PaginationUtil orderBy/direction arguments, and the endpoint never set
those keys. Results always came back full-name ascending.

The endpoint now passes both through extraParams, the same wiring
/v1/users/filter uses for orderBy. The direction value stays enum-gated
(OrderDirection.valueOf) and maps to the SQLUtil._ASC/_DESC constants
FilteringParams expects. orderBy remains subject to the
SQLUtil.sanitizeSortBy whitelist downstream.

Covered by a red-first integration test asserting DESC returns the
exact reverse of ASC.
PR review asked whether roles returned by loadRolesForUser carry a
hydrated roleChildren list, since childCount would silently report 0
otherwise. Code-wise they do (every role goes through getRoleById,
which runs populatChildrenForRoles), and this assertion pins that down:
the granted role in loadUserRoles_carriesCounts now has a child and the
view must report childCount 1.
@hassandotcms

hassandotcms commented Aug 18, 2026

Copy link
Copy Markdown
Member Author

Semgrep found 2 CUSTOM_INJECTION-2 findings:

The method identified is susceptible to injection. The input should be validated and properly escaped.

If this is a critical or high severity finding, please also link this issue in the #security channel in Slack.

False positive: the concatenated parts are only ? placeholders from map(id -> "?") and the constant getDBFalse(); all values are bound via addParam ->preparedStatement.setObject (DotConnect.java:1229).

…sers-endpoint-and-counts

# Conflicts:
#	dotCMS/src/main/java/com/dotcms/rest/api/v1/system/role/RoleHelper.java
…sers-endpoint-and-counts

# Conflicts:
#	dotCMS/src/main/java/com/dotcms/rest/api/v1/system/role/RoleHelper.java
#	dotCMS/src/main/webapp/WEB-INF/openapi/openapi.yaml
@hassandotcms
hassandotcms marked this pull request as ready for review August 19, 2026 07:40
Executable evidence for triaging the Semgrep CUSTOM_INJECTION-2
findings as false positives: the test passes stacked-statement,
tautology, UNION and quote-breaking payloads through the roleIds
collection and asserts they match nothing, throw nothing, and leave
the tables intact while a legitimate id in the same call still counts
correctly. The IN-list is size-derived placeholders only and every id
is bound as a PreparedStatement parameter.
Review feedback on the PR asked for the same datagen cleanup UserAPITest
uses. A literal per-test try/finally turned out to destabilize the REST
tests: every role deletion clears the global role cache (user deletion
cascades through the user's own role too), and
WebResource.checkRolePermissions swallows any transient exception from
doesUserHaveRole into 'lacks role', so deletions interleaved between
REST calls intermittently produced spurious 401s (reproduced
deterministically in RoleResourceCountsIntegrationTest).

Created roles and users are therefore tracked per class and removed
once in @afterclass: the suite is left just as clean, with no deletions
between REST calls. Children are removed before parents. All three
classes pass first-run with no retries.
…sers-endpoint-and-counts

# Conflicts:
#	dotcms-integration/src/test/java/com/dotcms/rest/api/v1/user/UserResourceIntegrationTest.java
@hassandotcms
hassandotcms enabled auto-merge August 24, 2026 15:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area : Backend PR changes Java/Maven backend code

Projects

Status: No status

2 participants