Skip to content

Commit 5b6a832

Browse files
Add RBAC with AuditedTxn transactional audit enforcement
Implement role-based access control (roles, members, inheritance, datasource role access) with scoped policy assignments and admin audit logging. Introduces AuditedTxn wrapper that enforces audit entries live inside the transaction boundary — commit() rejects empty audit queues, drop rolls back both entity mutations and audit entries. Key changes: - Role CRUD, membership, inheritance (cycle detection, depth cap of 10) - data_source_access replaces user_data_source (scopes: user/role/all) - policy_assignment gains role_id and assignment_scope columns - role_resolver: BFS role resolution, effective members/assignments/access - AuditedTxn: all mutation handlers use transactional audit (fixes audit outside txn in set_datasource_users, create/update_role, delete_role) - delete_role audits cascaded policy assignment removals - remove_parent invalidates cache after commit, not before - set_datasource_role_access validates roles before mutations - Admin UI: roles list/create/edit pages, member/inheritance/access panels, centralized admin audit page with datetime filter fix - 18 migrations (024-041), 73 integration tests, 6 AuditedTxn unit tests
1 parent 7cda918 commit 5b6a832

73 files changed

Lines changed: 8991 additions & 252 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/CLAUDE.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,47 @@ git config core.hooksPath .githooks
3232

3333
Use `/release` to prepare the changelog, bump versions, commit, and tag. Use `/commit` for day-to-day commits.
3434

35+
## Planning & Feature Design
36+
37+
### Design-First, Discuss Before Building
38+
For any non-trivial feature, enter plan mode and work through the design iteratively with the user before writing code. Don't jump to implementation — discuss trade-offs, edge cases, and security implications first. The goal is alignment on approach before any code is written.
39+
40+
**Planning workflow:**
41+
1. **Explore** — read the relevant code paths end-to-end. Understand what exists before proposing what to build.
42+
2. **Design** — propose the approach with concrete trade-offs. Present options with pros/cons, not just one solution.
43+
3. **Discuss** — ask the user targeted questions about design decisions. Don't make assumptions on ambiguous points. Use AskUserQuestion for specific choices, not open-ended "what do you think?" questions.
44+
4. **Harden** — after the core design is agreed, proactively ask: "What else can we improve?" Look for security gaps, edge cases, performance concerns, and missing test coverage. Iterate until the user says "enough."
45+
5. **Finalize** — write the plan with all decisions documented, then exit plan mode.
46+
47+
### Test Vector Design During Planning (Non-Optional)
48+
Every feature plan MUST include a comprehensive test case inventory before implementation begins. Tests are designed during planning, not added as an afterthought. The test cases serve as the specification — if you can't write the test case, you don't understand the feature well enough.
49+
50+
**Systematic test categories to cover for every feature:**
51+
52+
| Category | What to ask | Examples |
53+
|----------|------------|---------|
54+
| **Happy path** | Does the basic flow work? | CRUD operations, expected inputs, normal usage |
55+
| **Attack vectors** | Can it be exploited? | SQL injection, parameter tampering, scope mismatches, privilege escalation |
56+
| **Deny-wins / security invariants** | Do security guarantees hold? | Deny overrides allow, deactivation blocks access, audit can't be tampered |
57+
| **State interactions** | How does it interact with existing features? | is_active flags, is_enabled flags, access_mode, template variables |
58+
| **FK cascades / data integrity** | What happens when related entities are deleted? | Delete parent → child cleanup, unique constraint violations |
59+
| **Cache consistency** | Do changes take effect immediately? | Mutation → cache invalidation → next query reflects change |
60+
| **Timing / concurrency** | What about race conditions? | Mid-session changes, concurrent mutations, rapid successive operations |
61+
| **Edge cases** | What about boundary conditions? | Empty sets, max lengths, zero members, duplicate entries |
62+
| **API validation** | Are invalid inputs rejected? | Missing fields, wrong types, out-of-range values, conflicting parameters |
63+
| **Audit integrity** | Are all mutations tracked? | Every CRUD op logged, correct actor, accurate before/after snapshots |
64+
| **Multi-entity interaction** | How do multiple instances interact? | Multiple roles, multiple datasources, overlapping policies, priority conflicts |
65+
| **Backward compatibility** | Does existing functionality still work? | Old API formats, migration of existing data, default values |
66+
67+
**Test naming convention:** Group tests by category with descriptive names. Map security-relevant tests to vector numbers in `docs/permission-security-tests.md`.
68+
69+
### Security-First Thinking
70+
This is a data security product. Every feature that touches access control, policy resolution, or data visibility must be evaluated through a security lens:
71+
- **What can an attacker do?** — enumerate bypass vectors before building defenses
72+
- **What breaks when state changes?** — deactivation, deletion, membership changes, policy mutations
73+
- **What's the blast radius?** — how many users/connections are affected by a change?
74+
- **Is the audit trail complete?** — can every mutation be traced back to who did it and when?
75+
3576
## Migrations (`migration/`)
3677

