Skip to content

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

Open
hassandotcms wants to merge 11 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 11 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 21s —— View job


Claude Code Review

  • Read the diff vs origin/main
  • Review REST layer (RoleResource, RoleHelper, RoleView)
  • Review API/Factory layer (RoleAPI, RoleFactoryImpl, UserFactoryImpl)
  • Check SQL injection concern from Semgrep
  • Assess test coverage
  • Post findings

I reviewed the counts query, the new /users endpoint wiring, the RoleView shape change, and the appendRoleFilter prerequisite fix. The design is sound — batched counts, no extra queries for childCount, direct-grants-only semantics, and visibility filters that genuinely mirror getUsersByName.

New Issues

  • 🟡 Medium: RoleResource.java:882OrderDirection.valueOf(direction) throws IllegalArgumentException for any direction value other than exactly ASC/DESC (e.g. lowercase asc, or garbage). The endpoint's @ApiResponse documents 400 for "invalid pagination or sorting parameters", but an unmapped IllegalArgumentException will surface as 500, not 400. This mirrors the existing pattern at UserResource.java:472, so it is not a regression and is non-blocking — but since this is a new endpoint newly asserting a 400 contract, consider guarding the parse (e.g. Try.of(() -> OrderDirection.valueOf(direction.toUpperCase())) with a BadRequestException fallback). Fix this →

Resolved

  • RoleFactoryImpl.java:599-605 — Semgrep CUSTOM_INJECTION-2 is a false positive, as the author noted. The only concatenated fragments into the SQL are ? placeholders from chunk.stream().map(id -> "?") and the constant DbConnectionFactory.getDBFalse(); every value (role_ids and User.DEFAULT) is bound via dc.addParam(...). No user-controlled string reaches the SQL text.

Notes (non-blocking, no action required)

  • RoleFactoryImpl.countUsersByRoleIds visibility filters (companyid <> User.DEFAULT, userid <> 'system'/'anonymous', delete_in_progress = false) correctly match getUsersByName's defaults (includeDefaultUser=false, includeAnonymousUser=false), so userCount will match the /users listing total. Verified against UserFactoryImpl.java:293-317.
  • Lowercase result keys role_id/user_count are consistent with the codebase convention (row.get("parentid"), getInt("count")), so they resolve across Postgres/other DBs.
  • Old 2-arg new RoleView(role, children) constructor is fully removed and both remaining call sites (RoleHelper.java:548,551) are updated — no orphaned callers.
  • extraParams is correctly copied into a mutable HashMap before the conditional put, avoiding the Map.of immutability trap.

Overall this is in good shape — no blocking issues. The single Medium is a documented-vs-actual status-code nit that matches pre-existing behavior.
· 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
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

1 participant