feat: sync OIDC roles on every login, not just first
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user