feat(access): expose actual permission grant path on Diagnostics#21
Merged
Conversation
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.
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.
Follow-up #3/5. #18 wired the Diagnostics
permission-checkresponse to admin-ui field names (hasPermission/matchedVia) but had to stubmatchedViawith 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)PermissionGrantrecord:{ permissionCode, roleCode, groupCode }.groupCode == nullmeans the role is bound to the user directly; non-null means the role reached the user through a group. The record'sdescribe()helper renders the path compactly:"role:ADMIN"for direct grants"group:eng-team>role:ADMIN"for grants through a groupThat's exactly what the admin UI surfaces under
matchedVia.Repository (
access-core)findGrantRowsForUserAndPermission(userId, permissionCode)onJpaPlatformPermissionRepository. Two unioned branches:platform_user_role→ role direct,group_code = NULLplatform_user_group → platform_group_role→ role indirect,group_code= the group's codeObject[]per row{permission_code, role_code, group_code}; the caller maps toPermissionGrant.Service (
access-core)PermissionGrantQueryService.findGrantsFor(userId, permissionCode)wrapping that query and producingList<PermissionGrant>. Lives next toDefaultPermissionCheckerbut is a separate class — thePermissionCheckercontract stays thin (it answers yes/no on the hot path; this service is for human-facing diagnostics).Wiring (
autoconfigure)AccessAutoConfigurationregistersPermissionGrantQueryServiceas a@ConditionalOnMissingBeanso consumers can override.Controller (
admin-api)DiagnosticsController.permissionChecknow resolves every grant path for(userId, permissionCode)via the new service.matchedViacarries the path strings (viaPermissionGrant.describe),distinctto collapse duplicates when the user holds the permission both directly and through a group. Empty list whenhas == false.Test plan
./gradlew buildgreen (55 tasks, all tests pass — no schema change, no breaking change)admin.user.readvia both a direct role and a group role, hit/diagnostics/permission-check; verifymatchedViashows bothrole:ADMINandgroup:eng-team>role:READER(deduped if identical)