From 9565956f798f3ac5cc5962c909d4e57ef7d6a28c Mon Sep 17 00:00:00 2001 From: Dorian Date: Sun, 15 Mar 2026 04:32:59 +0000 Subject: [PATCH] feat: enforce RBAC in RPC dispatcher Check user role against method permissions before dispatch. All current users default to Admin, laying groundwork for multi-user. Co-Authored-By: Claude Opus 4.6 (1M context) --- core/archipelago/src/api/rpc/mod.rs | 23 +++++++++++++++++++++++ loop/plan.md | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/core/archipelago/src/api/rpc/mod.rs b/core/archipelago/src/api/rpc/mod.rs index 8d21c8f4..b1afecae 100644 --- a/core/archipelago/src/api/rpc/mod.rs +++ b/core/archipelago/src/api/rpc/mod.rs @@ -242,6 +242,29 @@ impl RpcHandler { } } + // RBAC: check if the user's role allows this method + if !is_unauthenticated { + if let Ok(Some(user)) = self.auth_manager.get_user().await { + if !user.role.can_access(&rpc_req.method) { + let rpc_resp = RpcResponse { + result: None, + error: Some(RpcError { + code: 403, + message: "Forbidden: insufficient permissions".to_string(), + data: None, + }), + }; + let resp_body = serde_json::to_vec(&rpc_resp) + .context("Failed to serialize response")?; + return Ok(Response::builder() + .status(StatusCode::FORBIDDEN) + .header("Content-Type", "application/json") + .body(hyper::Body::from(resp_body)) + .unwrap()); + } + } + } + // CSRF protection: validate X-CSRF-Token header for authenticated methods if !is_unauthenticated { let csrf_cookie = extract_csrf_cookie(&parts.headers); diff --git a/loop/plan.md b/loop/plan.md index d7fee15d..ede15110 100644 --- a/loop/plan.md +++ b/loop/plan.md @@ -74,7 +74,7 @@ - [x] **Fix session TTL clock bug — use SystemTime instead of Instant**: Read `core/archipelago/src/session.rs`. Find where `Instant::now()` is used for session TTL/expiry (around line 97). `Instant` is monotonic but can drift on sleep/hibernate — common on NUC/Pi hardware. Replace with `SystemTime::now()` for absolute time comparison. The `FULL_SESSION_TTL` (24 hours) and `PENDING_TOTP_TTL` (5 minutes) checks should use `SystemTime::elapsed()` or store `SystemTime` timestamps and compare with `SystemTime::now()`. Run `cargo test --all-features` in `core/` on the dev server. -- [ ] **Enforce RBAC in RPC handler**: Read `core/archipelago/src/auth.rs` — find the `UserRole` enum and `can_access()` method. Then read `core/archipelago/src/api/rpc/mod.rs` — find where authenticated requests are dispatched to handlers. Add a role check before dispatching: after validating the session, get the user's role, call `role.can_access(method_name)`, and return an authorization error if denied. For now, all users created via onboarding should default to `Admin` role (single-user system), but this lays the groundwork for multi-user. Run `cargo clippy --all-targets --all-features && cargo test --all-features` on the dev server. +- [x] **Enforce RBAC in RPC handler**: Read `core/archipelago/src/auth.rs` — find the `UserRole` enum and `can_access()` method. Then read `core/archipelago/src/api/rpc/mod.rs` — find where authenticated requests are dispatched to handlers. Add a role check before dispatching: after validating the session, get the user's role, call `role.can_access(method_name)`, and return an authorization error if denied. For now, all users created via onboarding should default to `Admin` role (single-user system), but this lays the groundwork for multi-user. Run `cargo clippy --all-targets --all-features && cargo test --all-features` on the dev server. - [ ] **Remove dead code and #[allow(dead_code)]**: Search `core/` for all `#[allow(dead_code)]` and `#[allow(unused)]` annotations. For each: (1) if the code is genuinely unused and not part of a planned feature, delete it, (2) if it should be used (like RBAC — now wired up in previous task), remove the allow annotation. Key file: `core/archipelago/src/auth.rs` lines ~70, 83, 88. Run `cargo clippy --all-targets --all-features` to verify no new warnings.