3778
### Rules (violations here cause hard-to-fix production incidents)

README.md

Lines changed: 66 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ psql / app
117117
↓ PostgreSQL wire protocol (port 5434)
118118
QueryProxy (Rust)
119119
├─ Authenticates user (Argon2id)
120-
├─ Checks data source access (user_data_source table)
120+
├─ Checks data source access (data_source_access table — direct, role-based, or all)
121121
├─ Runs query hook pipeline:
122122
│ ReadOnlyHook — blocks writes (SQLSTATE 25006)
123123
│ PolicyHook — row filters, column masks, column access control
@@ -147,7 +147,7 @@ Upstream PostgreSQL
147147
```
148148
betweenrows/
149149
├── Cargo.toml workspace root (proxy, migration crates)
150-
├── migration/src/ SeaORM migrations (7 total)
150+
├── migration/src/ SeaORM migrations (41 total)
151151
├── docs/ User-facing documentation
152152
│ ├── permission-system.md Policy system user guide
153153
│ ├── permission-security-tests.md Security test plan
@@ -159,9 +159,10 @@ betweenrows/
159159
│ ├── api/ axios + fetch-event-source clients
160160
│ ├── auth/ AuthContext, ProtectedRoute, LoginPage
161161
│ ├── components/ Layout, DataSourceForm, CatalogDiscoveryWizard,
162-
│ │ PolicyForm, PolicyAssignmentPanel, …
162+
│ │ PolicyForm, PolicyAssignmentPanel, RoleForm,
163+
│ │ RoleMemberPanel, RoleInheritancePanel, AuditTimeline, …
163164
│ ├── pages/ Users*, DataSources*, DataSourceCatalogPage,
164-
│ │ Policies*, QueryAuditPage
165+
│ │ Policies*, Roles*, QueryAuditPage
165166
│ └── types/ TypeScript interfaces
166167
└── proxy/src/
167168
├── main.rs entry point: CLI, DB init, EngineCache, servers
@@ -170,11 +171,14 @@ betweenrows/
170171
├── auth.rs Argon2 auth, user creation
171172
├── crypto.rs AES-256-GCM encrypt/decrypt
172173
├── admin/ REST API: mod, dto, jwt, handlers, discovery_job,
173-
│ policy_handlers, audit_handlers, policy_yaml
174+
│ policy_handlers, role_handlers, audit_handlers,
175+
│ admin_audit
174176
├── discovery/ DiscoveryProvider trait + Postgres impl
175-
├── entity/ SeaORM entities (proxy_user, data_source, policy,
176-
│ policy_assignment, policy_version,
177-
│ query_audit_log, …)
177+
├── entity/ SeaORM entities (proxy_user, data_source, role,
178+
│ role_member, role_inheritance, data_source_access,
179+
│ policy, policy_assignment, policy_version,
180+
│ admin_audit_log, query_audit_log, …)
181+
├── role_resolver.rs BFS role resolution, cycle detection, effective assignments
178182
├── engine/mod.rs EngineCache, VirtualCatalogProvider, build_arrow_schema()
179183
└── hooks/ QueryHook trait, ReadOnlyHook, PolicyHook
180184
```
@@ -301,16 +305,16 @@ After authentication succeeds in `handler.rs`, a background task pre-builds the
301305
Access control is enforced **before** any query reaches the engine:
302306

303307
1. `validate_data_source()` — datasource must exist and be active
304-
2. `check_access(user_id, datasource_name)` — user must have an explicit `user_data_source` row
308+
2. `check_access(user_id, datasource_name)` — user must have access via `data_source_access` (direct, role-based, or all-scoped)
305309
3. If either check fails → `FATAL` PG error, connection rejected before `get_ctx()` is ever called
306310

307311
### Why the Shared Pool Is Safe
308312

309313
The upstream connection pool carries **no user identity** — it is pure TCP connectivity to the upstream Postgres server. All identity and access decisions are made at the pgwire auth layer (steps 1–2 above), not at the pool layer.
310314

311315
Per-user isolation is enforced by:
312-
- **Data plane**`user_data_source` allowlist (no row → connection rejected)
313-
- **RLS hook** — per-query `WHERE tenant = '<value>'` filter injected via DataFusion's logical plan tree, based on the authenticated user's tenant metadata
316+
- **Data plane**`data_source_access` allowlist (no matching row → connection rejected). Access can be granted directly to a user, via role membership (including inherited roles), or to all users.
317+
- **Policy hook** — per-query row filters, column masks, and access controls injected via DataFusion's logical plan tree, based on the authenticated user's policy assignments (direct, role-based, or wildcard)
314318
- **Virtual catalog** — the stored catalog is an allowlist; tables/columns not explicitly saved are invisible to the engine
315319

316320
The shared pool is safe for all authorized users of a datasource: Pool = "how to talk to upstream". Auth + RLS = "what this user can see". These are orthogonal.
@@ -328,16 +332,16 @@ QueryProxy enforces a two-layer access control model:
328332
**Management plane** — controlled by `is_admin` flag. Admins manage users, data sources, policies, and catalogs via the Admin API. Non-admins have no Admin API access.
329333

330334
**Data plane** — controlled by two independent mechanisms:
331-
1. *Connection access*explicit `user_data_source` assignment. A user can only connect to a datasource with an explicit row. Being an admin does **not** automatically grant data plane access.
332-
2. *Query policy*`PolicyHook` applies row filters, column masks, and column access controls per-query based on assigned policies. If the datasource `access_mode` is `"policy_required"`, tables with no matching permit policy return empty results.
335+
1. *Connection access*`data_source_access` entries. A user can connect to a datasource via direct assignment, role membership (including inherited roles), or all-user scope. Being an admin does **not** automatically grant data plane access.
336+
2. *Query policy*`PolicyHook` applies row filters, column masks, and column access controls per-query based on assigned policies (direct, role-based, or all-scoped). If the datasource `access_mode` is `"policy_required"`, tables with no matching permit policy return empty results.
333337

334338
See `docs/permission-system.md` for the full policy system user guide.
335339

336340
Connection flow:
337341
1. Client connects: `psql -d <datasource_name> -U <username>`
338342
2. Proxy authenticates (Argon2id)
339343
3. Proxy validates data source exists and is active
340-
4. Proxy checks `user_data_source` — denied if no row
344+
4. Proxy checks `data_source_access` — denied if no matching row (direct, role, or all scope)
341345
5. Background task pre-warms `SessionContext` + pool
342346
6. First query: fast path — context and pool already ready
343347

