From 298f6e3e71960527b10dcc2f229ae8513aee4906 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Sun, 5 Apr 2026 14:04:16 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20scope-based=20authorization=20=E2=80=94?= =?UTF-8?q?=20read=20standard=20scope=20claim,=20remove=20custom=20roles?= =?UTF-8?q?=20extraction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../cameleer/saas/config/MeController.java | 5 ----- .../cameleer/saas/config/SecurityConfig.java | 17 +++++++---------- .../cameleer/saas/tenant/TenantController.java | 4 ++-- .../cameleer/saas/TestSecurityConfig.java | 4 +--- .../saas/license/LicenseControllerTest.java | 4 ++-- .../saas/tenant/TenantControllerTest.java | 16 ++++++++-------- 6 files changed, 20 insertions(+), 30 deletions(-) diff --git a/src/main/java/net/siegeln/cameleer/saas/config/MeController.java b/src/main/java/net/siegeln/cameleer/saas/config/MeController.java index 1ed4276..46ef1eb 100644 --- a/src/main/java/net/siegeln/cameleer/saas/config/MeController.java +++ b/src/main/java/net/siegeln/cameleer/saas/config/MeController.java @@ -34,9 +34,6 @@ public class MeController { String orgId = jwt.getClaimAsString("organization_id"); - List globalRoles = jwt.getClaimAsStringList("roles"); - boolean isPlatformAdmin = globalRoles != null && globalRoles.contains("platform-admin"); - if (orgId != null) { var tenant = tenantService.getByLogtoOrgId(orgId).orElse(null); List> tenants = tenant != null @@ -49,7 +46,6 @@ public class MeController { return ResponseEntity.ok(Map.of( "userId", userId, - "isPlatformAdmin", isPlatformAdmin, "tenants", tenants)); } @@ -67,7 +63,6 @@ public class MeController { return ResponseEntity.ok(Map.of( "userId", userId, - "isPlatformAdmin", isPlatformAdmin, "tenants", tenants)); } } diff --git a/src/main/java/net/siegeln/cameleer/saas/config/SecurityConfig.java b/src/main/java/net/siegeln/cameleer/saas/config/SecurityConfig.java index c2dbce3..b951ea0 100644 --- a/src/main/java/net/siegeln/cameleer/saas/config/SecurityConfig.java +++ b/src/main/java/net/siegeln/cameleer/saas/config/SecurityConfig.java @@ -62,17 +62,14 @@ public class SecurityConfig { var converter = new JwtAuthenticationConverter(); converter.setJwtGrantedAuthoritiesConverter(jwt -> { List authorities = new ArrayList<>(); - - var roles = jwt.getClaimAsStringList("roles"); - if (roles != null) { - roles.forEach(r -> authorities.add(new SimpleGrantedAuthority("ROLE_" + r))); + String scope = jwt.getClaimAsString("scope"); + if (scope != null) { + for (String s : scope.split(" ")) { + if (!s.isBlank()) { + authorities.add(new SimpleGrantedAuthority("SCOPE_" + s)); + } + } } - - var orgRoles = jwt.getClaimAsStringList("organization_roles"); - if (orgRoles != null) { - orgRoles.forEach(r -> authorities.add(new SimpleGrantedAuthority("ROLE_org_" + r))); - } - return authorities; }); return converter; diff --git a/src/main/java/net/siegeln/cameleer/saas/tenant/TenantController.java b/src/main/java/net/siegeln/cameleer/saas/tenant/TenantController.java index ea5c008..903fd0f 100644 --- a/src/main/java/net/siegeln/cameleer/saas/tenant/TenantController.java +++ b/src/main/java/net/siegeln/cameleer/saas/tenant/TenantController.java @@ -28,7 +28,7 @@ public class TenantController { } @GetMapping - @PreAuthorize("hasRole('platform-admin')") + @PreAuthorize("hasAuthority('SCOPE_platform:admin')") public ResponseEntity> listAll() { List tenants = tenantService.findAll().stream() .map(this::toResponse).toList(); @@ -36,7 +36,7 @@ public class TenantController { } @PostMapping - @PreAuthorize("hasRole('platform-admin')") + @PreAuthorize("hasAuthority('SCOPE_platform:admin')") public ResponseEntity create(@Valid @RequestBody CreateTenantRequest request, Authentication authentication) { try { diff --git a/src/test/java/net/siegeln/cameleer/saas/TestSecurityConfig.java b/src/test/java/net/siegeln/cameleer/saas/TestSecurityConfig.java index 77d0b58..b19037b 100644 --- a/src/test/java/net/siegeln/cameleer/saas/TestSecurityConfig.java +++ b/src/test/java/net/siegeln/cameleer/saas/TestSecurityConfig.java @@ -7,7 +7,6 @@ import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtDecoder; import java.time.Instant; -import java.util.List; @TestConfiguration public class TestSecurityConfig { @@ -20,8 +19,7 @@ public class TestSecurityConfig { .claim("sub", "test-user") .claim("iss", "https://test-issuer.example.com/oidc") .claim("organization_id", "test-org-id") - .claim("roles", List.of("platform-admin")) - .claim("organization_roles", List.of("admin")) + .claim("scope", "platform:admin tenant:manage apps:manage apps:deploy observe:read observe:debug secrets:manage billing:manage team:manage settings:manage") .issuedAt(Instant.now()) .expiresAt(Instant.now().plusSeconds(3600)) .build(); diff --git a/src/test/java/net/siegeln/cameleer/saas/license/LicenseControllerTest.java b/src/test/java/net/siegeln/cameleer/saas/license/LicenseControllerTest.java index 82b914c..d7dcdc7 100644 --- a/src/test/java/net/siegeln/cameleer/saas/license/LicenseControllerTest.java +++ b/src/test/java/net/siegeln/cameleer/saas/license/LicenseControllerTest.java @@ -40,8 +40,8 @@ class LicenseControllerTest { var result = mockMvc.perform(post("/api/tenants") .with(jwt().jwt(j -> j .claim("sub", "test-user") - .claim("roles", java.util.List.of("platform-admin"))) - .authorities(new SimpleGrantedAuthority("ROLE_platform-admin"))) + .claim("scope", "platform:admin")) + .authorities(new SimpleGrantedAuthority("SCOPE_platform:admin"))) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isCreated()) diff --git a/src/test/java/net/siegeln/cameleer/saas/tenant/TenantControllerTest.java b/src/test/java/net/siegeln/cameleer/saas/tenant/TenantControllerTest.java index 7b96657..30e6914 100644 --- a/src/test/java/net/siegeln/cameleer/saas/tenant/TenantControllerTest.java +++ b/src/test/java/net/siegeln/cameleer/saas/tenant/TenantControllerTest.java @@ -41,8 +41,8 @@ class TenantControllerTest { .with(jwt().jwt(j -> j .claim("sub", "test-user") .claim("organization_id", "test-org") - .claim("roles", java.util.List.of("platform-admin"))) - .authorities(new SimpleGrantedAuthority("ROLE_platform-admin"))) + .claim("scope", "platform:admin")) + .authorities(new SimpleGrantedAuthority("SCOPE_platform:admin"))) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isCreated()) @@ -59,8 +59,8 @@ class TenantControllerTest { mockMvc.perform(post("/api/tenants") .with(jwt().jwt(j -> j .claim("sub", "test-user") - .claim("roles", java.util.List.of("platform-admin"))) - .authorities(new SimpleGrantedAuthority("ROLE_platform-admin"))) + .claim("scope", "platform:admin")) + .authorities(new SimpleGrantedAuthority("SCOPE_platform:admin"))) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isCreated()); @@ -68,8 +68,8 @@ class TenantControllerTest { mockMvc.perform(post("/api/tenants") .with(jwt().jwt(j -> j .claim("sub", "test-user") - .claim("roles", java.util.List.of("platform-admin"))) - .authorities(new SimpleGrantedAuthority("ROLE_platform-admin"))) + .claim("scope", "platform:admin")) + .authorities(new SimpleGrantedAuthority("SCOPE_platform:admin"))) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isConflict()); @@ -93,8 +93,8 @@ class TenantControllerTest { var createResult = mockMvc.perform(post("/api/tenants") .with(jwt().jwt(j -> j .claim("sub", "test-user") - .claim("roles", java.util.List.of("platform-admin"))) - .authorities(new SimpleGrantedAuthority("ROLE_platform-admin"))) + .claim("scope", "platform:admin")) + .authorities(new SimpleGrantedAuthority("SCOPE_platform:admin"))) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isCreated())