Skip to content

Casbin RBAC Implementation - #44

Draft
nbnugggget wants to merge 1 commit into
mainfrom
feature/casbin-rbac
Draft

nbnugggget wants to merge 1 commit into
mainfrom
feature/casbin-rbac

Conversation

@nbnugggget

Copy link
Copy Markdown
  • Add CasbinConfig, CasbinFilter, DataSeeder to configs/
  • Add model.conf and policy.csv for PERM metamodel
  • Add RoleController for access request/approval/rejection workflow
  • Add AuditLogController for audit log retrieval
  • Add AccessRequest and AuditLog data models
  • Add Casbin dependency to build.gradle

…logging

- Add CasbinConfig, CasbinFilter, DataSeeder to configs/
- Add model.conf and policy.csv for PERM metamodel
- Add RoleController for access request/approval/rejection workflow
- Add AuditLogController for audit log retrieval
- Add AccessRequest and AuditLog data models
- Add Casbin dependency to build.gradle
@lintianjue

Copy link
Copy Markdown
Contributor

Review: build/launch verification + findings

TL;DR — this branch does not compile, so the service never starts. Verified by launching main and this branch back-to-back with the same command, in the project's own dev container (Dockerfile target dev: Gradle 8.12 / JDK 21) against the live quant-ai-mongo-db and quant-ai-timescale-db containers from main/docker-compose.dev.yml. main boots and serves a Mongo-backed request; 2572604 fails at compileKotlin with 29 errors and exits in 17s. Same result with host Gradle 9.3.1 / JDK 22 (gradle clean build -x test and gradle bootRun), so it's not a toolchain quirk.

Reproduction

