feat: replace flat users.roles with relational RBAC model

New package com.cameleer3.server.core.rbac with SystemRole constants,
detail/summary records, GroupRepository, RoleRepository, RbacService.
Remove roles field from UserInfo. Implement PostgresGroupRepository,
PostgresRoleRepository, RbacServiceImpl with inheritance computation.
Update UiAuthController, OidcAuthController, AgentRegistrationController
to assign roles via user_roles table. JWT populated from effective system roles.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-17 17:44:32 +01:00
parent b06b3f52a8
commit eb0cc8c141
22 changed files with 639 additions and 44 deletions

View File

@@ -11,6 +11,8 @@ import com.cameleer3.server.app.security.BootstrapTokenValidator;
import com.cameleer3.server.core.agent.AgentInfo;
import com.cameleer3.server.core.agent.AgentRegistryService;
import com.cameleer3.server.core.agent.AgentState;
import com.cameleer3.server.core.rbac.RbacService;
import com.cameleer3.server.core.rbac.SystemRole;
import com.cameleer3.server.core.security.Ed25519SigningService;
import com.cameleer3.server.core.security.InvalidTokenException;
import com.cameleer3.server.core.security.JwtService;
@@ -50,17 +52,20 @@ public class AgentRegistrationController {
private final BootstrapTokenValidator bootstrapTokenValidator;
private final JwtService jwtService;
private final Ed25519SigningService ed25519SigningService;
private final RbacService rbacService;
public AgentRegistrationController(AgentRegistryService registryService,
AgentRegistryConfig config,
BootstrapTokenValidator bootstrapTokenValidator,
JwtService jwtService,
Ed25519SigningService ed25519SigningService) {
Ed25519SigningService ed25519SigningService,
RbacService rbacService) {
this.registryService = registryService;
this.config = config;
this.bootstrapTokenValidator = bootstrapTokenValidator;
this.jwtService = jwtService;
this.ed25519SigningService = ed25519SigningService;
this.rbacService = rbacService;
}
@PostMapping("/register")
@@ -97,6 +102,9 @@ public class AgentRegistrationController {
request.agentId(), request.name(), group, request.version(), routeIds, capabilities);
log.info("Agent registered: {} (name={}, group={})", request.agentId(), request.name(), group);
// Assign AGENT role via RBAC
rbacService.assignRoleToUser(request.agentId(), SystemRole.AGENT_ID);
// Issue JWT tokens with AGENT role
List<String> roles = List.of("AGENT");
String accessToken = jwtService.createAccessToken(request.agentId(), group, roles);

View File

@@ -3,6 +3,8 @@ package com.cameleer3.server.app.controller;
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.SystemRole;
import com.cameleer3.server.core.security.UserInfo;
import com.cameleer3.server.core.security.UserRepository;
import jakarta.servlet.http.HttpServletRequest;
@@ -21,6 +23,7 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* Admin endpoints for user management.
@@ -34,10 +37,13 @@ public class UserAdminController {
private final UserRepository userRepository;
private final AuditService auditService;
private final RbacService rbacService;
public UserAdminController(UserRepository userRepository, AuditService auditService) {
public UserAdminController(UserRepository userRepository, AuditService auditService,
RbacService rbacService) {
this.userRepository = userRepository;
this.auditService = auditService;
this.rbacService = rbacService;
}
@GetMapping
@@ -67,7 +73,20 @@ public class UserAdminController {
if (userRepository.findById(userId).isEmpty()) {
return ResponseEntity.notFound().build();
}
userRepository.updateRoles(userId, request.roles());
// Remove all existing direct roles, then assign the new ones
List<String> currentRoles = rbacService.getSystemRoleNames(userId);
for (String roleName : currentRoles) {
UUID roleId = SystemRole.BY_NAME.get(roleName);
if (roleId != null) {
rbacService.removeRoleFromUser(userId, roleId);
}
}
for (String roleName : request.roles()) {
UUID roleId = SystemRole.BY_NAME.get(roleName.toUpperCase());
if (roleId != null) {
rbacService.assignRoleToUser(userId, roleId);
}
}
auditService.log("update_roles", AuditCategory.USER_MGMT, userId,
Map.of("roles", request.roles()), AuditResult.SUCCESS, httpRequest);
return ResponseEntity.ok().build();