refactor: rename group/groupName to application/applicationName
The execution-related "group" concept actually represents the application name. Rename all Java fields, API parameters, and frontend types from groupName→applicationName and group→application for clarity. - Java records: ExecutionSummary, ExecutionDetail, ExecutionDocument, ExecutionRecord, ProcessorRecord - API params: SearchRequest.group→application, SearchController @RequestParam group→application - Services: IngestionService, DetailService, SearchIndexer, StatsStore - Frontend: schema.d.ts, Dashboard, ExchangeDetail, RouteDetail, executions query hooks Database column names (group_name) and OpenSearch field names are unchanged — only the API-facing Java/TS field names are renamed. RBAC group references (groups table, GroupRepository, GroupsTab) are a separate domain concept and are NOT affected by this change. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
142
docs/superpowers/specs/2026-03-17-rbac-crud-gaps-design.md
Normal file
142
docs/superpowers/specs/2026-03-17-rbac-crud-gaps-design.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# RBAC CRUD Gaps — Design Specification
|
||||
|
||||
## Goal
|
||||
|
||||
Add missing CRUD and assignment UI to the RBAC management page, fix date formatting, seed a built-in Admins group, and fix dashboard diagram ordering.
|
||||
|
||||
## References
|
||||
|
||||
- Parent spec: `docs/superpowers/specs/2026-03-17-rbac-management-design.md`
|
||||
- Visual prototype: `examples/RBAC/rbac_management_ui.html`
|
||||
|
||||
---
|
||||
|
||||
## Changes
|
||||
|
||||
### 1. Users Tab — Delete + Assignments
|
||||
|
||||
Users cannot be created manually (they arrive via login). The detail pane gains:
|
||||
|
||||
- **Delete button** in the detail header area. Uses existing `ConfirmDeleteDialog` with the user's `displayName` as the confirmation string. Calls `useDeleteUser()`. **Guard:** the currently authenticated user (from `useAuthStore`) cannot delete themselves — button disabled with tooltip "Cannot delete your own account".
|
||||
- **Group membership section** — "+ Add" chip opens a **multi-select dropdown** listing all groups the user is NOT already a member of. Checkboxes for batch selection, "Apply" button to commit. Calls are batched via `Promise.allSettled()` — if any fail, show an inline error, invalidate queries regardless to refresh. Existing group chips gain an "x" remove button calling `useRemoveUserFromGroup()`.
|
||||
- **Direct roles section** — the existing "Effective roles" section renders both direct and inherited roles. The "+ Add" multi-select dropdown lists roles not yet directly assigned. Calls `useAssignRoleToUser()` (batched via `Promise.allSettled()`). Direct role chips gain an "x" button calling `useRemoveRoleFromUser()`. Inherited role chips (dashed border) do NOT get remove buttons — they can only be removed by changing group membership or group role assignments.
|
||||
- **Created field** — change from date-only to full date+time: `new Date(createdAt).toLocaleString()`.
|
||||
- **Mutation button states** — all action buttons (delete, remove chip "x") disable while their mutation is in-flight to prevent double-clicks.
|
||||
|
||||
### 2. Groups Tab — CRUD + Assignments
|
||||
|
||||
- **"+ Add group" button** in the panel header (`.btnAdd` style exists). Opens an inline form below the search bar with: name text input, optional parent group dropdown, "Create" button. Calls `useCreateGroup()`. Form clears and closes on success. On error: shows error message inline.
|
||||
- **Delete button** in detail pane header. Uses `ConfirmDeleteDialog` with group name. Calls `useDeleteGroup()`. Resets selected group. **Guard:** the built-in Admins group (`SystemRole.ADMINS_GROUP_ID`) cannot be deleted — button disabled with tooltip "Built-in group cannot be deleted".
|
||||
- **Assigned roles section** — "+ Add" multi-select dropdown listing roles not yet assigned to this group. Batched via `Promise.allSettled()`. Calls `useAssignRoleToGroup()`. Role chips gain "x" for `useRemoveRoleFromGroup()`.
|
||||
- **Parent group** — shown as a dropdown in the detail header area, allowing re-parenting. Calls `useUpdateGroup()`. The dropdown excludes the group itself and its transitive descendants (cycle prevention — requires recursive traversal of `childGroups` on each `GroupDetail`). Setting to empty/none makes it top-level.
|
||||
|
||||
### 3. Roles Tab — CRUD
|
||||
|
||||
- **"+ Add role" button** in panel header. Opens an inline form: name (required), description (optional), scope (optional, free-text, defaults to "custom"). Calls `useCreateRole()`.
|
||||
- **Delete button** in detail pane header. **Disabled for system roles** (lock icon + tooltip "System roles cannot be deleted"). Custom roles use `ConfirmDeleteDialog` with role name → `useDeleteRole()`.
|
||||
- No assignment UI on the roles tab — assignments are managed from the User and Group detail panes.
|
||||
|
||||
### 4. Multi-Select Dropdown Component
|
||||
|
||||
A reusable component used across all assignment actions:
|
||||
|
||||
```
|
||||
Props:
|
||||
items: { id: string; label: string }[] — available items to pick from
|
||||
onApply: (selectedIds: string[]) => void — called with all checked IDs
|
||||
placeholder?: string — search filter placeholder
|
||||
```
|
||||
|
||||
Behavior:
|
||||
- Opens as a positioned dropdown below the "+ Add" chip
|
||||
- Search/filter input at top
|
||||
- Checkbox list of items (max-height with scroll)
|
||||
- "Apply" button at bottom (disabled when nothing selected)
|
||||
- Closes on Apply, Escape, or click-outside
|
||||
- Shows count badge on Apply button: "Apply (3)"
|
||||
|
||||
Styling: background `var(--bg-raised)`, border `var(--border)`, border-radius `var(--radius-md)`, items with `var(--bg-hover)` on hover, checkboxes with `var(--amber)` accent.
|
||||
|
||||
### 5. Inline Create Form
|
||||
|
||||
A reusable pattern for "Add group" and "Add role":
|
||||
|
||||
- Appears below the search bar in the list pane, pushing content down
|
||||
- Input fields with labels
|
||||
- "Create" and "Cancel" buttons
|
||||
- On success: closes form, clears inputs, new entity appears in list
|
||||
- On error: shows error message inline
|
||||
- "Create" button disabled while mutation is in-flight
|
||||
|
||||
### 6. Built-in Admins Group Seed
|
||||
|
||||
**Database migration** — new `V2__admin_group_seed.sql` (V1 is already deployed, V2-V10 were deleted in the migration consolidation so V2 is safe):
|
||||
|
||||
```sql
|
||||
-- Built-in Admins group
|
||||
INSERT INTO groups (id, name) VALUES
|
||||
('00000000-0000-0000-0000-000000000010', 'Admins');
|
||||
|
||||
-- Assign ADMIN role to Admins group
|
||||
INSERT INTO group_roles (group_id, role_id) VALUES
|
||||
('00000000-0000-0000-0000-000000000010', '00000000-0000-0000-0000-000000000004');
|
||||
```
|
||||
|
||||
**SystemRole.java** — add constants:
|
||||
```java
|
||||
public static final UUID ADMINS_GROUP_ID = UUID.fromString("00000000-0000-0000-0000-000000000010");
|
||||
```
|
||||
|
||||
**UiAuthController.login()** — after upserting the user and assigning ADMIN role, also add to Admins group:
|
||||
```java
|
||||
rbacService.addUserToGroup(subject, SystemRole.ADMINS_GROUP_ID);
|
||||
```
|
||||
|
||||
**Frontend guard:** The Admins group UUID is hardcoded as a constant in the frontend to disable deletion. Alternatively, check if a group's ID matches a known system group ID.
|
||||
|
||||
### 7. Dashboard Diagram Ordering
|
||||
|
||||
The inheritance diagram's three columns (Groups → Roles → Users) must show items in a consistent, matching order:
|
||||
|
||||
- **Groups column**: alphabetical by name, children indented under parents
|
||||
- **Roles column**: iterate groups top-to-bottom, collect their direct roles, deduplicate preserving first-seen order. Roles not assigned to any group are omitted from the diagram.
|
||||
- **Users column**: alphabetical by display name
|
||||
|
||||
Sort explicitly in `DashboardTab.tsx` before rendering.
|
||||
|
||||
---
|
||||
|
||||
## Files Changed
|
||||
|
||||
### Frontend — Modified
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `ui/src/pages/admin/rbac/UsersTab.tsx` | Delete button, group/role assignment dropdowns, date format fix, self-delete guard |
|
||||
| `ui/src/pages/admin/rbac/GroupsTab.tsx` | Add group form, delete button, role assignment dropdown, parent group dropdown, Admins guard |
|
||||
| `ui/src/pages/admin/rbac/RolesTab.tsx` | Add role form, delete button (disabled for system) |
|
||||
| `ui/src/pages/admin/rbac/DashboardTab.tsx` | Sort diagram columns consistently |
|
||||
| `ui/src/pages/admin/rbac/RbacPage.module.css` | Styles for multi-select dropdown, inline create form, delete button, action chips, remove buttons |
|
||||
|
||||
### Frontend — New
|
||||
| File | Responsibility |
|
||||
|---|---|
|
||||
| `ui/src/pages/admin/rbac/components/MultiSelectDropdown.tsx` | Reusable multi-select picker with search, checkboxes, batch apply |
|
||||
|
||||
### Backend — Modified
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `cameleer3-server-core/.../rbac/SystemRole.java` | Add `ADMINS_GROUP_ID` constant |
|
||||
| `cameleer3-server-app/.../security/UiAuthController.java` | Add admin user to Admins group on login |
|
||||
|
||||
### Backend — New Migration
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `cameleer3-server-app/src/main/resources/db/migration/V2__admin_group_seed.sql` | Seed Admins group + ADMIN role assignment |
|
||||
|
||||
---
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Editing user profile fields (name, email) — users are managed by their identity provider
|
||||
- Drag-and-drop group hierarchy management
|
||||
- Role permission editing (custom roles have no effect on Spring Security yet)
|
||||
327
docs/superpowers/specs/2026-03-17-rbac-management-design.md
Normal file
327
docs/superpowers/specs/2026-03-17-rbac-management-design.md
Normal file
@@ -0,0 +1,327 @@
|
||||
# RBAC Management — Design Specification
|
||||
|
||||
## Goal
|
||||
|
||||
Implement a full RBAC management system (issue #41) with group hierarchy, role inheritance, and a management UI integrated into the admin section. Replace the flat `users.roles` text array with a proper relational model.
|
||||
|
||||
## References
|
||||
|
||||
- Functional spec: `examples/RBAC/rbac-ui-spec.md`
|
||||
- Visual prototype: `examples/RBAC/rbac_management_ui.html`
|
||||
|
||||
---
|
||||
|
||||
## Backend
|
||||
|
||||
### Database Schema
|
||||
|
||||
Squash V1–V10 Flyway migrations into a single `V1__init.sql`. The `users` table drops the `roles TEXT[]` column. New tables:
|
||||
|
||||
```sql
|
||||
CREATE TABLE users (
|
||||
user_id TEXT PRIMARY KEY,
|
||||
provider TEXT NOT NULL,
|
||||
email TEXT,
|
||||
display_name TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- RBAC: all roles — system roles seeded with fixed UUIDs, custom roles created by admins
|
||||
CREATE TABLE roles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
scope TEXT NOT NULL DEFAULT 'custom',
|
||||
system BOOLEAN NOT NULL DEFAULT false,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- Seed system roles with fixed UUIDs (stable across environments)
|
||||
INSERT INTO roles (id, name, description, scope, system) VALUES
|
||||
('00000000-0000-0000-0000-000000000001', 'AGENT', 'Agent registration and data ingestion', 'system-wide', true),
|
||||
('00000000-0000-0000-0000-000000000002', 'VIEWER', 'Read-only access to dashboards and data', 'system-wide', true),
|
||||
('00000000-0000-0000-0000-000000000003', 'OPERATOR', 'Operational commands (start/stop/configure agents)', 'system-wide', true),
|
||||
('00000000-0000-0000-0000-000000000004', 'ADMIN', 'Full administrative access', 'system-wide', true);
|
||||
|
||||
-- RBAC: groups with self-referential hierarchy
|
||||
CREATE TABLE groups (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
parent_group_id UUID REFERENCES groups(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- Join: roles assigned to groups (system + custom)
|
||||
CREATE TABLE group_roles (
|
||||
group_id UUID NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
|
||||
role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (group_id, role_id)
|
||||
);
|
||||
|
||||
-- Join: direct group membership for users
|
||||
CREATE TABLE user_groups (
|
||||
user_id TEXT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
|
||||
group_id UUID NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (user_id, group_id)
|
||||
);
|
||||
|
||||
-- Join: direct role assignments to users (system + custom)
|
||||
CREATE TABLE user_roles (
|
||||
user_id TEXT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
|
||||
role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (user_id, role_id)
|
||||
);
|
||||
|
||||
-- Indexes for join query performance
|
||||
CREATE INDEX idx_user_roles_user_id ON user_roles(user_id);
|
||||
CREATE INDEX idx_user_groups_user_id ON user_groups(user_id);
|
||||
CREATE INDEX idx_group_roles_group_id ON group_roles(group_id);
|
||||
CREATE INDEX idx_groups_parent ON groups(parent_group_id);
|
||||
```
|
||||
|
||||
Note: `roles TEXT[]` column is removed from `users`. All roles (system and custom) live in the `roles` table. System roles are seeded rows with `system = true` and fixed UUIDs — the application prevents their deletion or modification.
|
||||
|
||||
### System Roles
|
||||
|
||||
The four system roles (AGENT, VIEWER, OPERATOR, ADMIN) are:
|
||||
- Seeded as rows in the `roles` table with `system = true` and fixed UUIDs
|
||||
- Assigned to users via the same `user_roles` join table as custom roles
|
||||
- Protected by application logic: creation, deletion, and name/scope modification are rejected
|
||||
- Displayed in the UI as read-only entries (lock icon, non-deletable)
|
||||
- Used by Spring Security / JWT for authorization decisions
|
||||
|
||||
Custom roles (`system = false`) are application-defined and have no effect on Spring Security — they serve the RBAC management model only (for future permission expansion).
|
||||
|
||||
The `scope` field distinguishes role domains: system roles use `system-wide`, custom roles can use descriptive scopes like `monitoring:read`, `config:write` for future permission gating.
|
||||
|
||||
### Domain Model (Java)
|
||||
|
||||
```java
|
||||
// Existing, modified — drop roles field
|
||||
public record UserInfo(
|
||||
String userId,
|
||||
String provider,
|
||||
String email,
|
||||
String displayName,
|
||||
Instant createdAt
|
||||
) {}
|
||||
|
||||
// New — enriched user for admin API responses
|
||||
public record UserDetail(
|
||||
String userId,
|
||||
String provider,
|
||||
String email,
|
||||
String displayName,
|
||||
Instant createdAt,
|
||||
List<RoleSummary> directRoles, // from user_roles join (system + custom)
|
||||
List<GroupSummary> directGroups, // from user_groups join
|
||||
List<RoleSummary> effectiveRoles, // computed: union of direct + inherited via groups
|
||||
List<GroupSummary> effectiveGroups // computed: direct groups + their ancestor chain
|
||||
) {}
|
||||
|
||||
public record GroupDetail(
|
||||
UUID id,
|
||||
String name,
|
||||
UUID parentGroupId, // nullable
|
||||
Instant createdAt,
|
||||
List<RoleSummary> directRoles,
|
||||
List<RoleSummary> effectiveRoles, // direct + inherited from parent chain
|
||||
List<UserSummary> members, // direct members
|
||||
List<GroupSummary> childGroups
|
||||
) {}
|
||||
|
||||
public record RoleDetail(
|
||||
UUID id,
|
||||
String name,
|
||||
String description,
|
||||
String scope,
|
||||
boolean system, // true for AGENT/VIEWER/OPERATOR/ADMIN
|
||||
Instant createdAt,
|
||||
List<GroupSummary> assignedGroups,
|
||||
List<UserSummary> directUsers,
|
||||
List<UserSummary> effectivePrincipals // all users who hold this role
|
||||
) {}
|
||||
|
||||
// Summaries for embedding in detail responses
|
||||
public record UserSummary(String userId, String displayName, String provider) {}
|
||||
public record GroupSummary(UUID id, String name) {}
|
||||
public record RoleSummary(UUID id, String name, boolean system, String source) {}
|
||||
// source: "direct" | group name (for inherited)
|
||||
```
|
||||
|
||||
### Inheritance Logic
|
||||
|
||||
Server-side computation in a service class (e.g., `RbacService`):
|
||||
|
||||
1. **Effective groups for user**: Start from `user_groups` (direct memberships), then for each group walk `parent_group_id` chain upward to collect all ancestor groups. The union is every group the user is transitively a member of.
|
||||
2. **Effective roles for user**: Direct `user_roles` + all `group_roles` for every effective group. Both system and custom roles flow through the same path.
|
||||
3. **Effective roles for group**: Direct `group_roles` + inherited from parent chain.
|
||||
4. **Effective principals for role**: All users who hold the role directly + all users in any group that has the role (transitively).
|
||||
|
||||
No role negation — roles only grant, never deny.
|
||||
|
||||
**Cycle detection**: When setting `parent_group_id` on a group, the application must walk the proposed parent chain upward and reject the update if it would create a cycle (i.e., the group appears in its own ancestor chain). Return HTTP 409 Conflict.
|
||||
|
||||
### Auth Integration
|
||||
|
||||
`JwtService` and `SecurityConfig` read system roles from `user_roles` joined to `roles WHERE system = true`, instead of `users.roles`. The `UserRepository` methods that currently read/write `users.roles` are updated to use the join table. JWT claims remain unchanged (`roles: ["ADMIN", "VIEWER"]`).
|
||||
|
||||
OIDC auto-signup: When a user is auto-registered via OIDC token exchange, they get a row in `users` with `provider = "oidc:<issuer>"` and a default system role (VIEWER) via `user_roles`. No group membership by default.
|
||||
|
||||
### API Endpoints
|
||||
|
||||
All under `/api/v1/admin/` prefix, protected by `@PreAuthorize("hasRole('ADMIN')")`.
|
||||
|
||||
The existing `PUT /users/{userId}/roles` bulk endpoint is removed. Role assignments use individual add/remove endpoints.
|
||||
|
||||
All mutation endpoints log to the `AuditService` (category: `USER_MGMT` for user operations, `RBAC` for group/role operations).
|
||||
|
||||
**Users** — response type: `UserDetail`
|
||||
|
||||
| Method | Path | Description | Request Body |
|
||||
|---|---|---|---|
|
||||
| GET | `/users` | List all users with effective roles/groups | — |
|
||||
| GET | `/users/{id}` | Full user detail | — |
|
||||
| POST | `/users/{id}/roles/{roleId}` | Assign role to user (system or custom) | — |
|
||||
| DELETE | `/users/{id}/roles/{roleId}` | Remove role from user | — |
|
||||
| POST | `/users/{id}/groups/{groupId}` | Add user to group | — |
|
||||
| DELETE | `/users/{id}/groups/{groupId}` | Remove user from group | — |
|
||||
| DELETE | `/users/{id}` | Delete user | — |
|
||||
|
||||
**Groups** — response type: `GroupDetail`
|
||||
|
||||
| Method | Path | Description | Request Body |
|
||||
|---|---|---|---|
|
||||
| GET | `/groups` | List all groups with hierarchy | — |
|
||||
| GET | `/groups/{id}` | Full group detail | — |
|
||||
| POST | `/groups` | Create group | `{ name, parentGroupId? }` |
|
||||
| PUT | `/groups/{id}` | Update group | `{ name?, parentGroupId? }` — returns 409 on cycle |
|
||||
| DELETE | `/groups/{id}` | Delete group — cascades role/member associations; child groups become top-level (parent set to null) | — |
|
||||
| POST | `/groups/{id}/roles/{roleId}` | Assign role to group | — |
|
||||
| DELETE | `/groups/{id}/roles/{roleId}` | Remove role from group | — |
|
||||
|
||||
**Roles** — response type: `RoleDetail`
|
||||
|
||||
| Method | Path | Description | Request Body |
|
||||
|---|---|---|---|
|
||||
| GET | `/roles` | List all roles (system + custom) | — |
|
||||
| GET | `/roles/{id}` | Role detail | — |
|
||||
| POST | `/roles` | Create custom role | `{ name, description?, scope? }` |
|
||||
| PUT | `/roles/{id}` | Update custom role (rejects system roles) | `{ name?, description?, scope? }` |
|
||||
| DELETE | `/roles/{id}` | Delete custom role (rejects system roles) | — |
|
||||
|
||||
**Dashboard:**
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| GET | `/rbac/stats` | `{ userCount, activeUserCount, groupCount, maxGroupDepth, roleCount }` |
|
||||
|
||||
---
|
||||
|
||||
## Frontend
|
||||
|
||||
### Routing
|
||||
|
||||
New route at `/admin/rbac` in `router.tsx`, lazy-loaded:
|
||||
|
||||
```tsx
|
||||
const RbacPage = lazy(() => import('./pages/admin/rbac/RbacPage').then(m => ({ default: m.RbacPage })));
|
||||
// ...
|
||||
{ path: 'admin/rbac', element: <Suspense fallback={null}><RbacPage /></Suspense> }
|
||||
```
|
||||
|
||||
Update `AppSidebar` ADMIN_LINKS to add `{ to: '/admin/rbac', label: 'User Management' }`.
|
||||
|
||||
### Component Structure
|
||||
|
||||
```
|
||||
pages/admin/rbac/
|
||||
├── RbacPage.tsx ← ADMIN role gate + tab navigation
|
||||
├── RbacPage.module.css ← All RBAC-specific styles
|
||||
├── DashboardTab.tsx ← Stat cards + inheritance diagram
|
||||
├── UsersTab.tsx ← Split pane orchestrator
|
||||
├── GroupsTab.tsx ← Split pane orchestrator
|
||||
├── RolesTab.tsx ← Split pane orchestrator
|
||||
├── components/
|
||||
│ ├── EntityListPane.tsx ← Reusable: search input + scrollable card list
|
||||
│ ├── EntityCard.tsx ← Single list row: avatar, name, meta, tags, status dot
|
||||
│ ├── UserDetail.tsx ← Header, fields, groups, effective roles, group tree
|
||||
│ ├── GroupDetail.tsx ← Header, fields, members, children, roles, hierarchy
|
||||
│ ├── RoleDetail.tsx ← Header, fields, assigned groups/users, effective principals
|
||||
│ ├── InheritanceChip.tsx ← Chip with dashed border + "↑ Source" annotation
|
||||
│ ├── GroupTree.tsx ← Indented tree with corner connectors
|
||||
│ ├── EntityAvatar.tsx ← Circle (user), rounded-square (group/role), color by type
|
||||
│ ├── OidcBadge.tsx ← Small badge showing OIDC provider origin
|
||||
│ ├── InheritanceDiagram.tsx ← Three-column Groups→Roles→Users read-only diagram
|
||||
│ └── InheritanceNote.tsx ← Green-bordered explanation block
|
||||
api/queries/admin/
|
||||
│ └── rbac.ts ← useUsers, useUser, useGroups, useGroup, useRoles, useRole, useRbacStats + mutation hooks
|
||||
```
|
||||
|
||||
### Tab Navigation
|
||||
|
||||
`RbacPage` uses a horizontal tab bar (Dashboard | Users | Groups | Roles) with URL-synced active state via query parameter (`?tab=users`). Each tab renders its content below the tab bar in the full main panel area.
|
||||
|
||||
### Split Pane Layout
|
||||
|
||||
Users, Groups, and Roles tabs share the same layout:
|
||||
- **Left (52%)**: `EntityListPane` with search input + scrollable entity cards
|
||||
- **Right (48%)**: Detail pane showing selected entity, or empty state prompt
|
||||
- Resizable via `ResizableDivider` (existing shared component)
|
||||
|
||||
### Entity Card Patterns
|
||||
|
||||
**User card:** Circle avatar (initials, blue tint) + name + email/primary-group meta + role tags (amber) + group tags (green) + status dot + OIDC badge if `provider !== "local"`
|
||||
|
||||
**Group card:** Rounded-square avatar (initials, green/amber/red by domain) + name + parent/member-count meta + role tags (direct solid, inherited faded+italic)
|
||||
|
||||
**Role card:** Rounded-square avatar (initials, amber tint) + name + description/assignment-count meta + assigned-to tags. System roles show a lock icon.
|
||||
|
||||
### Badge/Chip Styling
|
||||
|
||||
Following the spec and existing CSS token system:
|
||||
|
||||
| Chip type | Background | Border | Text |
|
||||
|---|---|---|---|
|
||||
| Role (direct) | `var(--amber-dim)` | solid `var(--amber)` | amber text |
|
||||
| Role (inherited) | transparent | dashed `var(--amber)` | faded amber, italic |
|
||||
| Group | `var(--green-dim)` / `#E1F5EE` | solid green | green text |
|
||||
| OIDC badge | `var(--cyan-dim)` | solid cyan | cyan text, shows provider |
|
||||
| System role | Same as role but with lock icon | — | — |
|
||||
|
||||
Inherited role chips include `↑ GroupName` annotation in the detail pane.
|
||||
|
||||
### OIDC Badge
|
||||
|
||||
Displayed on user cards and user detail when `provider !== "local"`. Shows a small cyan-tinted pill with the provider name (e.g., "OIDC" or the issuer hostname). Positioned after the user's name in the card, and as a field in the detail pane.
|
||||
|
||||
### Search
|
||||
|
||||
Client-side filtering on entity list panes — filter by any visible text (name, email, group, role). Sufficient for the expected user count.
|
||||
|
||||
### State Management
|
||||
|
||||
- React Query for all server state (users, groups, roles, stats)
|
||||
- Local `useState` for selected entity, search filter, active tab
|
||||
- Mutations invalidate related queries (e.g., updating a user's groups invalidates both user and group queries)
|
||||
|
||||
---
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
1. Delete all V1–V10 migration files
|
||||
2. Create single `V1__init.sql` containing the full consolidated schema
|
||||
3. Deployed environments: drop and recreate the database (data loss accepted)
|
||||
4. CI/CD: no special handling — clean database on deploy
|
||||
5. Update `application.yml` if needed: `spring.flyway.clean-on-validation-error: true` or manual DB drop
|
||||
|
||||
---
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Permission-based access control (custom roles don't gate endpoints — system roles do)
|
||||
- Audit log panel within RBAC (existing audit log page covers this)
|
||||
- Bulk import/export of users or groups
|
||||
- SCIM provisioning
|
||||
- Role negation / deny rules
|
||||
Reference in New Issue
Block a user