Skip to content

feat(access): expose actual permission grant path on Diagnostics#21

Merged
jlc488 merged 1 commit into
mainfrom
feat/access-permission-grant-path
May 30, 2026
Merged

feat(access): expose actual permission grant path on Diagnostics#21
jlc488 merged 1 commit into
mainfrom
feat/access-permission-grant-path

Conversation

@jlc488
Copy link
Copy Markdown
Contributor

@jlc488 jlc488 commented May 27, 2026

Follow-up #3/5. #18 wired the Diagnostics permission-check response to admin-ui field names (hasPermission / matchedVia) but had to stub matchedVia with just the permission code itself because the access SPI couldn't tell how the user got it. This PR adds the trace.

SPI additions (access-api)

  • PermissionGrant record: { permissionCode, roleCode, groupCode }. groupCode == null means the role is bound to the user directly; non-null means the role reached the user through a group. The record's describe() helper renders the path compactly:

    • "role:ADMIN" for direct grants
    • "group:eng-team>role:ADMIN" for grants through a group

    That's exactly what the admin UI surfaces under matchedVia.

Repository (access-core)

  • New native query findGrantRowsForUserAndPermission(userId, permissionCode) on JpaPlatformPermissionRepository. Two unioned branches:
    1. platform_user_role → role direct, group_code = NULL
    2. platform_user_group → platform_group_role → role indirect, group_code = the group's code
  • Returns Object[] per row {permission_code, role_code, group_code}; the caller maps to PermissionGrant.

Service (access-core)

  • New PermissionGrantQueryService.findGrantsFor(userId, permissionCode) wrapping that query and producing List<PermissionGrant>. Lives next to DefaultPermissionChecker but is a separate class — the PermissionChecker contract stays thin (it answers yes/no on the hot path; this service is for human-facing diagnostics).

Wiring (autoconfigure)

  • AccessAutoConfiguration registers PermissionGrantQueryService as a @ConditionalOnMissingBean so consumers can override.

Controller (admin-api)

  • DiagnosticsController.permissionCheck now resolves every grant path for (userId, permissionCode) via the new service. matchedVia carries the path strings (via PermissionGrant.describe), distinct to collapse duplicates when the user holds the permission both directly and through a group. Empty list when has == false.

Test plan

  • ./gradlew build green (55 tasks, all tests pass — no schema change, no breaking change)
  • Manual: with a user who holds admin.user.read via both a direct role and a group role, hit /diagnostics/permission-check; verify matchedVia shows both role:ADMIN and group:eng-team>role:READER (deduped if identical)

Follow-up #3/5 to the contract-gap series. #18 wired the
Diagnostics `permission-check` response to admin-ui field names
(hasPermission / matchedVia) but had to stub `matchedVia` with just
the permission code itself because the access SPI couldn't tell
*how* the user got it. This PR adds the trace.

SPI additions (access-api)
--------------------------
- New PermissionGrant record: { permissionCode, roleCode, groupCode }.
  groupCode is null when the role is bound to the user directly,
  non-null when the role reached the user through a group
  membership. The record's `describe()` helper renders the path
  compactly — "role:ADMIN" for direct grants,
  "group:eng-team>role:ADMIN" for group-mediated ones — which is
  exactly what the admin UI's permission-tester surfaces under
  matchedVia.

Repository (access-core)
------------------------
- New native query findGrantRowsForUserAndPermission(userId,
  permissionCode) on JpaPlatformPermissionRepository. Two unioned
  branches:
    1. platform_user_role  -> role direct,  group_code = NULL
    2. platform_user_group -> platform_group_role -> role indirect,
                              group_code = group's code
  Returns Object[] per row {permission_code, role_code, group_code}
  so the caller maps to PermissionGrant.

Service (access-core)
---------------------
- New PermissionGrantQueryService.findGrantsFor(userId,
  permissionCode) wrapping that query and producing
  List<PermissionGrant>. Lives next to DefaultPermissionChecker
  but is a separate class so the PermissionChecker contract
  stays thin — this service is for human-facing diagnostics, not
  the hot path of yes/no permission checks.

Wiring (autoconfigure)
----------------------
- AccessAutoConfiguration registers PermissionGrantQueryService as
  a @ConditionalOnMissingBean so consumers can override.

Controller (admin-api)
----------------------
- DiagnosticsController.permissionCheck now resolves every grant
  path for (userId, permissionCode) via the new service. matchedVia
  carries the path string (via PermissionGrant.describe), distinct
  to collapse duplicates when the user holds the permission both
  directly and through a group. Empty list when has == false.
@jlc488 jlc488 merged commit ba04b00 into main May 30, 2026
1 check passed
@jlc488 jlc488 deleted the feat/access-permission-grant-path branch May 30, 2026 06:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant