diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/OidcAuthController.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/OidcAuthController.java index ec4e162b..9cb0ca95 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/OidcAuthController.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/OidcAuthController.java @@ -7,6 +7,7 @@ import com.cameleer3.server.core.admin.AuditCategory; import com.cameleer3.server.core.admin.AuditResult; import com.cameleer3.server.core.admin.AuditService; import com.cameleer3.server.core.rbac.RbacService; +import com.cameleer3.server.core.rbac.RoleSummary; import com.cameleer3.server.core.rbac.SystemRole; import com.cameleer3.server.core.security.JwtService; import com.cameleer3.server.core.security.OidcConfig; @@ -32,10 +33,13 @@ import org.springframework.web.server.ResponseStatusException; import java.net.URI; import java.time.Instant; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; /** * OIDC authentication endpoints for the UI. @@ -140,10 +144,10 @@ public class OidcAuthController { userRepository.upsert(new UserInfo( userId, provider, oidcUser.email(), oidcUser.name(), Instant.now())); - // Assign roles if new user - if (existingUser.isEmpty()) { - assignRolesForNewUser(userId, oidcUser.roles(), config.get()); - } + // Sync system roles from OIDC scopes on every login (not just first). + // This propagates scope revocations from the provider. Group memberships + // (manually assigned) are not touched. + syncOidcRoles(userId, oidcUser.roles(), config.get()); List roles = rbacService.getSystemRoleNames(userId); @@ -167,8 +171,11 @@ public class OidcAuthController { } } - private void assignRolesForNewUser(String userId, List oidcRoles, OidcConfig config) { + private void syncOidcRoles(String userId, List oidcRoles, OidcConfig config) { List roleNames = !oidcRoles.isEmpty() ? oidcRoles : config.defaultRoles(); + + // Resolve desired role IDs from OIDC scopes + Set desired = new HashSet<>(); for (String roleName : roleNames) { String normalized = roleName.toUpperCase(); if (normalized.startsWith("SERVER:")) { @@ -176,7 +183,26 @@ public class OidcAuthController { } UUID roleId = SystemRole.BY_NAME.get(normalized); if (roleId != null) { - rbacService.assignRoleToUser(userId, roleId); + desired.add(roleId); + } + } + + // Current system roles (excludes group-inherited roles) + Set current = rbacService.getEffectiveRolesForUser(userId).stream() + .filter(r -> SystemRole.isSystem(r.id())) + .map(RoleSummary::id) + .collect(Collectors.toSet()); + + // Add missing + for (UUID id : desired) { + if (!current.contains(id)) { + rbacService.assignRoleToUser(userId, id); + } + } + // Remove revoked (skip AGENT — never managed by OIDC) + for (UUID id : current) { + if (!desired.contains(id) && !id.equals(SystemRole.AGENT_ID)) { + rbacService.removeRoleFromUser(userId, id); } } }