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:
@@ -1,5 +1,5 @@
|
||||
package com.cameleer3.server.core.admin;
|
||||
|
||||
public enum AuditCategory {
|
||||
INFRA, AUTH, USER_MGMT, CONFIG
|
||||
INFRA, AUTH, USER_MGMT, CONFIG, RBAC
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.cameleer3.server.core.rbac;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public record GroupDetail(UUID id, String name, UUID parentGroupId, Instant createdAt,
|
||||
List<RoleSummary> directRoles, List<RoleSummary> effectiveRoles,
|
||||
List<UserSummary> members, List<GroupSummary> childGroups) {}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.cameleer3.server.core.rbac;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface GroupRepository {
|
||||
List<GroupSummary> findAll();
|
||||
Optional<GroupDetail> findById(UUID id);
|
||||
UUID create(String name, UUID parentGroupId);
|
||||
void update(UUID id, String name, UUID parentGroupId);
|
||||
void delete(UUID id);
|
||||
void addRole(UUID groupId, UUID roleId);
|
||||
void removeRole(UUID groupId, UUID roleId);
|
||||
List<GroupSummary> findChildGroups(UUID parentId);
|
||||
List<GroupSummary> findAncestorChain(UUID groupId);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.cameleer3.server.core.rbac;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public record GroupSummary(UUID id, String name) {}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.cameleer3.server.core.rbac;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface RbacService {
|
||||
List<UserDetail> listUsers();
|
||||
UserDetail getUser(String userId);
|
||||
void assignRoleToUser(String userId, UUID roleId);
|
||||
void removeRoleFromUser(String userId, UUID roleId);
|
||||
void addUserToGroup(String userId, UUID groupId);
|
||||
void removeUserFromGroup(String userId, UUID groupId);
|
||||
List<RoleSummary> getEffectiveRolesForUser(String userId);
|
||||
List<GroupSummary> getEffectiveGroupsForUser(String userId);
|
||||
List<RoleSummary> getEffectiveRolesForGroup(UUID groupId);
|
||||
List<UserSummary> getEffectivePrincipalsForRole(UUID roleId);
|
||||
List<String> getSystemRoleNames(String userId);
|
||||
RbacStats getStats();
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.cameleer3.server.core.rbac;
|
||||
|
||||
public record RbacStats(int userCount, int activeUserCount, int groupCount, int maxGroupDepth, int roleCount) {}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.cameleer3.server.core.rbac;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public record RoleDetail(UUID id, String name, String description, String scope, boolean system,
|
||||
Instant createdAt, List<GroupSummary> assignedGroups, List<UserSummary> directUsers,
|
||||
List<UserSummary> effectivePrincipals) {}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.cameleer3.server.core.rbac;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface RoleRepository {
|
||||
List<RoleDetail> findAll();
|
||||
Optional<RoleDetail> findById(UUID id);
|
||||
UUID create(String name, String description, String scope);
|
||||
void update(UUID id, String name, String description, String scope);
|
||||
void delete(UUID id);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.cameleer3.server.core.rbac;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public record RoleSummary(UUID id, String name, boolean system, String source) {}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.cameleer3.server.core.rbac;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class SystemRole {
|
||||
private SystemRole() {}
|
||||
|
||||
public static final UUID AGENT_ID = UUID.fromString("00000000-0000-0000-0000-000000000001");
|
||||
public static final UUID VIEWER_ID = UUID.fromString("00000000-0000-0000-0000-000000000002");
|
||||
public static final UUID OPERATOR_ID = UUID.fromString("00000000-0000-0000-0000-000000000003");
|
||||
public static final UUID ADMIN_ID = UUID.fromString("00000000-0000-0000-0000-000000000004");
|
||||
|
||||
public static final Set<UUID> IDS = Set.of(AGENT_ID, VIEWER_ID, OPERATOR_ID, ADMIN_ID);
|
||||
|
||||
public static final Map<String, UUID> BY_NAME = Map.of(
|
||||
"AGENT", AGENT_ID, "VIEWER", VIEWER_ID, "OPERATOR", OPERATOR_ID, "ADMIN", ADMIN_ID);
|
||||
|
||||
public static boolean isSystem(UUID id) { return IDS.contains(id); }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.cameleer3.server.core.rbac;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
public record UserDetail(String userId, String provider, String email, String displayName,
|
||||
Instant createdAt, List<RoleSummary> directRoles, List<GroupSummary> directGroups,
|
||||
List<RoleSummary> effectiveRoles, List<GroupSummary> effectiveGroups) {}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.cameleer3.server.core.rbac;
|
||||
|
||||
public record UserSummary(String userId, String displayName, String provider) {}
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.cameleer3.server.core.security;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents a persisted user in the system.
|
||||
@@ -10,7 +9,6 @@ import java.util.List;
|
||||
* @param provider authentication provider ({@code "local"}, {@code "oidc:<issuer-host>"})
|
||||
* @param email user email (may be empty)
|
||||
* @param displayName display name (may be empty)
|
||||
* @param roles assigned roles (e.g. {@code ["ADMIN"]}, {@code ["VIEWER"]})
|
||||
* @param createdAt first creation timestamp
|
||||
*/
|
||||
public record UserInfo(
|
||||
@@ -18,6 +16,5 @@ public record UserInfo(
|
||||
String provider,
|
||||
String email,
|
||||
String displayName,
|
||||
List<String> roles,
|
||||
Instant createdAt
|
||||
) {}
|
||||
|
||||
@@ -14,7 +14,5 @@ public interface UserRepository {
|
||||
|
||||
void upsert(UserInfo user);
|
||||
|
||||
void updateRoles(String userId, List<String> roles);
|
||||
|
||||
void delete(String userId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user