# in a clean checkout of this branch (2572604)
gradle clean build -x test        # -> BUILD FAILED, 29 errors in compileKotlin
gradle bootRun --args=--spring.profiles.active=local   # -> same, no Tomcat, nothing listening
Log: main (c029ce4) — boots in the real stack
> Task :compileKotlin
> Task :bootRun
INFO ... com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
INFO ... org.mongodb.driver.cluster               : Monitor thread successfully connected to server with description ServerDescription{address=quant-ai-mongo-db:27017, type=STANDALONE, state=CONNECTED, ok=true, ...}
INFO ... o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 10001 (http) with context path '/'
INFO ... s.c.q.m.MiddlewareApplicationKt          : Started MiddlewareApplicationKt in 2.359 seconds (process running for 5.05)
$ curl -s -w "\nHTTP %{http_code}\n" http://localhost:10001/users/count
{"adminUsers":1,"nonAdminUsers":0}
HTTP 200
Log: this branch (2572604) — same container, same command
6:> Task :compileKotlin
7:e: <repo>/src/main/kotlin/sg/com/quantai/middleware/configs/CasbinFilter.kt:84:33 Unresolved reference 'role'.
8:e: <repo>/src/main/kotlin/sg/com/quantai/middleware/configs/DataSeeder.kt:30:21 No parameter with name 'role' found.
9:e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/AuditLogController.kt:6:53 Unresolved reference 'AuditLogRepository'.
10:e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/AuditLogController.kt:11:37 Unresolved reference 'AuditLogRepository'.
11:e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/AuditLogController.kt:16:39 Unresolved reference 'findAll'.
12:e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/AuditLogController.kt:17:33 Unresolved reference 'timestamp'.
13:e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/AuditLogController.kt:17:35 Unresolved reference 'it'.
14:e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:9:53 Unresolved reference 'AccessRequestRepository'.
15:e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:10:53 Unresolved reference 'AuditLogRepository'.
16:e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:18:42 Unresolved reference 'AccessRequestRepository'.
17:e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:19:37 Unresolved reference 'AuditLogRepository'.
18:e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:27:18 Unresolved reference 'role'.
19:e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:30:48 Unresolved reference 'findByUserIdAndStatus'.
20:e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:34:47 Unresolved reference 'save'.
21:e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:35:28 Unresolved reference 'save'.
22:e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:45:58 Unresolved reference 'findByStatus'.
23:e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:57:39 No parameter with name 'role' found.
24:e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:64:43 Unresolved reference 'findByUserIdAndStatus'.
25:e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:65:14 Cannot infer type for this parameter. Please specify it explicitly.
26:e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:65:18 Cannot infer type for this parameter. Please specify it explicitly.
27:e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:66:37 Unresolved reference 'save'.
28:e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:66:45 Unresolved reference 'copy'.
29:e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:74:28 Unresolved reference 'save'.
30:e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:89:43 Unresolved reference 'findByUserIdAndStatus'.
31:e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:90:14 Cannot infer type for this parameter. Please specify it explicitly.
32:e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:90:18 Cannot infer type for this parameter. Please specify it explicitly.
33:e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:91:37 Unresolved reference 'save'.
34:e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:91:45 Unresolved reference 'copy'.
35:e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:97:28 Unresolved reference 'save'.
37:> Task :compileKotlin FAILED
42:* What went wrong:
45:   > Compilation error. See log for more details
53:BUILD FAILED in 16s
container: Exited (1)
$ curl http://localhost:10001/   -> no HTTP response (nothing listening)
Log: host gradle clean build -x test on this branch — all 29 errors
e: <repo>/src/main/kotlin/sg/com/quantai/middleware/configs/CasbinFilter.kt:84:33 Unresolved reference 'role'.
e: <repo>/src/main/kotlin/sg/com/quantai/middleware/configs/DataSeeder.kt:30:21 No parameter with name 'role' found.
e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/AuditLogController.kt:6:53 Unresolved reference 'AuditLogRepository'.
e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/AuditLogController.kt:11:37 Unresolved reference 'AuditLogRepository'.
e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/AuditLogController.kt:16:39 Unresolved reference 'findAll'.
e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/AuditLogController.kt:17:33 Unresolved reference 'timestamp'.
e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/AuditLogController.kt:17:35 Unresolved reference 'it'.
e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:9:53 Unresolved reference 'AccessRequestRepository'.
e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:10:53 Unresolved reference 'AuditLogRepository'.
e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:18:42 Unresolved reference 'AccessRequestRepository'.
e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:19:37 Unresolved reference 'AuditLogRepository'.
e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:27:18 Unresolved reference 'role'.
e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:30:48 Unresolved reference 'findByUserIdAndStatus'.
e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:34:47 Unresolved reference 'save'.
e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:35:28 Unresolved reference 'save'.
e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:45:58 Unresolved reference 'findByStatus'.
e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:57:39 No parameter with name 'role' found.
e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:64:43 Unresolved reference 'findByUserIdAndStatus'.
e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:65:14 Cannot infer type for this parameter. Please specify it explicitly.
e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:65:18 Cannot infer type for this parameter. Please specify it explicitly.
e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:66:37 Unresolved reference 'save'.
e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:66:45 Unresolved reference 'copy'.
e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:74:28 Unresolved reference 'save'.
e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:89:43 Unresolved reference 'findByUserIdAndStatus'.
e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:90:14 Cannot infer type for this parameter. Please specify it explicitly.
e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:90:18 Cannot infer type for this parameter. Please specify it explicitly.
e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:91:37 Unresolved reference 'save'.
e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:91:45 Unresolved reference 'copy'.
e: <repo>/src/main/kotlin/sg/com/quantai/middleware/controllers/mongo/RoleController.kt:97:28 Unresolved reference 'save'.

* What went wrong:
   > Compilation error. See log for more details
BUILD FAILED in 14s

Why it fails (2 blockers)

  1. User has no role field. User.kt is untouched by this PR, yet CasbinFilter.kt:84 reads user.role, DataSeeder.kt:30 passes role = …, and RoleController.kt:27/57 read/copy it.
  2. AccessRequestRepository and AuditLogRepository don't exist and aren't added. RoleController and AuditLogController inject both; findByUserIdAndStatus / findByStatus also need declaring. Everything else in the error list cascades from these two.

Most likely there are uncommitted local changes to User.kt plus two repository files — please push them.