@@ -377,7 +381,24 @@ All endpoints require `Authorization: Bearer <token>` (obtained from `/auth/logi
377381
| DELETE | `/datasources/{id}` | Delete data source |
378382
| POST | `/datasources/{id}/test` | Test upstream connection |
379383
| GET | `/datasources/{id}/users` | List assigned users |
380-
| PUT | `/datasources/{id}/users` | Replace user assignments |
384+
| PUT | `/datasources/{id}/users` | Replace user assignments (user-scoped access) |
385+
| PUT | `/datasources/{id}/access/roles` | Set role-based access `{ role_ids: [uuid] }` |
386+
387+
### Roles
388+
389+
| Method | Path | Description |
390+
|--------|------|-------------|
391+
| GET | `/roles` | List roles (paginated, searchable) |
392+
| POST | `/roles` | Create role `{ name, description? }` |
393+
| GET | `/roles/{id}` | Get role + members + inheritance + policy assignments |
394+
| PUT | `/roles/{id}` | Update name/description/is_active |
395+
| DELETE | `/roles/{id}` | Delete role → returns impact `{ affected_users, affected_assignments }` |
396+
| GET | `/roles/{id}/effective-members` | All users inheriting policies (direct + inherited), with source |
397+
| GET | `/roles/{id}/impact` | Preview impact of deleting this role |
398+
| POST | `/roles/{id}/members` | Add members `{ user_ids: [uuid] }` |
399+
| DELETE | `/roles/{id}/members/{user_id}` | Remove member |
400+
| POST | `/roles/{id}/parents` | Add parent `{ parent_role_id }` (cycle detection + depth check) |
401+
| DELETE | `/roles/{id}/parents/{parent_id}` | Remove parent |
381402

382403
### Catalog Discovery
383404

@@ -431,14 +452,21 @@ All policy endpoints require admin (`is_admin = true`).
431452
| GET | `/policies/export` | Export all policies as YAML |
432453
| POST | `/policies/import` | Import YAML (`?dry_run=true` to preview) |
433454
| GET | `/datasources/{id}/policies` | List policy assignments for datasource |
434-
| POST | `/datasources/{id}/policies` | Assign policy to datasource (optionally scoped to a user) |
455+
| POST | `/datasources/{id}/policies` | Assign policy to datasource (scope: user/role/all) |
435456
| DELETE | `/datasources/{id}/policies/{assignment_id}` | Remove assignment |
436457

437458
### Audit Log
438459

439460
| Method | Path | Description |
440461
|--------|------|-------------|
441-
| GET | `/audit/queries` | Paginated query audit log (filter by user, datasource, date range) |
462+
| GET | `/audit/queries` | Paginated query audit log (filter by user, datasource, date range, status) |
463+
| GET | `/audit/admin` | Paginated admin audit log (filter by resource_type, resource_id, actor_id, date range) |
464+
465+
### Effective Policies
466+
467+
| Method | Path | Description |
468+
|--------|------|-------------|
469+
| GET | `/users/{id}/effective-policies?datasource_id=X` | All policies applying to user (with source annotation) |
442470

443471
## Catalog Workflow
444472

@@ -458,21 +486,26 @@ The catalog is an **allowlist** — the proxy can never expose tables or columns
458486
All primary keys are UUIDs. The admin store uses SQLite by default (configurable via `DATABASE_URL`).
459487

460488
```
461-
proxy_user (id UUID, username, password_hash, tenant, is_admin, is_active, …)
462-
data_source (id UUID, name, ds_type, config JSON, secure_config encrypted,
463-
is_active, access_mode, last_sync_at, last_sync_result, …)
464-
user_data_source (id UUID, user_id → proxy_user, data_source_id → data_source)
465-
discovered_schema (id UUID v5, data_source_id, schema_name, is_selected)
466-
discovered_table (id UUID v5, discovered_schema_id, table_name, table_type, is_selected)
467-
discovered_column (id UUID v5, discovered_table_id, column_name, ordinal_position,
468-
data_type, is_nullable, column_default, arrow_type)
469-
470-
policy (id UUID v7, name, description, policy_type, is_enabled, version, targets JSON, definition JSON, …)
471-
policy_version (id UUID v7, policy_id, version, snapshot JSON, change_type, changed_by)
472-
policy_assignment (id UUID v7, policy_id, data_source_id, user_id?, priority)
473-
query_audit_log (id UUID v7, user_id, username, data_source_id, datasource_name,
474-
original_query, rewritten_query, policies_applied JSON,
475-
execution_time_ms, client_ip, client_info, created_at)
489+
proxy_user (id UUID, username, password_hash, tenant, is_admin, is_active, …)
490+
data_source (id UUID, name, ds_type, config JSON, secure_config encrypted,
491+
is_active, access_mode, last_sync_at, last_sync_result, …)
492+
data_source_access (id UUID, user_id?, role_id?, data_source_id, assignment_scope, …)
493+
role (id UUID, name UNIQUE, description, is_active, …)
494+
role_member (id UUID, role_id → role, user_id → proxy_user)
495+
role_inheritance (id UUID, parent_role_id → role, child_role_id → role)
496+
discovered_schema (id UUID v5, data_source_id, schema_name, is_selected)
497+
discovered_table (id UUID v5, discovered_schema_id, table_name, table_type, is_selected)
498+
discovered_column (id UUID v5, discovered_table_id, column_name, ordinal_position,
499+
data_type, is_nullable, column_default, arrow_type)
500+
501+
policy (id UUID v7, name, description, policy_type, is_enabled, version, targets JSON, definition JSON, …)
502+
policy_version (id UUID v7, policy_id, version, snapshot JSON, change_type, changed_by)
503+
policy_assignment (id UUID v7, policy_id, data_source_id, user_id?, role_id?,
504+
assignment_scope, priority)
505+
admin_audit_log (id UUID v7, resource_type, resource_id, action, actor_id, changes JSON, created_at)
506+
query_audit_log (id UUID v7, user_id, username, data_source_id, datasource_name,
507+
original_query, rewritten_query, policies_applied JSON,
508+
execution_time_ms, client_ip, client_info, created_at)
476509
```
477510

478511
Catalog entity IDs (schemas, tables, columns) are deterministic UUID v5 fingerprints derived from their natural keys. Re-discovering the same upstream object always produces the same ID, so re-syncs are safe upserts.
@@ -481,6 +514,6 @@ Catalog entity IDs (schemas, tables, columns) are deterministic UUID v5 fingerpr
481514

482515
```bash
483516
cargo build -p proxy # compile
484-
cargo test -p proxy # run tests (101 unit tests)
517+
cargo test -p proxy # run tests (213 unit tests + integration tests)
485518
cd admin-ui && npm run build # production UI bundle
486519
```

admin-ui/CLAUDE.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,23 @@ React 19, Vite 6, Tailwind 4, TanStack Query 5, react-router-dom 7, Vitest 3, @t
66
## Key Files
77
- `vite.config.ts` — proxies `/api``http://localhost:5435`
88
- `src/api/client.ts` — axios instance with JWT interceptor and 401 redirect
9+
- `src/api/roles.ts` — Role CRUD, member management, inheritance, datasource role access
10+
- `src/api/adminAudit.ts` — Admin audit log queries
911
- `src/auth/AuthContext.tsx``AuthProvider`, `useAuth`, localStorage-backed token/user
1012
- `src/components/CatalogDiscoveryWizard.tsx` — 4-step discovery wizard (schemas → tables → columns → save)
13+
- `src/components/RoleForm.tsx` — Reusable role name + description form
14+
- `src/components/RoleMemberPanel.tsx` — Effective member list (direct + inherited with source badges), add/remove for direct members
15+
- `src/components/RoleInheritancePanel.tsx` — Parent/child role management with cycle detection feedback
16+
- `src/components/RoleAccessPanel.tsx` — Checkbox-based role access panel for datasource edit page
17+
- `src/components/AuditTimeline.tsx` — Reusable admin audit timeline (used on role/user/policy/datasource detail pages)
18+
- `src/utils/auditBadge.ts` — Shared `actionBadgeClass()` for audit action badge styling (used by AuditTimeline + AdminAuditPage)
19+
- `src/components/PolicyAssignmentPanel.tsx` — Three components: `PolicyAssignmentsReadonly`, `PolicyAssignmentEditPanel` (with scope selector: all/user/role), `DatasourceAssignmentsReadonly`
20+
- `src/pages/RolesListPage.tsx` — Paginated list with search, member counts, active/inactive badges
21+
- `src/pages/RoleCreatePage.tsx` — Create form
22+
- `src/pages/RoleEditPage.tsx` — Tabbed view (Details, Members, Inheritance, Data Sources, Policies, Activity)
23+
- `src/pages/AdminAuditPage.tsx` — Centralized admin audit log with filters (resource type, actor, date range)
24+
- `src/types/policy.ts` — TypeScript interfaces for policies, assignments (`PolicyType`, `AssignmentScope`, `TargetEntry`)
25+
- `src/types/role.ts` — TypeScript interfaces for roles, members, audit entries
1126
- `src/test/test-utils.tsx``renderWithProviders` (QueryClient + AuthProvider + MemoryRouter)
1227
- `src/test/factories.ts``makeUser`, `makeDataSource`, `makeDataSourceType`, `makeDiscoveredSchema/Table/Column`
1328

0 commit comments

Comments
 (0)