Skip to content

feat(admin-api): enforce admin.* RBAC on every admin endpoint (out of the box)#45

Merged
jlc488 merged 1 commit into
mainfrom
feat/admin-rbac-enforcement
May 31, 2026
Merged

feat(admin-api): enforce admin.* RBAC on every admin endpoint (out of the box)#45
jlc488 merged 1 commit into
mainfrom
feat/admin-rbac-enforcement

Conversation

@jlc488
Copy link
Copy Markdown
Contributor

@jlc488 jlc488 commented May 31, 2026

What / 무엇

Makes the admin REST API enforce the kit's admin.* permissions on every endpoint — with zero consumer wiring. Add the starter and the admin API is permission-enforced; the seeded first admin can use everything immediately.

관리자 REST API의 모든 엔드포인트가 kit의 admin.* 권한을 실제로 강제하도록 함 — 소비자는 설정할 게 전혀 없음. 스타터만 추가하면 권한이 강제되는 admin API가 그대로 나오고, 시드된 최초 관리자는 즉시 모든 기능을 사용 가능.

Why / 왜

The admin API was authenticated-only: any valid token could call any endpoint, even though the kit ships a full permission model and the docs state the endpoints are "guarded by ... the kit's permissions". That promise wasn't actually enforced. This closes the gap.

기존엔 인증만 하면 통과였음 — 유효한 토큰만 있으면 권한과 무관하게 모든 엔드포인트 호출 가능. kit이 권한 모델을 제공하고 문서도 "권한으로 보호된다"고 했지만 실제 강제는 없었음. 그 간극을 닫음.

How / 어떻게

  • AdminSecurityConfig — one auditable authorization matrix in the security chain: each /admin/api/v1/** endpoint maps to its admin.* permission (GET*.read, mutating → *.write). No @PreAuthorize scattered across 12 controllers, no @EnableMethodSecurity. login + bootstrap/status stay public; change-password stays merely authenticated.
  • JwtAuthenticationFilter — populates the security context with the caller's effective permissions as authorities, resolved per request from findCodesForUserId (the same role + group grant query PermissionChecker uses). Resolved fresh, not embedded in the JWT, so a grant/revocation takes effect on the next request and the token stays small. Roles still added as ROLE_*.
  • No seed change — bootstrap already grants all 16 admin.* permissions to PLATFORM_ADMIN.
  • Both beans stay @ConditionalOnMissingBean, so a consumer can replace the chain or filter wholesale.

Tests / 테스트

New AdminRbacEnforcementTests (sample-app, real HTTP + Testcontainers):

  • seeded admin (all permissions) → 200 on a protected read;
  • authenticated caller with no grants → 403 on the same endpoint.

Unauthenticated 401/403 stays covered by BootstrapStatusEndpointTests.

Verification / 검증

Local full ./gradlew build green — ExternalConsumer 2/2, BootstrapStatus 2/2, SampleApplication 4/4, AdminRbacEnforcement 2/2; 0 failures across all modules. CI green on this branch.

… the box

The admin REST API was authenticated-only: any valid token could call any endpoint,
even though the kit ships a full permission model and the docs claim the endpoints are
"guarded by ... the kit's permissions". This closes that gap so the kit delivers real
RBAC with zero consumer wiring — add the starter and the admin API is permission-enforced.

- AdminSecurityConfig now maps every /admin/api/v1/** endpoint to the admin.* permission
  it requires (read endpoints -> *.read, mutating -> *.write), expressed as one auditable
  authorization matrix in the security chain (no @PreAuthorize scattered across 12
  controllers, no @EnableMethodSecurity needed). login + bootstrap/status stay public;
  self-service change-password stays merely authenticated.
- JwtAuthenticationFilter now populates the security context with the caller's effective
  permissions as authorities, resolved PER REQUEST from JpaPlatformPermissionRepository
  .findCodesForUserId (the same role+group effective-grant query PermissionChecker uses).
  Resolved fresh rather than embedded in the JWT, so a grant/revocation takes effect on the
  next request and the token stays small. Roles are still added as ROLE_* for any
  role-based rules a consumer layers on.
- No seed change needed: the bootstrap already provisions all 16 admin.* permissions onto
  PLATFORM_ADMIN, so the first admin can use every endpoint immediately.
- Both beans remain @ConditionalOnMissingBean, so a consumer can replace the chain or the
  filter wholesale.

New AdminRbacEnforcementTests (sample-app, real HTTP + Testcontainers) proves both sides:
the seeded admin (all permissions) gets 200 on a protected read; an authenticated caller
holding no grants gets 403 on the same endpoint. The unauthenticated 401/403 case stays
covered by BootstrapStatusEndpointTests.

Verification: full `./gradlew build` green — sample-app suite ExternalConsumer 2/2,
BootstrapStatus 2/2, SampleApplication 4/4, AdminRbacEnforcement 2/2; 0 failures across all
modules.
@jlc488 jlc488 merged commit f08b184 into main May 31, 2026
1 check passed
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