Cross-repo: front-end never sends the header this filter requires

Checked against front-end@003fdae:

  • X-User-UID appears nowhere in the front-end. The filter would return 401 AUTH_MISSING for every real user on every protected route.
  • The front-end identifies the user by putting the uid in the URL path (/portfolios/user/{uid}, /strategies/user/{uid}, /users/{uid} — see StrategyService.ts, AccountInfoService.ts).
  • UserService.ts authHeader() builds Authorization: Bearer <accessToken>, but accessToken is never set anywhere; login stores the raw response in localStorage("user").

So the middleware-only change can't work with the current client. Either this PR needs a paired front-end change, or the filter should derive identity from what already exists (the /users/authenticate-token flow).

Security — the authorization is bypassable as designed

  1. X-User-UID is unauthenticated identity (CasbinFilter.kt:52). The caller asserts who they are; any uid (admin uids are listed by GET /users/list / GET /users/{uid}) gives that user's permissions. Identity must be bound to something unforgeable.
  2. The route map matches almost nothing, and is fail-open. Exact-string requestURI match can't hit path variables. Against the real controllers only GET:/users/list matches; PUT/DELETE:/users/admin (real: /users/admin/{email}), GET:/portfolio (/portfolios/user/{user_id}), GET:/analytics (/analytics/volume…), POST:/strategy (/strategies), POST:/strategy/execute (doesn't exist), POST:/funds/deposit (/portfolios/cash/deposit/{user_id}/{portfolio_id}) never match — and unmapped routes are allowed through. Use AntPathMatcher/PathPattern and default-deny under protected prefixes.
  3. DataSeeder creates a default admin with hard-coded credentials and prints them to stdout, on every boot in every environment where no admin exists (deleting it recreates it). Gate on a profile and read the credential from config with no default.
  4. /roles/request is in publicPaths and takes userId as a query param — anyone can file requests for arbitrary uids.
  5. adminId on /roles/approve and /roles/reject is caller-supplied, so the audit trail's performedBy is forgeable.

Correctness

  1. Casbin g writes are dead code and non-durable. approveTraderAccess calls addRoleForUser(userId, "Trader"), but the filter enforces with the role string from Mongo, never the uid — the grouping is never consulted. And CasbinConfig copies policy.csv into Files.createTempDirectory(...) with enableAutoSave(true), so writes go to a temp file lost on restart. Either enforce on uid with a persistent adapter, or drop step 2 of approve and keep Mongo as the single source of truth.
  2. Enforcer isn't thread-safe; RoleController mutates it while CasbinFilter reads concurrently. Use SyncedEnforcer.
  3. findOneByUid is declared non-null but Spring Data returns null on miss. The filter's blanket catch (Exception) hides it as a 401; RoleController.requestTraderAccess / approveTraderAccess NPE → 500 for an unknown uid.
  4. catch (e: Exception) around the user lookup (CasbinFilter.kt:56) turns a Mongo outage into "User not found" / 401.
  5. Denials are never audited. AuditLog.event lists PERMISSION_DENIED, but the filter only writes the HTTP response.
  6. approveTraderAccess doesn't validate state — no check that a pending request exists or that the user is a Viewer; approving an Admin demotes them to Trader.
  7. Three independent saves in approve (user role, request status, audit) with no atomicity.

Quality

  • AuditLogController does findAll() + in-memory sort — unbounded. Use a sorted derived query with Pageable.
  • Casbin temp dirs leak per JVM start (deleteOnExit()).
  • model.conf: obj holds the action and act holds the literal "allow" — works, but misleads the next reader.
  • policy.csv grants view_market (no route maps to it) and manage_permissions (never used); publicPaths lists /actuator/health but actuator isn't a dependency.
  • println in DataSeeder instead of a logger; missing trailing newline in 5 files; build.gradle exclude block indentation.
  • No tests — src/test/kotlin/.../controllers exists; the filter's allow/deny paths, route matching and the approve flow are straightforward to cover.

The SecurityConfiguration package fix (configconfigs) is correct and worth keeping.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants