feat: scope-based authorization — read standard scope claim, remove custom roles extraction
Some checks failed
CI / build (push) Failing after 18s
CI / docker (push) Has been skipped

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-05 14:04:16 +02:00
parent 9c2a1d27b7
commit 298f6e3e71
6 changed files with 20 additions and 30 deletions

View File

@@ -34,9 +34,6 @@ public class MeController {
String orgId = jwt.getClaimAsString("organization_id"); String orgId = jwt.getClaimAsString("organization_id");
List<String> globalRoles = jwt.getClaimAsStringList("roles");
boolean isPlatformAdmin = globalRoles != null && globalRoles.contains("platform-admin");
if (orgId != null) { if (orgId != null) {
var tenant = tenantService.getByLogtoOrgId(orgId).orElse(null); var tenant = tenantService.getByLogtoOrgId(orgId).orElse(null);
List<Map<String, Object>> tenants = tenant != null List<Map<String, Object>> tenants = tenant != null
@@ -49,7 +46,6 @@ public class MeController {
return ResponseEntity.ok(Map.of( return ResponseEntity.ok(Map.of(
"userId", userId, "userId", userId,
"isPlatformAdmin", isPlatformAdmin,
"tenants", tenants)); "tenants", tenants));
} }
@@ -67,7 +63,6 @@ public class MeController {
return ResponseEntity.ok(Map.of( return ResponseEntity.ok(Map.of(
"userId", userId, "userId", userId,
"isPlatformAdmin", isPlatformAdmin,
"tenants", tenants)); "tenants", tenants));
} }
} }

View File

@@ -62,17 +62,14 @@ public class SecurityConfig {
var converter = new JwtAuthenticationConverter(); var converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(jwt -> { converter.setJwtGrantedAuthoritiesConverter(jwt -> {
List<GrantedAuthority> authorities = new ArrayList<>(); List<GrantedAuthority> authorities = new ArrayList<>();
String scope = jwt.getClaimAsString("scope");
var roles = jwt.getClaimAsStringList("roles"); if (scope != null) {
if (roles != null) { for (String s : scope.split(" ")) {
roles.forEach(r -> authorities.add(new SimpleGrantedAuthority("ROLE_" + r))); 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 authorities;
}); });
return converter; return converter;

View File

@@ -28,7 +28,7 @@ public class TenantController {
} }
@GetMapping @GetMapping
@PreAuthorize("hasRole('platform-admin')") @PreAuthorize("hasAuthority('SCOPE_platform:admin')")
public ResponseEntity<List<TenantResponse>> listAll() { public ResponseEntity<List<TenantResponse>> listAll() {
List<TenantResponse> tenants = tenantService.findAll().stream() List<TenantResponse> tenants = tenantService.findAll().stream()
.map(this::toResponse).toList(); .map(this::toResponse).toList();
@@ -36,7 +36,7 @@ public class TenantController {
} }
@PostMapping @PostMapping
@PreAuthorize("hasRole('platform-admin')") @PreAuthorize("hasAuthority('SCOPE_platform:admin')")
public ResponseEntity<TenantResponse> create(@Valid @RequestBody CreateTenantRequest request, public ResponseEntity<TenantResponse> create(@Valid @RequestBody CreateTenantRequest request,
Authentication authentication) { Authentication authentication) {
try { try {

View File

@@ -7,7 +7,6 @@ import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.JwtDecoder;
import java.time.Instant; import java.time.Instant;
import java.util.List;
@TestConfiguration @TestConfiguration
public class TestSecurityConfig { public class TestSecurityConfig {
@@ -20,8 +19,7 @@ public class TestSecurityConfig {
.claim("sub", "test-user") .claim("sub", "test-user")
.claim("iss", "https://test-issuer.example.com/oidc") .claim("iss", "https://test-issuer.example.com/oidc")
.claim("organization_id", "test-org-id") .claim("organization_id", "test-org-id")
.claim("roles", List.of("platform-admin")) .claim("scope", "platform:admin tenant:manage apps:manage apps:deploy observe:read observe:debug secrets:manage billing:manage team:manage settings:manage")
.claim("organization_roles", List.of("admin"))
.issuedAt(Instant.now()) .issuedAt(Instant.now())
.expiresAt(Instant.now().plusSeconds(3600)) .expiresAt(Instant.now().plusSeconds(3600))
.build(); .build();

View File

@@ -40,8 +40,8 @@ class LicenseControllerTest {
var result = mockMvc.perform(post("/api/tenants") var result = mockMvc.perform(post("/api/tenants")
.with(jwt().jwt(j -> j .with(jwt().jwt(j -> j
.claim("sub", "test-user") .claim("sub", "test-user")
.claim("roles", java.util.List.of("platform-admin"))) .claim("scope", "platform:admin"))
.authorities(new SimpleGrantedAuthority("ROLE_platform-admin"))) .authorities(new SimpleGrantedAuthority("SCOPE_platform:admin")))
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))) .content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated()) .andExpect(status().isCreated())

View File

@@ -41,8 +41,8 @@ class TenantControllerTest {
.with(jwt().jwt(j -> j .with(jwt().jwt(j -> j
.claim("sub", "test-user") .claim("sub", "test-user")
.claim("organization_id", "test-org") .claim("organization_id", "test-org")
.claim("roles", java.util.List.of("platform-admin"))) .claim("scope", "platform:admin"))
.authorities(new SimpleGrantedAuthority("ROLE_platform-admin"))) .authorities(new SimpleGrantedAuthority("SCOPE_platform:admin")))
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))) .content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated()) .andExpect(status().isCreated())
@@ -59,8 +59,8 @@ class TenantControllerTest {
mockMvc.perform(post("/api/tenants") mockMvc.perform(post("/api/tenants")
.with(jwt().jwt(j -> j .with(jwt().jwt(j -> j
.claim("sub", "test-user") .claim("sub", "test-user")
.claim("roles", java.util.List.of("platform-admin"))) .claim("scope", "platform:admin"))
.authorities(new SimpleGrantedAuthority("ROLE_platform-admin"))) .authorities(new SimpleGrantedAuthority("SCOPE_platform:admin")))
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))) .content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated()); .andExpect(status().isCreated());
@@ -68,8 +68,8 @@ class TenantControllerTest {
mockMvc.perform(post("/api/tenants") mockMvc.perform(post("/api/tenants")
.with(jwt().jwt(j -> j .with(jwt().jwt(j -> j
.claim("sub", "test-user") .claim("sub", "test-user")
.claim("roles", java.util.List.of("platform-admin"))) .claim("scope", "platform:admin"))
.authorities(new SimpleGrantedAuthority("ROLE_platform-admin"))) .authorities(new SimpleGrantedAuthority("SCOPE_platform:admin")))
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))) .content(objectMapper.writeValueAsString(request)))
.andExpect(status().isConflict()); .andExpect(status().isConflict());
@@ -93,8 +93,8 @@ class TenantControllerTest {
var createResult = mockMvc.perform(post("/api/tenants") var createResult = mockMvc.perform(post("/api/tenants")
.with(jwt().jwt(j -> j .with(jwt().jwt(j -> j
.claim("sub", "test-user") .claim("sub", "test-user")
.claim("roles", java.util.List.of("platform-admin"))) .claim("scope", "platform:admin"))
.authorities(new SimpleGrantedAuthority("ROLE_platform-admin"))) .authorities(new SimpleGrantedAuthority("SCOPE_platform:admin")))
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))) .content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated()) .andExpect(status().isCreated())