feat: sync OIDC roles on every login, not just first
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / build (push) Has been cancelled

Roles from the id_token's rolesClaim are now diffed against stored
system roles on each OIDC login. Missing roles are added, revoked
roles are removed. Group memberships (manually assigned) are never
touched. This propagates scope revocations from the OIDC provider
on next user login.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-06 10:11:06 +02:00
parent 4e12fcbe7a
commit f4eafd9a0f

View File

@@ -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<String> roles = rbacService.getSystemRoleNames(userId);
@@ -167,8 +171,11 @@ public class OidcAuthController {
}
}
private void assignRolesForNewUser(String userId, List<String> oidcRoles, OidcConfig config) {
private void syncOidcRoles(String userId, List<String> oidcRoles, OidcConfig config) {
List<String> roleNames = !oidcRoles.isEmpty() ? oidcRoles : config.defaultRoles();
// Resolve desired role IDs from OIDC scopes
Set<UUID> 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<UUID> 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);
}
}
}