Skip to content

Conversation

nikhilsinhaparseable
Copy link
Contributor

@nikhilsinhaparseable nikhilsinhaparseable commented Aug 31, 2025

new structure for POST /usergroup

{
  "name": "parseable",
  "roles": [],
  "users": [
    {
      "username": "nikhil",
      "method": "native"
    }
  ]
}

new structure for add/remove user - PATCH /usergroup/{name}/add(remove)

{
  "users": [
    {
      "username": "authentik Default Admin",
      "method": "oauth"
    }
  ]
}

Summary by CodeRabbit

  • New Features

    • Added a Prism user lookup endpoint.
  • Refactor

    • Switched RBAC, sessions, and group membership to canonical user IDs (userid) across APIs, routes and storage; user-facing endpoints now use {userid}.
    • Introduced a canonical group-user representation and migrated role/user management and session refresh to operate on user IDs.
    • Centralized update concurrency guard and standardized several handlers to return HTTP 200 responses.
  • Documentation

    • Route docs and comments updated to reflect userid-based endpoints.

Copy link
Contributor

coderabbitai bot commented Aug 31, 2025

Walkthrough

Identity handling migrated from username-based to userid-based across RBAC core, HTTP handlers, routes, sessions, ingestors, and Prism conversion. Introduced GroupUser as canonical member of UserGroup. UPDATE_LOCK centralized and exported. Many handlers, routes, and public method signatures now use userid.

Changes

Cohort / File(s) Summary
RBAC core & types
src/rbac/user.rs, src/rbac/mod.rs, src/rbac/map.rs, src/rbac/utils.rs
Add GroupUser; change UserGroup.users from HashSet<String>HashSet<GroupUser>; introduce GroupUser helpers and conversion from User; migrate maps, sessions, and prism helpers to use userid as canonical identity; rename session helpers to userid variants and update public method signatures.
HTTP RBAC handlers
src/handlers/http/rbac.rs, src/handlers/http/role.rs, src/handlers/http/cluster/mod.rs
Shift handlers and serialization to userid-centric operations; derive display names via username_by_userid(); UPDATE_LOCK visibility changed; add get_prism_user; update many public signatures and routing semantics to accept userid.
OIDC upsert & migration
src/handlers/http/oidc.rs
put_user now accepts userid; lookup/creation and migration use userid; metadata update timing adjusted; use GroupUser::from_user when updating group memberships.
Modal query handlers
src/handlers/http/modal/query/querier_rbac.rs, src/handlers/http/modal/query/querier_role.rs
Remove local static lock and import shared UPDATE_LOCK; use users() and UserType; endpoints switch to userid; iterate by reference (no cloning) and collect userids for session refresh.
Modal ingest handlers
src/handlers/http/modal/ingest/ingestor_rbac.rs, src/handlers/http/modal/ingest/ingestor_role.rs
Use shared UPDATE_LOCK; handlers accept userid; validate/modify metadata and in-memory state by userid; return HttpResponse for some endpoints; session-refresh collects userid strings and avoids cloning.
Server route mappings
src/handlers/http/modal/ingest_server.rs, src/handlers/http/modal/query_server.rs
Rename route tokens {username}{userid} for user-role and delete endpoints; update route comments.
Session & utils
src/utils/mod.rs, src/rbac/map.rs
Session lookup and storage switched from username to userid (get_usernameget_userid, session tuples keyed by userid); user maps keyed by userid.
Iteration & cloning reductions
src/handlers/http/modal/ingest/ingestor_role.rs, src/handlers/http/modal/query/querier_role.rs, src/handlers/http/role.rs
Replace values().cloned() with values() (borrowed iteration); collect userid().to_string() for session refresh; removal loops use userid.
Ingest server return & lock export
src/handlers/http/modal/ingest/ingestor_rbac.rs
post_user/post_gen_password now return HttpResponse; UPDATE_LOCK exported from rbac module and used to guard modifications; role endpoints accept userid.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client
  participant API as HTTP RBAC API
  participant RBAC as RBAC Core
  participant Store as Metadata Store
  participant Ing as Ingestors

  rect rgb(245,248,255)
    note right of API: DELETE /api/v1/user/{userid}
    Client->>API: DELETE {userid}
    API->>API: Acquire UPDATE_LOCK (_guard)
    API->>RBAC: users().get(userid) → display name (optional)
    API->>Store: put_metadata(updated without user where user.userid == userid)
    API->>Ing: sync_user_deletion_with_ingestors(userid)
    API->>RBAC: Users.delete_user(userid)
    API-->>Client: 200 OK (display name if available)
  end
Loading
sequenceDiagram
  autonumber
  actor Admin
  participant API as Role Handler
  participant RBAC as RBAC Core
  participant Sess as Sessions

  rect rgb(245,255,245)
    note right of API: PUT /role/{role}
    Admin->>API: Update role
    API->>RBAC: read_user_groups() & users() (by reference)
    API->>API: Collect affected userids (GroupUser.userid / User.userid)
    loop for each userid
      API->>Sess: remove_sessions(userid)
    end
    API->>RBAC: sync_role_update_with_ingestors(role, affected_userids)
    API-->>Admin: 200 OK
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

for next release

Suggested reviewers

  • nitisht

Poem

I swapped my name for a sturdy id,
I hopped through groups where data hid.
Locks held steady, sessions cleared,
Ingestors hummed, the map adhered.
Rabbit cheers — userid's here! 🥕🐇

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/handlers/http/modal/query/querier_rbac.rs (1)

106-149: Use the single shared UPDATE_LOCK and hold its guard

  • In src/handlers/http/modal/query/querier_rbac.rs (and similarly in ingestor_rbac.rs), remove the local
    static UPDATE_LOCK: Mutex<()> = Mutex::const_new(());
    and instead import the one from src/handlers/http/rbac.rs (make it pub(crate) if not already).
  • Change
    let _ = UPDATE_LOCK.lock().await;
    to
    let _guard = UPDATE_LOCK.lock().await;
    so the lock stays held for the entire delete flow.
🧹 Nitpick comments (3)
src/rbac/user.rs (2)

329-343: Avoid extra clone when extending users.

You clone the entire HashSet<GroupUser> only to iterate it again. Iterate first for session refresh, then extend by moving the set.

Apply this diff:

-        self.users.extend(users.clone());
-        // also refresh all user sessions
-        for group_user in &users {
+        // refresh all user sessions
+        for group_user in &users {
             mut_sessions().remove_user(group_user.userid());
         }
+        self.users.extend(users);

384-391: Expose userids to prevent misuse of display names downstream.

Multiple handlers need stable IDs for session invalidation. Add a get_userids() helper alongside get_usernames().

Apply this diff:

     /// Get all usernames in this group
     pub fn get_usernames(&self) -> Vec<String> {
         self.users
             .iter()
             .map(|u| u.username().to_string())
             .collect()
     }
+
+    /// Get all userids in this group (stable identifier)
+    pub fn get_userids(&self) -> Vec<String> {
+        self.users
+            .iter()
+            .map(|u| u.userid().to_string())
+            .collect()
+    }

Then update callers to use get_userids() for session invalidation.

src/handlers/http/oidc.rs (1)

435-437: Group migration is correct; consider small clarity/robustness tweaks.

  • Nit: old_username is actually the prior userid for OAuth; consider renaming for clarity.
  • Optional: proactively invalidate sessions keyed by the old id to avoid stray sessions on other devices.

Example:

-    // Store the old username before modifying the user object
-    let old_username = user.username().to_string();
+    // Store the old userid before modifying the user object
+    let old_userid = user.username().to_string();
...
-            group.users.retain(|u| u.userid() != old_username);
+            group.users.retain(|u| u.userid() != old_userid);
             group.users.insert(GroupUser::from_user(&user));

And optionally after metadata write:

// optional cleanup
mut_sessions().remove_user(&old_userid);
mut_sessions().remove_user(user.username());
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f10b28a and 2d8c655.

📒 Files selected for processing (7)
  • src/handlers/http/modal/ingest/ingestor_role.rs (1 hunks)
  • src/handlers/http/modal/query/querier_rbac.rs (2 hunks)
  • src/handlers/http/modal/query/querier_role.rs (1 hunks)
  • src/handlers/http/oidc.rs (2 hunks)
  • src/handlers/http/rbac.rs (4 hunks)
  • src/handlers/http/role.rs (1 hunks)
  • src/rbac/user.rs (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
src/handlers/http/role.rs (1)
src/rbac/map.rs (2)
  • read_user_groups (42-48)
  • users (58-64)
src/handlers/http/modal/ingest/ingestor_role.rs (1)
src/rbac/map.rs (2)
  • read_user_groups (42-48)
  • users (58-64)
src/handlers/http/modal/query/querier_role.rs (1)
src/rbac/map.rs (2)
  • read_user_groups (42-48)
  • users (58-64)
src/handlers/http/oidc.rs (2)
src/rbac/user.rs (1)
  • from_user (231-253)
src/handlers/http/modal/utils/rbac_utils.rs (1)
  • put_metadata (34-38)
src/rbac/user.rs (2)
src/rbac/mod.rs (1)
  • is_oauth (68-70)
src/rbac/map.rs (2)
  • users (58-64)
  • mut_sessions (98-104)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: Build Default aarch64-apple-darwin
  • GitHub Check: Build Default x86_64-pc-windows-msvc
  • GitHub Check: Build Default x86_64-unknown-linux-gnu
  • GitHub Check: Build Default aarch64-unknown-linux-gnu
  • GitHub Check: Build Default x86_64-apple-darwin
  • GitHub Check: Build Kafka x86_64-unknown-linux-gnu
  • GitHub Check: Quest Smoke and Load Tests for Standalone deployments
  • GitHub Check: coverage
  • GitHub Check: Quest Smoke and Load Tests for Distributed deployments
🔇 Additional comments (14)
src/handlers/http/modal/ingest/ingestor_role.rs (1)

58-61: LGTM: direct-user scan uses stable key.

user.username() resolves to userid for OAuth, so this path invalidates sessions correctly.

src/rbac/user.rs (6)

292-295: Creation-time validation expects userid, but PR API examples send only username + method.

users().contains_key(group_user.userid()) assumes userid is present. If endpoints deserialize request bodies directly into GroupUser, requests without userid will fail.

Please verify HTTP DTOs for POST/PATCH /usergroup do NOT deserialize into GroupUser directly. If they do, introduce a request type:

#[derive(Deserialize)]
struct GroupUserInput { username: String, method: String }

and resolve to GroupUser { userid, username, method } server-side via lookup.


319-321: Constructor change looks good.

Accepts HashSet<GroupUser>; pairs with the refactors elsewhere.


360-362: LGTM: session refresh on role removal uses userid.


366-381: LGTM: removal computes set diff and refreshes removed users by userid.


392-397: LGTM: ergonomic helper for adding from &User refs.


399-413: LGTM: remove_users_by_user_ids aligns with userid-as-canonical semantics.

src/handlers/http/modal/query/querier_role.rs (1)

61-64: LGTM: direct-user path uses stable key.

src/handlers/http/role.rs (1)

60-63: LGTM: direct-user path uses stable key.

src/handlers/http/rbac.rs (3)

69-76: Docs cleanups LGTM

Route comments/readability improvements look good.


353-361: Public JSON shape change to camelCase — confirm downstream compatibility

Switching to #[serde(rename_all = "camelCase")] changes field names in HTTP error payloads. Ensure UI/SDKs/tests consuming InvalidUserGroupError are updated.


393-395: User-facing error string changed — verify callers not asserting on text

“UserGroup {0} is still in use” replaces the prior message. If any clients/tests compare strings, update them or prefer structured errors.

src/handlers/http/modal/query/querier_rbac.rs (2)

130-141: In-place metadata update LGTM

The replace-or-push logic is correct and idempotent for group names.


106-106: Route doc correction LGTM

Updated DELETE route path matches the handler behavior.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/handlers/http/rbac.rs (2)

127-152: Avoid calling unlocked helpers while holding the global lock (risk of inconsistent patterns/deadlocks later)

You correctly hold the guard here, but add_roles_to_user does not lock; other call sites may run it concurrently. Either inline the role update under this guard or release it before calling and have the callee lock.

Apply:

-    Users.put_user(user.clone());
-
-    add_roles_to_user(
+    Users.put_user(user.clone());
+    // Release the global UPDATE_LOCK before calling a function that acquires it.
+    drop(_guard);
+    add_roles_to_user(
         web::Path::<String>::from(username.clone()),
         web::Json(created_role),
     )
     .await?;

And add a lock at the start of add_roles_to_user/remove_roles_from_user (in this file) so they are always atomic:

// inside add/remove role handlers
let _guard = UPDATE_LOCK.lock().await;

448-454: #[serde(rename = "camelCase")] is ineffective for field casing on structs

Use rename_all to actually camelCase the fields (you already alias direct_rolesroles).

-#[derive(Serialize)]
-#[serde(rename = "camelCase")]
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
 pub struct RolesResponse {
     #[serde(rename = "roles")]
     pub direct_roles: HashMap<String, Vec<DefaultPrivilege>>,
     pub group_roles: HashMap<String, HashMap<String, Vec<DefaultPrivilege>>>,
 }
♻️ Duplicate comments (3)
src/handlers/http/rbac.rs (1)

45-46: Export a single shared UPDATE_LOCK; duplicate locks across modules break atomicity

Make this lock reusable across modules and remove per-file duplicates in querier/ingestor. Holding different locks provides no cross-module exclusion.

Apply:

-// async aware lock for updating storage metadata and user map atomically
-static UPDATE_LOCK: Mutex<()> = Mutex::const_new(());
+// async-aware lock for updating storage metadata and user map atomically
+pub(crate) static UPDATE_LOCK: Mutex<()> = Mutex::const_new(());
#!/usr/bin/env bash
# Find duplicate UPDATE_LOCKs and transient guard usages
rg -nP 'static\s+UPDATE_LOCK' -g '!**/target/**' -C2
rg -nP 'let\s+_\s*=\s*UPDATE_LOCK\.lock\(\)\.await' -g '!**/target/**' -C2
src/handlers/http/modal/ingest/ingestor_rbac.rs (1)

34-36: Use the shared UPDATE_LOCK; fix typo

Duplicate lock defeats cross-module serialization; also fix “atomicically” → “atomically”.

-// async aware lock for updating storage metadata and user map atomicically
-static UPDATE_LOCK: Mutex<()> = Mutex::const_new(());
+// Use the shared lock to keep cross-module updates atomic
+use crate::handlers::http::rbac::UPDATE_LOCK;

If Mutex import becomes unused:

-use tokio::sync::Mutex;
#!/usr/bin/env bash
rg -nP 'static\s+UPDATE_LOCK' -g '!**/target/**' -C2
src/handlers/http/modal/query/querier_rbac.rs (1)

41-42: Consolidate on the shared UPDATE_LOCK

Replace the local static with the exported lock to ensure global serialization.

-// async aware lock for updating storage metadata and user map atomically
-static UPDATE_LOCK: Mutex<()> = Mutex::const_new(());
+// Use the shared lock for cross-module atomicity
+use crate::handlers::http::rbac::UPDATE_LOCK;

Drop use tokio::sync::Mutex; if unused after this change.

🧹 Nitpick comments (7)
src/handlers/http/rbac.rs (1)

54-66: Clarify field naming: id is actually userid for OAuth users

Since user.username() resolves to OAuth userid, consider renaming the response field to userid (or document clearly) to avoid API ambiguity.

src/rbac/user.rs (3)

235-241: Prefer a typed method enum over string literals

To avoid typos and ease pattern matching, consider enum AuthMethod { Native, OAuth } with #[serde(rename_all = "lowercase")].


243-266: from_user mapping is correct; minor nit on style

You can shorten the iterator map in other call sites to map(GroupUser::from_user).


404-409: Tiny iterator nit

Use function pointer style.

-        let group_users: HashSet<GroupUser> =
-            user_refs.iter().map(|u| GroupUser::from_user(u)).collect();
+        let group_users: HashSet<GroupUser> =
+            user_refs.iter().map(GroupUser::from_user).collect();
src/handlers/http/modal/query/querier_rbac.rs (3)

109-159: Delete: now resolves correct userid for group removal — good; update misleading comment

Code removes by internal userid; comment mentions “display name”.

-            // Look up the user by display name to get the correct userid
+            // Resolve the canonical internal userid for this username

173-181: Spelling nit: non_existant_roles → non_existent_roles

-    let mut non_existant_roles = Vec::new();
+    let mut non_existent_roles = Vec::new();
@@
-    if !non_existant_roles.is_empty() {
-        return Err(RBACError::RolesDoNotExist(non_existant_roles));
+    if !non_existent_roles.is_empty() {
+        return Err(RBACError::RolesDoNotExist(non_existent_roles));
     }

220-228: Spelling nit: non_existant_roles → non_existent_roles

-    let mut non_existant_roles = Vec::new();
+    let mut non_existent_roles = Vec::new();
@@
-    if !non_existant_roles.is_empty() {
-        return Err(RBACError::RolesDoNotExist(non_existant_roles));
+    if !non_existent_roles.is_empty() {
+        return Err(RBACError::RolesDoNotExist(non_existent_roles));
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2d8c655 and d74816b.

📒 Files selected for processing (7)
  • src/handlers/http/modal/ingest/ingestor_rbac.rs (1 hunks)
  • src/handlers/http/modal/ingest/ingestor_role.rs (1 hunks)
  • src/handlers/http/modal/query/querier_rbac.rs (5 hunks)
  • src/handlers/http/modal/query/querier_role.rs (1 hunks)
  • src/handlers/http/rbac.rs (7 hunks)
  • src/handlers/http/role.rs (1 hunks)
  • src/rbac/user.rs (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/handlers/http/modal/ingest/ingestor_role.rs
  • src/handlers/http/role.rs
  • src/handlers/http/modal/query/querier_role.rs
🧰 Additional context used
🧬 Code graph analysis (2)
src/rbac/user.rs (2)
src/rbac/mod.rs (1)
  • is_oauth (68-70)
src/rbac/map.rs (2)
  • users (58-64)
  • mut_sessions (98-104)
src/handlers/http/modal/query/querier_rbac.rs (4)
src/rbac/user.rs (3)
  • roles (98-100)
  • username (84-92)
  • username (227-229)
src/rbac/map.rs (3)
  • roles (74-80)
  • users (58-64)
  • write_user_groups (50-56)
src/handlers/http/modal/ingest/ingestor_rbac.rs (1)
  • delete_user (59-75)
src/handlers/http/rbac.rs (1)
  • delete_user (225-248)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: Build Default aarch64-unknown-linux-gnu
  • GitHub Check: Build Default x86_64-apple-darwin
  • GitHub Check: Build Default x86_64-pc-windows-msvc
  • GitHub Check: Build Default aarch64-apple-darwin
  • GitHub Check: coverage
  • GitHub Check: Build Kafka x86_64-unknown-linux-gnu
  • GitHub Check: Build Default x86_64-unknown-linux-gnu
  • GitHub Check: Quest Smoke and Load Tests for Distributed deployments
  • GitHub Check: Quest Smoke and Load Tests for Standalone deployments
🔇 Additional comments (11)
src/handlers/http/rbac.rs (3)

162-183: Password reset now correctly holds the lock

Guard lifetime covers metadata + in-memory update. Good.


238-248: User deletion holds the lock across metadata and in-memory updates

This fixes lost-update races here.


353-361: serde casing fix looks good

#[serde(rename_all = "camelCase")] on InvalidUserGroupError matches the API intent.

src/rbac/user.rs (6)

206-224: Eq/Hash by userid-only — prevents duplicate membership on display-name changes

Good manual impls; this fixes the prior duplication risk.


304-307: Validate by userid, report by display name — good UX

This matches the new identity model.


347-357: Session refresh on add_users — LGTM

Revoking sessions for newly added users is correct.


378-394: Session refresh on remove_users — LGTM

Minimal and correct set math.


396-403: Helper returns display usernames — OK

Naming matches behavior.


206-213: Separate persisted GroupUser from request DTO
Ensure your create/update endpoints accept request bodies without userid. I didn’t find any handlers deserializing GroupUser directly—please verify none of your POST/PATCH handlers accept GroupUser as the JSON body. If they do, introduce an input DTO:

#[derive(serde::Deserialize)]
pub struct GroupUserInput {
    pub username: String,
    pub method: String,
}

Then map GroupUserInputGroupUser internally, filling in userid. Otherwise keep GroupUser internal-only.

src/handlers/http/modal/ingest/ingestor_rbac.rs (1)

61-61: Guard held during delete — LGTM

This aligns with the locking model.

src/handlers/http/modal/query/querier_rbac.rs (1)

275-298: Password reset: guard now held — LGTM

Lock covers metadata write + in-memory update + cluster sync.

coderabbitai[bot]
coderabbitai bot previously approved these changes Aug 31, 2025
coderabbitai[bot]
coderabbitai bot previously approved these changes Sep 1, 2025
new structure for POST /usergroup

```
{
  "name": "parseable",
  "roles": [],
  "users": [
    {
      "username": "nikhil",
      "method": "native"
    }
  ]
}
```

new structure for add/remove user - PATCH /usergroup/{name}/add(remove)

```
{
  "users": [
    {
      "username": "authentik Default Admin",
      "method": "oauth"
    }
  ]
}
```
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (8)
src/rbac/map.rs (1)

206-212: Expired sessions are retained (comparison inverted).

retain keeps items where predicate is true; expiry < now keeps expired sessions and drops valid ones.

-        sessions.retain(|(_, expiry)| expiry < &now);
+        // keep only non-expired sessions
+        sessions.retain(|(_, expiry)| expiry > &now);
src/rbac/mod.rs (1)

210-229: serde attribute should be rename_all, not rename.

#[serde(rename = "camelCase")] won’t camel-case the field names; it renames the type. This breaks the API contract for list_users_prism/get_prism_user.

-#[serde(rename = "camelCase")]
+#[serde(rename_all = "camelCase")]
 pub struct UsersPrism {
src/handlers/http/modal/ingest/ingestor_rbac.rs (2)

84-103: Guard metadata mutation with the shared UPDATE_LOCK and avoid cloning userid.

Two tweaks:

  • Acquire the lock before reading/updating metadata to avoid TOCTOU.
  • Pass &userid directly to Users.add_roles.
 pub async fn add_roles_to_user(
     userid: web::Path<String>,
     roles_to_add: web::Json<HashSet<String>>,
 ) -> Result<String, RBACError> {
     let userid = userid.into_inner();
     let roles_to_add = roles_to_add.into_inner();

+    let _guard = UPDATE_LOCK.lock().await;
     if !Users.contains(&userid) {
         return Err(RBACError::UserDoesNotExist);
     };
@@
-    let _ = storage::put_staging_metadata(&metadata);
-    // update in mem table
-    Users.add_roles(&userid.clone(), roles_to_add.clone());
+    let _ = storage::put_staging_metadata(&metadata);
+    // update in mem table
+    Users.add_roles(&userid, roles_to_add.clone());

135-175: Same here: hold UPDATE_LOCK and avoid cloning userid.

Mirror the locking discipline from delete and rbac handlers; pass &userid.

 pub async fn remove_roles_from_user(
     userid: web::Path<String>,
     roles_to_remove: web::Json<HashSet<String>>,
 ) -> Result<String, RBACError> {
     let userid = userid.into_inner();
     let roles_to_remove = roles_to_remove.into_inner();

+    let _guard = UPDATE_LOCK.lock().await;
     if !Users.contains(&userid) {
         return Err(RBACError::UserDoesNotExist);
     };
src/handlers/http/rbac.rs (4)

79-91: Bug: endpoint uses “username” but lookups are keyed by userid.

OAuth users will 404. Make the path parameter userid and look up by userid.

-/// Function for GET /users/{username}
-pub async fn get_prism_user(username: Path<String>) -> Result<impl Responder, RBACError> {
-    let username = username.into_inner();
+/// Function for GET /users/{userid}
+pub async fn get_prism_user(userid: Path<String>) -> Result<impl Responder, RBACError> {
+    let userid = userid.into_inner();
     // First check if the user exists
     let users = rbac::map::users();
-    if let Some(user) = users.get(&username) {
+    if let Some(user) = users.get(&userid) {
         let prism_user = to_prism_user(user);
         Ok(web::Json(prism_user))
     } else {
         Err(RBACError::UserDoesNotExist)
     }
 }

101-133: Avoid stale read: take UPDATE_LOCK before reading metadata; simplify existence check.

Current flow reads metadata, then locks, risking lost updates. Also the nested Users.contains is redundant.

-    let mut metadata = get_metadata().await?;
+    let _guard = UPDATE_LOCK.lock().await;
+    let mut metadata = get_metadata().await?;
@@
-    let _guard = UPDATE_LOCK.lock().await;
-    if Users.contains(&username) {
-        if Users.contains(&username)
-            || metadata.users.iter().any(|user| match &user.ty {
-                UserType::Native(basic) => basic.username == username,
-                UserType::OAuth(_) => false,
-            })
-        {
-            return Err(RBACError::UserExists);
-        }
-    }
+    if Users.contains(&username)
+        || metadata
+            .users
+            .iter()
+            .any(|user| matches!(&user.ty, UserType::Native(basic) if basic.username == username))
+    {
+        return Err(RBACError::UserExists);
+    }

158-166: Same TOCTOU: lock before reading metadata.

Ensure atomic read-modify-write for password reset.

-    let mut metadata = get_metadata().await?;
-
-    let _guard = UPDATE_LOCK.lock().await;
+    let _guard = UPDATE_LOCK.lock().await;
+    let mut metadata = get_metadata().await?;

185-220: Compile bug: userid is a Path; call into_inner() before use.

Handlers elsewhere already do this.

-pub async fn get_role(userid: web::Path<String>) -> Result<impl Responder, RBACError> {
-    if !Users.contains(&userid) {
+pub async fn get_role(userid: web::Path<String>) -> Result<impl Responder, RBACError> {
+    let userid = userid.into_inner();
+    if !Users.contains(&userid) {
         return Err(RBACError::UserDoesNotExist);
     };
-    let direct_roles: HashMap<String, Vec<DefaultPrivilege>> = Users
-        .get_role(&userid)
+    let direct_roles: HashMap<String, Vec<DefaultPrivilege>> = Users
+        .get_role(&userid)
@@
-    for user_group in Users.get_user_groups(&userid) {
+    for user_group in Users.get_user_groups(&userid) {
🧹 Nitpick comments (19)
src/rbac/utils.rs (2)

33-41: Clarify 'username' semantics (display name vs identifier).

You now set id=userid for both auth types, but username=userid (native) and username=display_name (OAuth). Ensure all downstream code treats id as the stable key and username purely as a display field; otherwise uniqueness assumptions may break.

Would you like me to scan handlers/UI for any equality checks or map keys still using username?


103-129: Unicode-safe masking.

mask_string uses .len() (bytes) while extracting the first char via .chars(), which can miscount for multi-byte names. Count chars, not bytes.

-        let parts: Vec<&str> = input.split('@').collect();
+        let parts: Vec<&str> = input.split('@').collect();

         //mask everything if not a proper email format
         if parts.len() != 2 {
-            return Some("X".repeat(input.len()));
+            return Some("X".repeat(input.chars().count()));
         }

         let username = parts[0];

-        let masked_username = if !username.is_empty() {
-            let first = username.chars().next().unwrap().to_uppercase().to_string();
-            format!("{}{}", first, "X".repeat(username.len() - 1))
+        let masked_username = if !username.is_empty() {
+            let mut it = username.chars();
+            let first = it.next().unwrap().to_uppercase().to_string();
+            let rest = it.count();
+            format!("{}{}", first, "X".repeat(rest))
         } else {
-            return Some("X".repeat(input.len()));
+            return Some("X".repeat(input.chars().count()));
         };

-        Some(format!("{masked_username}@XXX"))
+        Some(format!("{masked_username}@XXX"))
     } else {
         // mask all other strings with X
-        Some("X".repeat(input.len()))
+        Some("X".repeat(input.chars().count()))
     }
src/rbac/map.rs (3)

124-138: Use userid for admin session (good), but rename var to avoid confusion.

Logic is correct; consider renaming admin_username -> admin_userid for clarity.

-    let admin_username = admin.userid().to_owned();
+    let admin_userid = admin.userid().to_owned();
...
-    sessions.track_new(
-        admin_username,
+    sessions.track_new(
+        admin_userid,

196-204: Parameter naming drift after userid switch.

remove_user(&mut self, username: &str) still says username, but it now represents userid. Rename for clarity; no behavior change.

-    pub fn remove_user(&mut self, username: &str) {
-        let sessions = self.user_sessions.remove(username);
+    pub fn remove_user(&mut self, userid: &str) {
+        let sessions = self.user_sessions.remove(userid);

305-333: Make aggregate_group_permissions parameter name reflect userid.

Avoid mental mismatches; the map key is now userid.

-fn aggregate_group_permissions(username: &str) -> HashSet<Permission> {
+fn aggregate_group_permissions(userid: &str) -> HashSet<Permission> {
     let mut group_perms = HashSet::new();

-    let Some(user) = users().get(username).cloned() else {
+    let Some(user) = users().get(userid).cloned() else {
         return group_perms;
     };
src/utils/mod.rs (1)

61-66: Avoid unwrap on missing/invalid session key.

extract_session_key_from_req(req).unwrap() can panic. Prefer graceful error.

-    let session_key = extract_session_key_from_req(req).unwrap();
+    let Some(session_key) = extract_session_key_from_req(req) else {
+        return Err(RBACError::UserDoesNotExist);
+    };
src/handlers/http/modal/query_server.rs (1)

196-210: Route param mismatch and incorrect HTTP verb in docs for the same resource

  • The resource path is /{username} but the comment for DELETE says {userid}. Also, the comment says "PUT" while the code uses POST.
  • For clarity and consistency with userid-first APIs, prefer a single token name.

Apply:

-                web::resource("/{username}")
-                    // PUT /user/{username} => Create a new user
+                web::resource("/{userid}")
+                    // POST /user/{userid} => Create a new native user (userid=username)
                     .route(
                         web::post()
                             .to(querier_rbac::post_user)
                             .authorize(Action::PutUser),
                     )
                     // DELETE /user/{userid} => Delete a user
                     .route(
                         web::delete()
                             .to(querier_rbac::delete_user)
                             .authorize(Action::DeleteUser),
                     )
src/handlers/http/cluster/mod.rs (1)

334-349: Use userid all the way through (naming and docs)

Functionally correct: you now sync user creation via userid in the URL. For readability, consider renaming parameters and comments of related helpers (e.g., sync_users_with_roles_with_ingestors, sync_user_deletion_with_ingestors) from “username” to “userid” to avoid confusion. No behavior change needed.

src/handlers/http/modal/ingest_server.rs (1)

186-200: Keep route token consistent with userid

The resource is /{username}/sync but comments now reference {userid}. Prefer consistent tokens to avoid confusion (extractors don’t care about the token name, but docs and maintainability do).

-                web::resource("/{username}/sync")
-                    // PUT /user/{username}/sync => Sync creation of a new user
+                web::resource("/{userid}/sync")
+                    // POST /user/{userid}/sync => Sync creation of a new user
                     .route(
                         web::post()
                             .to(ingestor_rbac::post_user)
                             .authorize(Action::PutUser),
                     )
-                    // DELETE /user/{userid} => Sync deletion of a user
+                    // DELETE /user/{userid}/sync => Sync deletion of a user
src/handlers/http/oidc.rs (2)

370-388: Avoid unnecessary metadata writes in put_user

put_user writes metadata even when the user already exists (no mutation). Optimize to write only on insert.

-pub async fn put_user(
-    userid: &str,
-    group: HashSet<String>,
-    user_info: user::UserInfo,
-) -> Result<User, ObjectStorageError> {
-    let mut metadata = get_metadata().await?;
-
-    let user = metadata
-        .users
-        .iter()
-        .find(|user| user.userid() == userid)
-        .cloned()
-        .unwrap_or_else(|| {
-            let user = User::new_oauth(userid.to_owned(), group, user_info);
-            metadata.users.push(user.clone());
-            user
-        });
-
-    put_metadata(&metadata).await?;
-    Users.put_user(user.clone());
-    Ok(user)
-}
+pub async fn put_user(
+    userid: &str,
+    group: HashSet<String>,
+    user_info: user::UserInfo,
+) -> Result<User, ObjectStorageError> {
+    let mut metadata = get_metadata().await?;
+    if let Some(existing) = metadata.users.iter().find(|u| u.userid() == userid).cloned() {
+        Users.put_user(existing.clone());
+        return Ok(existing);
+    }
+    let user = User::new_oauth(userid.to_owned(), group, user_info);
+    metadata.users.push(user.clone());
+    put_metadata(&metadata).await?;
+    Users.put_user(user.clone());
+    Ok(user)
+}

398-443: Variable naming: old_userid (not old_username) + minor robustness

  • let old_username = user.userid().to_string(); is actually an old userid, which is confusing when reading migration logic. Rename for clarity.
  • Optional: If the user entry isn’t found in metadata (shouldn’t happen), consider inserting rather than silently doing nothing before writing.
-    let old_username = user.userid().to_string();
+    let old_userid = user.userid().to_string();
...
-        .find(|x| x.userid() == old_username)
+        .find(|x| x.userid() == old_userid)
...
-            group.users.retain(|u| u.userid() != old_username);
+            group.users.retain(|u| u.userid() != old_userid);
...
-    Users.delete_user(&old_username);
+    Users.delete_user(&old_userid);
src/handlers/http/modal/query/querier_rbac.rs (2)

73-83: Redundant existence checks — simplify

The nested Users.contains(&username) + repeated Users.contains is redundant; return early on first check, then validate metadata separately if needed.

-    let _guard = UPDATE_LOCK.lock().await;
-    if Users.contains(&username) {
-        if Users.contains(&username)
-            || metadata.users.iter().any(|user| match &user.ty {
-                UserType::Native(basic) => basic.username == username,
-                UserType::OAuth(_) => false, // OAuth users should be created differently
-            })
-        {
-            return Err(RBACError::UserExists);
-        }
-    }
+    let _guard = UPDATE_LOCK.lock().await;
+    if Users.contains(&username)
+        || metadata.users.iter().any(|u| matches!(&u.ty, UserType::Native(b) if b.username == username))
+    {
+        return Err(RBACError::UserExists);
+    }

104-166: Release UPDATE_LOCK before network sync; avoid repeat lookups

  • Hold the lock for metadata + in-memory mutations only; release before sync_user_deletion_with_ingestors to reduce contention.
  • Avoid repeated users().get(&userid) inside the loop. Compute once.
-    // also delete from user groups
-    let user_groups = Users.get_user_groups(&userid);
+    // also delete from user groups
+    let user_groups = Users.get_user_groups(&userid);
+    let user_opt = users().get(&userid).cloned();
     let mut groups_to_update = Vec::new();
     for user_group in user_groups {
         if let Some(ug) = write_user_groups().get_mut(&user_group) {
-            // Look up the user by display name to get the correct userid
-            if let Some(user) = users().get(&userid) {
-                let userid = match &user.ty {
+            if let Some(user) = user_opt.as_ref() {
+                let canonical_userid = match &user.ty {
                     UserType::Native(basic) => basic.username.clone(),
                     UserType::OAuth(oauth) => oauth.userid.clone(),
                 };
-                ug.remove_users_by_user_ids(HashSet::from_iter([userid]))?;
+                ug.remove_users_by_user_ids(HashSet::from_iter([canonical_userid]))?;
                 groups_to_update.push(ug.clone());
             } else {
                 // User not found, skip or log as needed
                 continue;
             }
         } else {
             continue;
         }
     }
@@
-    put_metadata(&metadata).await?;
-
-    sync_user_deletion_with_ingestors(&userid).await?;
-
-    // update in mem table
-    Users.delete_user(&userid);
+    put_metadata(&metadata).await?;
+    // update in mem table
+    Users.delete_user(&userid);
+    // release the lock before network I/O
+    drop(_guard);
+    sync_user_deletion_with_ingestors(&userid).await?;
src/rbac/mod.rs (3)

89-97: Prefer &str for hash to avoid unnecessary coupling to String.

Signature can accept &str; callers with String will coerce. Minor ergonomics win.

-    pub fn change_password_hash(&self, userid: &str, hash: &String) {
+    pub fn change_password_hash(&self, userid: &str, hash: &str) {
         if let Some(User {
             ty: UserType::Native(user),
             ..
         }) = mut_users().get_mut(userid)
         {
-            user.password_hash.clone_from(hash);
+            user.password_hash.clear();
+            user.password_hash.push_str(hash);
             mut_sessions().remove_user(userid);
         };
     }

100-105: Add missing semicolon after session invalidation.

Silences clippy’s “semicolon_if_nothing_returned” and clarifies intent.

-            mut_sessions().remove_user(userid)
+            mut_sessions().remove_user(userid);

107-113: Add missing semicolon after session invalidation.

Same nit as above.

-            mut_sessions().remove_user(userid)
+            mut_sessions().remove_user(userid);
src/handlers/http/modal/ingest/ingestor_rbac.rs (1)

192-197: Avoid cloning userid when updating in-memory roles.

No need to allocate here.

-    Users.remove_roles(&userid.clone(), roles_to_remove.clone());
+    Users.remove_roles(&userid, roles_to_remove.clone());
src/handlers/http/rbac.rs (2)

256-306: Avoid cloning userid when mutating roles.

Pass &userid directly.

-    Users.add_roles(&userid.clone(), roles_to_add);
+    Users.add_roles(&userid, roles_to_add);

308-370: Same nit: drop unnecessary userid clone.

-    Users.remove_roles(&userid.clone(), roles_to_remove);
+    Users.remove_roles(&userid, roles_to_remove);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3e6b947 and 3a7a06e.

📒 Files selected for processing (15)
  • src/handlers/http/cluster/mod.rs (1 hunks)
  • src/handlers/http/modal/ingest/ingestor_rbac.rs (7 hunks)
  • src/handlers/http/modal/ingest/ingestor_role.rs (1 hunks)
  • src/handlers/http/modal/ingest_server.rs (3 hunks)
  • src/handlers/http/modal/query/querier_rbac.rs (9 hunks)
  • src/handlers/http/modal/query/querier_role.rs (1 hunks)
  • src/handlers/http/modal/query_server.rs (3 hunks)
  • src/handlers/http/oidc.rs (5 hunks)
  • src/handlers/http/rbac.rs (14 hunks)
  • src/handlers/http/role.rs (1 hunks)
  • src/rbac/map.rs (4 hunks)
  • src/rbac/mod.rs (3 hunks)
  • src/rbac/user.rs (7 hunks)
  • src/rbac/utils.rs (2 hunks)
  • src/utils/mod.rs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/handlers/http/modal/ingest/ingestor_role.rs
  • src/handlers/http/role.rs
  • src/handlers/http/modal/query/querier_role.rs
  • src/rbac/user.rs
🧰 Additional context used
🧬 Code graph analysis (10)
src/utils/mod.rs (1)
src/handlers/http/rbac.rs (1)
  • Users (67-67)
src/handlers/http/cluster/mod.rs (3)
src/rbac/user.rs (2)
  • userid (84-89)
  • userid (243-245)
src/handlers/http/modal/mod.rs (2)
  • domain_name (572-572)
  • domain_name (579-581)
src/handlers/http/mod.rs (1)
  • base_path_without_preceding_slash (78-80)
src/rbac/utils.rs (1)
src/rbac/user.rs (2)
  • userid (84-89)
  • userid (243-245)
src/rbac/mod.rs (2)
src/rbac/map.rs (5)
  • mut_sessions (98-104)
  • mut_users (66-72)
  • users (58-64)
  • roles (74-80)
  • sessions (90-96)
src/rbac/user.rs (8)
  • userid (84-89)
  • userid (243-245)
  • is_oauth (106-108)
  • is_oauth (247-249)
  • hash (233-235)
  • add_roles (347-357)
  • roles (110-112)
  • remove_roles (371-388)
src/handlers/http/modal/query_server.rs (2)
src/handlers/http/rbac.rs (1)
  • get_role (185-220)
src/rbac/mod.rs (1)
  • get_role (76-81)
src/rbac/map.rs (1)
src/rbac/user.rs (2)
  • userid (84-89)
  • userid (243-245)
src/handlers/http/oidc.rs (2)
src/rbac/user.rs (4)
  • userid (84-89)
  • userid (243-245)
  • new_oauth (69-82)
  • from_user (255-277)
src/handlers/http/role.rs (1)
  • put_metadata (153-157)
src/handlers/http/modal/query/querier_rbac.rs (5)
src/rbac/map.rs (3)
  • roles (74-80)
  • users (58-64)
  • write_user_groups (50-56)
src/rbac/user.rs (4)
  • roles (110-112)
  • username (239-241)
  • userid (84-89)
  • userid (243-245)
src/handlers/http/modal/ingest/ingestor_rbac.rs (1)
  • delete_user (58-82)
src/rbac/mod.rs (1)
  • delete_user (83-86)
src/handlers/http/cluster/mod.rs (1)
  • sync_users_with_roles_with_ingestors (224-282)
src/handlers/http/rbac.rs (5)
src/rbac/map.rs (3)
  • read_user_groups (42-48)
  • roles (74-80)
  • users (58-64)
src/rbac/user.rs (4)
  • roles (110-112)
  • username (239-241)
  • userid (84-89)
  • userid (243-245)
src/rbac/utils.rs (1)
  • to_prism_user (31-85)
src/rbac/mod.rs (2)
  • get_role (76-81)
  • delete_user (83-86)
src/handlers/http/modal/ingest/ingestor_rbac.rs (3)
  • delete_user (58-82)
  • add_roles_to_user (85-133)
  • remove_roles_from_user (136-197)
src/handlers/http/modal/ingest/ingestor_rbac.rs (5)
src/handlers/http/rbac.rs (4)
  • Users (67-67)
  • delete_user (223-254)
  • add_roles_to_user (257-306)
  • remove_roles_from_user (309-370)
src/rbac/map.rs (2)
  • roles (74-80)
  • users (58-64)
src/rbac/user.rs (4)
  • roles (110-112)
  • userid (84-89)
  • userid (243-245)
  • username (239-241)
src/rbac/mod.rs (1)
  • delete_user (83-86)
src/storage/store_metadata.rs (1)
  • put_staging_metadata (286-301)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: Build Default x86_64-unknown-linux-gnu
  • GitHub Check: coverage
  • GitHub Check: Build Default aarch64-unknown-linux-gnu
  • GitHub Check: Build Default x86_64-apple-darwin
  • GitHub Check: Build Default x86_64-pc-windows-msvc
  • GitHub Check: Build Kafka aarch64-apple-darwin
  • GitHub Check: Build Default aarch64-apple-darwin
  • GitHub Check: Build Kafka x86_64-unknown-linux-gnu
  • GitHub Check: Quest Smoke and Load Tests for Distributed deployments
  • GitHub Check: Quest Smoke and Load Tests for Standalone deployments
🔇 Additional comments (18)
src/rbac/utils.rs (1)

59-73: Group lookups use userid, but session/permission code still uses username

  • get_user_groups is consistently called with userid everywhere (utils.rs, mod.rs, handlers)
  • However, in src/rbac/map.rs and src/rbac/mod.rs the auth/session APIs still accept and key off username, not userid
    users().get(username) (auth check)
    remove_user(&self, username: &str)
    active_sessions map entries (username, perms) and calls aggregate_group_permissions(username)
  • If these should now use userid (post-refactor), update those APIs and mappings; otherwise, rename parameters to clarify they’re login names
src/rbac/map.rs (3)

277-279: New Sessions::get_userid accessor aligns with userid-first design.

LGTM. Minimal and clear.


288-291: Index Users by userid (good).

Insertion path updated to userid as key; consistent with the PR objectives.


296-301: Build Users map keyed by userid (good).

The From<Vec> impl matches the new indexing.

src/utils/mod.rs (1)

60-68: Switched to get_userid_from_session (good).

This aligns with the userid-first refactor.

src/handlers/http/modal/query_server.rs (3)

212-217: Switched to userid for role read — looks consistent

The route /{userid}/role aligns with userid-based flows.


219-227: userid in role add path is correct

PATCH /user/{userid}/role/add matches the ingest/query syncing contract.


229-237: userid in role remove path is correct

PATCH /user/{userid}/role/remove is consistent with the rest of the PR.

src/handlers/http/modal/ingest_server.rs (2)

202-210: userid in role sync add path is correct

Matches the querier -> ingestor sync contract.


212-220: userid in role sync remove path is correct

Consistent and correct.

src/handlers/http/modal/query/querier_rbac.rs (3)

169-221: Role add flow via userid is correct

Validates roles, updates parseable.json and in-memory, and syncs using userid. Looks good.


222-286: Role remove flow via userid is correct

Checks presence, updates both metadata and memory, and syncs appropriately.


296-300: Lock around password reset — good

Ensures serialized writes for password updates.

src/rbac/mod.rs (2)

119-140: Good: session-derived userid authorization path.

Fetching userid from session and merging group-derived permissions looks correct and aligns with the userid pivot.


150-157: Good: sessions keyed by userid.

Session tracking by userid is consistent with the rest of the refactor.

src/handlers/http/modal/ingest/ingestor_rbac.rs (1)

57-82: LGTM: deletion path holds shared lock and updates staging + in-memory tables.

Consistent with the shared UPDATE_LOCK strategy.

src/handlers/http/rbac.rs (2)

41-43: LGTM: single shared async UPDATE_LOCK.

This resolves prior atomicity issues across modules.


64-77: LGTM: prism list endpoint builds DTOs via to_prism_user.

Consistent with userid-based roles and groups aggregation.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/handlers/http/rbac.rs (4)

448-469: Fix content-type for JSON error responses

You’re returning JSON while explicitly setting ContentType::plaintext(), which will mislead clients and break JSON parsing.

Apply:

match self {
    RBACError::RolesNotAssigned(obj) => actix_web::HttpResponse::build(self.status_code())
-        .insert_header(ContentType::plaintext())
-        .json(json!({
+        .json(json!({
             "roles_not_assigned": obj
         })),
    RBACError::RolesDoNotExist(obj) => actix_web::HttpResponse::build(self.status_code())
-        .insert_header(ContentType::plaintext())
-        .json(json!({
+        .json(json!({
             "non_existent_roles": obj
         })),
    RBACError::InvalidUserGroupRequest(obj) => {
        actix_web::HttpResponse::build(self.status_code())
-           .insert_header(ContentType::plaintext())
-           .json(obj)
+           .json(obj)
    }

260-310: Guard role-add mutations with UPDATE_LOCK

add_roles_to_user mutates metadata and in-memory Users without the global lock; concurrent updates can race and lose changes.

 pub async fn add_roles_to_user(
     userid: web::Path<String>,
     roles_to_add: web::Json<HashSet<String>>,
 ) -> Result<String, RBACError> {
     let userid = userid.into_inner();
     let roles_to_add = roles_to_add.into_inner();

     if !Users.contains(&userid) {
         return Err(RBACError::UserDoesNotExist);
     };

     // find username by userid, for native users, username is userid, for oauth users, we need to look up
     let username = if let Some(user) = users().get(&userid) {
         user.username_by_userid()
     } else {
         return Err(RBACError::UserDoesNotExist);
     };

     let mut non_existent_roles = Vec::new();
     // check if the role exists
     for role in &roles_to_add {
         if !roles().contains_key(role) {
             non_existent_roles.push(role.clone());
         }
     }
     if !non_existent_roles.is_empty() {
         return Err(RBACError::RolesDoNotExist(non_existent_roles));
     }

+    let _guard = UPDATE_LOCK.lock().await;
     // update parseable.json first
     let mut metadata = get_metadata().await?;
     if let Some(user) = metadata
         .users
         .iter_mut()
         .find(|user| user.userid() == userid)
     {
         user.roles.extend(roles_to_add.clone());
     } else {
         // should be unreachable given state is always consistent
         return Err(RBACError::UserDoesNotExist);
     }

     put_metadata(&metadata).await?;
     // update in mem table
-    Users.add_roles(&userid.clone(), roles_to_add);
+    Users.add_roles(&userid, roles_to_add);

     Ok(format!("Roles updated successfully for {username}"))
 }

312-374: Guard role-remove mutations with UPDATE_LOCK

Same race risk as add_roles_to_user; take the lock across metadata and Users updates.

 pub async fn remove_roles_from_user(
     userid: web::Path<String>,
     roles_to_remove: web::Json<HashSet<String>>,
 ) -> Result<String, RBACError> {
     let userid = userid.into_inner();
     let roles_to_remove = roles_to_remove.into_inner();

     if !Users.contains(&userid) {
         return Err(RBACError::UserDoesNotExist);
     };

     // find username by userid, for native users, username is userid, for oauth users, we need to look up
     let username = if let Some(user) = users().get(&userid) {
         user.username_by_userid()
     } else {
         return Err(RBACError::UserDoesNotExist);
     };

     let mut non_existent_roles = Vec::new();
     // check if the role exists
     for role in &roles_to_remove {
         if !roles().contains_key(role) {
             non_existent_roles.push(role.clone());
         }
     }
     if !non_existent_roles.is_empty() {
         return Err(RBACError::RolesDoNotExist(non_existent_roles));
     }

     // check for role not present with user
     let user_roles: HashSet<String> = HashSet::from_iter(Users.get_role(&userid));
     let roles_not_with_user: HashSet<String> =
         HashSet::from_iter(roles_to_remove.difference(&user_roles).cloned());
     if !roles_not_with_user.is_empty() {
         return Err(RBACError::RolesNotAssigned(Vec::from_iter(
             roles_not_with_user,
         )));
     }

+    let _guard = UPDATE_LOCK.lock().await;
     // update parseable.json first
     let mut metadata = get_metadata().await?;
     if let Some(user) = metadata
         .users
         .iter_mut()
         .find(|user| user.userid() == userid)
     {
         let diff: HashSet<String> =
             HashSet::from_iter(user.roles.difference(&roles_to_remove).cloned());
         user.roles = diff;
     } else {
         // should be unreachable given state is always consistent
         return Err(RBACError::UserDoesNotExist);
     }

     put_metadata(&metadata).await?;
     // update in mem table
-    Users.remove_roles(&userid.clone(), roles_to_remove);
+    Users.remove_roles(&userid, roles_to_remove);

     Ok(format!("Roles updated successfully for {username}"))
 }

83-95: GET /users/{username} uses the wrong key
The users map is keyed by each user’s userid (from User::userid()), not the display username, so OAuth users will always 404. Rename the path param to {userid} (and update docs/signature) or iterate users.values().find(|u| u.username_by_userid()==username).

🧹 Nitpick comments (2)
src/handlers/http/rbac.rs (2)

189-224: Minor: normalize Path usage in get_role

For consistency/readability, extract Path before use.

-pub async fn get_role(userid: web::Path<String>) -> Result<impl Responder, RBACError> {
-    if !Users.contains(&userid) {
+pub async fn get_role(userid: web::Path<String>) -> Result<impl Responder, RBACError> {
+    let userid = userid.into_inner();
+    if !Users.contains(&userid) {
         return Err(RBACError::UserDoesNotExist);
     };
-    let direct_roles: HashMap<String, Vec<DefaultPrivilege>> = Users
-        .get_role(&userid)
+    let direct_roles: HashMap<String, Vec<DefaultPrivilege>> = Users
+        .get_role(&userid)
         .iter()
         .filter_map(|role_name| {
             roles()
                 .get(role_name)
                 .map(|role| (role_name.to_owned(), role.clone()))
         })
         .collect();

-    for user_group in Users.get_user_groups(&userid) {
+    for user_group in Users.get_user_groups(&userid) {

472-478: Serde attribute likely intended as rename_all, not rename

rename = "camelCase" has no effect on field names; if you want camelCase fields (e.g., groupRoles), use rename_all.

-#[derive(Serialize)]
-#[serde(rename = "camelCase")]
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
 pub struct RolesResponse {
     #[serde(rename = "roles")]
     pub direct_roles: HashMap<String, Vec<DefaultPrivilege>>,
     pub group_roles: HashMap<String, HashMap<String, Vec<DefaultPrivilege>>>,
 }

Note: This changes group_roles -> groupRoles in responses. Verify API consumers before merging.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3a7a06e and 7f3f9ad.

📒 Files selected for processing (1)
  • src/handlers/http/rbac.rs (14 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/handlers/http/rbac.rs (4)
src/rbac/map.rs (3)
  • read_user_groups (42-48)
  • roles (74-80)
  • users (58-64)
src/rbac/user.rs (4)
  • roles (110-112)
  • username (239-241)
  • userid (84-89)
  • userid (243-245)
src/rbac/mod.rs (2)
  • get_role (76-81)
  • delete_user (83-86)
src/handlers/http/modal/query/querier_rbac.rs (3)
  • delete_user (105-166)
  • add_roles_to_user (169-220)
  • remove_roles_from_user (223-286)
🪛 GitHub Actions: Lint, Test and Coverage Report
src/handlers/http/rbac.rs

[error] 128-136: Clippy: this if statement can be collapsed (collapsible_if).

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Quest Smoke and Load Tests for Standalone deployments
  • GitHub Check: Quest Smoke and Load Tests for Distributed deployments
  • GitHub Check: Build Default x86_64-pc-windows-msvc
  • GitHub Check: Build Kafka x86_64-unknown-linux-gnu

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/handlers/http/rbac.rs (2)

83-95: Bug: get_prism_user looks up by userid, not username — OAuth users won’t resolve.

The map is keyed by userid; this handler receives a display name. Fall back to searching by username_by_userid().

-    let users = rbac::map::users();
-    if let Some(user) = users.get(&username) {
-        // Create UsersPrism for the found user only
-        let prism_user = to_prism_user(user);
-        Ok(web::Json(prism_user))
-    } else {
-        Err(RBACError::UserDoesNotExist)
-    }
+    let users = rbac::map::users();
+    if let Some(user) = users.get(&username) {
+        Ok(web::Json(to_prism_user(user)))
+    } else if let Some(user) = users.values().find(|u| u.username_by_userid() == username) {
+        Ok(web::Json(to_prism_user(user)))
+    } else {
+        Err(RBACError::UserDoesNotExist)
+    }

470-477: Serde container attr should be rename_all, not rename.

#[serde(rename = "camelCase")] has no effect on a struct; fields won’t be camelCased.

-#[serde(rename = "camelCase")]
+#[serde(rename_all = "camelCase")]
 pub struct RolesResponse {
♻️ Duplicate comments (2)
src/handlers/http/rbac.rs (2)

127-133: Collapse duplicate check in post_user.

Same logical issue as elsewhere; remove the repeated Users.contains.

-    let _guard = UPDATE_LOCK.lock().await;
-    if Users.contains(&username) && Users.contains(&username)
-        || metadata.users.iter().any(|user| match &user.ty {
-            UserType::Native(basic) => basic.username == username,
-            UserType::OAuth(_) => false, // OAuth users should be created differently
-        })
-    {
+    let _guard = UPDATE_LOCK.lock().await;
+    if Users.contains(&username)
+        || metadata.users.iter().any(|user| {
+            matches!(&user.ty, UserType::Native(basic) if basic.username == username)
+        })
+    {
         return Err(RBACError::UserExists);
     }

224-256: Close TOCTOU window in delete_user by holding the lock across the pre-check.

Acquire UPDATE_LOCK before checking group membership.

 pub async fn delete_user(userid: web::Path<String>) -> Result<impl Responder, RBACError> {
     let userid = userid.into_inner();

-    // if user is a part of any groups then don't allow deletion
+    let _guard = UPDATE_LOCK.lock().await;
+    // if user is a part of any groups then don't allow deletion
     if !Users.get_user_groups(&userid).is_empty() {
         return Err(RBACError::InvalidDeletionRequest(format!(
             "User: {userid} should not be a part of any groups"
         )));
     }
@@
-    let _guard = UPDATE_LOCK.lock().await;
🧹 Nitpick comments (3)
src/handlers/http/modal/query/querier_rbac.rs (2)

60-71: Tidy role validation loop and fix “existant” typo.

Readability and consistency.

-        let mut non_existant_roles = Vec::new();
-        user_roles
-            .iter()
-            .map(|r| {
-                if !roles().contains_key(r) {
-                    non_existant_roles.push(r.clone());
-                }
-            })
-            .for_each(drop);
-        if !non_existant_roles.is_empty() {
-            return Err(RBACError::RolesDoNotExist(non_existant_roles));
+        let mut non_existent_roles = Vec::new();
+        for r in &user_roles {
+            if !roles().contains_key(r) {
+                non_existent_roles.push(r.clone());
+            }
+        }
+        if !non_existent_roles.is_empty() {
+            return Err(RBACError::RolesDoNotExist(non_existent_roles));
         }

130-135: Avoid shadowing the outer userid in the deletion loop.

Shadowing is confusing during review/debugging.

-            let userid = match &user.ty {
+            let target_userid = match &user.ty {
                 UserType::Native(basic) => basic.username.clone(),
                 UserType::OAuth(oauth) => oauth.userid.clone(),
             };
-            ug.remove_users_by_user_ids(HashSet::from_iter([userid]))?;
+            ug.remove_users_by_user_ids(HashSet::from_iter([target_userid]))?;
src/rbac/user.rs (1)

317-319: Validate by userid assumes handlers resolved username+method → userid.

Given the API now accepts {username, method} for usergroup operations, ensure the HTTP layer resolves to a userid before constructing GroupUser; otherwise this validation will falsely flag existing users as non-existent.

I can patch the handlers to resolve username+method to userid via users() lookup and return per-user errors.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7f3f9ad and a55dac7.

📒 Files selected for processing (3)
  • src/handlers/http/modal/query/querier_rbac.rs (9 hunks)
  • src/handlers/http/rbac.rs (14 hunks)
  • src/rbac/user.rs (7 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/handlers/http/modal/query/querier_rbac.rs (5)
src/handlers/http/rbac.rs (4)
  • Users (71-71)
  • delete_user (225-256)
  • add_roles_to_user (259-308)
  • remove_roles_from_user (311-372)
src/rbac/user.rs (5)
  • roles (110-112)
  • username (239-241)
  • userid (84-89)
  • userid (243-245)
  • new (343-345)
src/rbac/map.rs (3)
  • roles (74-80)
  • users (58-64)
  • write_user_groups (50-56)
src/rbac/mod.rs (1)
  • delete_user (83-86)
src/handlers/http/cluster/mod.rs (1)
  • sync_users_with_roles_with_ingestors (224-282)
src/handlers/http/rbac.rs (4)
src/rbac/map.rs (3)
  • read_user_groups (42-48)
  • roles (74-80)
  • users (58-64)
src/rbac/user.rs (4)
  • roles (110-112)
  • username (239-241)
  • userid (84-89)
  • userid (243-245)
src/rbac/mod.rs (2)
  • get_role (76-81)
  • delete_user (83-86)
src/handlers/http/modal/query/querier_rbac.rs (3)
  • delete_user (103-161)
  • add_roles_to_user (164-215)
  • remove_roles_from_user (218-281)
src/rbac/user.rs (2)
src/rbac/map.rs (2)
  • users (58-64)
  • mut_sessions (98-104)
src/rbac/mod.rs (1)
  • is_oauth (68-70)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: Quest Smoke and Load Tests for Distributed deployments
  • GitHub Check: Build Default x86_64-pc-windows-msvc
  • GitHub Check: Build Default x86_64-apple-darwin
  • GitHub Check: Build Default aarch64-apple-darwin
  • GitHub Check: Build Default x86_64-unknown-linux-gnu
  • GitHub Check: Build Default aarch64-unknown-linux-gnu
  • GitHub Check: Quest Smoke and Load Tests for Standalone deployments
  • GitHub Check: Build Kafka x86_64-unknown-linux-gnu
  • GitHub Check: Build Kafka aarch64-apple-darwin
  • GitHub Check: coverage
🔇 Additional comments (1)
src/handlers/http/rbac.rs (1)

224-256: Inconsistent delete_user handlers – unify behavior

  • The HTTP route in src/handlers/http/rbac.rs blocks deletion if the user belongs to any groups, whereas the querier variant in src/handlers/http/modal/query/querier_rbac.rs cascades group removal and invokes sync_user_deletion_with_ingestors.
  • This duplication risks divergent outcomes depending on which endpoint is called.

Please choose one of the following:

  • Remove the basic HTTP handler and consolidate on the cascading variant.
  • Extend the basic handler to match the querier’s behavior (remove group memberships + call sync_user_deletion_with_ingestors).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
src/rbac/user.rs (2)

218-236: Eq/Hash scoped to userid only — good fix.

This prevents duplicate members when display name or method changes. Nice.


69-77: Do not fall back to display name for OAuth userid — use only stable subject.

Falling back to user_info.name can produce collisions and break identity stability. Only use OIDC sub (or the provided userid param as a last resort).

-            ty: UserType::OAuth(OAuth {
-                userid: user_info
-                    .sub
-                    .clone()
-                    .or_else(|| user_info.name.clone())
-                    .unwrap_or(userid),
-                user_info,
-            }),
+            ty: UserType::OAuth(OAuth {
+                // Use only a stable identifier for userid
+                userid: user_info.sub.clone().unwrap_or(userid),
+                user_info,
+            }),
🧹 Nitpick comments (4)
src/rbac/user.rs (4)

91-104: Prefer clearer naming and add preferred_username fallback; avoid extra clones.

  • Consider renaming to display_username() for intent.
  • Include preferred_username before email.
  • Avoid cloning user_info.
-    pub fn username_by_userid(&self) -> String {
-        match &self.ty {
-            UserType::Native(basic) => basic.username.clone(),
-            UserType::OAuth(oauth) => {
-                let user_info = oauth.user_info.clone();
-                user_info.name.clone().unwrap_or_else(|| {
-                    user_info
-                        .email
-                        .clone()
-                        .unwrap_or_else(|| oauth.userid.clone())
-                })
-            }
-        }
-    }
+    pub fn display_username(&self) -> String {
+        match &self.ty {
+            UserType::Native(basic) => basic.username.clone(),
+            UserType::OAuth(oauth) => {
+                let info = &oauth.user_info;
+                info.name
+                    .as_deref()
+                    .or(info.preferred_username.as_deref())
+                    .or(info.email.as_deref())
+                    .unwrap_or(&oauth.userid)
+                    .to_owned()
+            }
+        }
+    }

238-254: Use typed auth method or constants to avoid stringly-typed comparisons.

Replace "oauth"/"native" literals with:

  • An enum AuthMethod with serde rename_all = "lowercase", or
  • File-level constants to reduce typo risk.
+const METHOD_OAUTH: &str = "oauth";
+const METHOD_NATIVE: &str = "native";
@@
-    pub fn is_oauth(&self) -> bool {
-        self.method == "oauth"
+    pub fn is_oauth(&self) -> bool {
+        self.method == METHOD_OAUTH
     }
@@
-    pub fn user_type(&self) -> &str {
-        if self.is_oauth() { "oauth" } else { "native" }
+    pub fn user_type(&self) -> &str {
+        if self.is_oauth() { METHOD_OAUTH } else { METHOD_NATIVE }
     }

255-277: Reasonable defaults for display name; reuse method constants.

Construction from User looks solid; suggest using the method constants (or enum) here too.

-                method: "native".to_string(),
+                method: METHOD_NATIVE.to_string(),
@@
-                GroupUser {
+                GroupUser {
                     userid: userid.clone(),
                     username: display_username,
-                    method: "oauth".to_string(),
+                    method: METHOD_OAUTH.to_string(),
                 }

316-321: Validation good; consider including display names in error payload.

You currently return only userids in non_existent_users. Including username could improve DX for API clients.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a55dac7 and 33ccd47.

📒 Files selected for processing (2)
  • src/handlers/http/rbac.rs (14 hunks)
  • src/rbac/user.rs (7 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/handlers/http/rbac.rs
🧰 Additional context used
🧬 Code graph analysis (1)
src/rbac/user.rs (2)
src/rbac/map.rs (2)
  • users (58-64)
  • mut_sessions (98-104)
src/rbac/mod.rs (1)
  • is_oauth (68-70)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: Build Default x86_64-pc-windows-msvc
  • GitHub Check: Build Default x86_64-apple-darwin
  • GitHub Check: Build Default aarch64-apple-darwin
  • GitHub Check: Build Kafka x86_64-unknown-linux-gnu
  • GitHub Check: Quest Smoke and Load Tests for Distributed deployments
  • GitHub Check: Quest Smoke and Load Tests for Standalone deployments
  • GitHub Check: Build Default x86_64-unknown-linux-gnu
  • GitHub Check: Build Default aarch64-unknown-linux-gnu
  • GitHub Check: coverage
  • GitHub Check: Build Kafka aarch64-apple-darwin
🔇 Additional comments (8)
src/rbac/user.rs (8)

84-89: Getter reads well; return-by-ref is correct.

userid() uses a stable key across user types with zero copy. LGTM.


343-345: Constructor aligns with new model.

Signature and field init are coherent. LGTM.


353-355: Session invalidation on role changes — good.

Ensures permissions refresh. LGTM.


384-386: Session refresh on role removal — good.

Behavior is consistent with add/remove roles. LGTM.


390-406: Set ops for removal are correct and dedupe-safe.

Intersection to compute removed_users + session refresh before replacing set looks good.


408-412: Helper is fine.

Efficient projection of userids. LGTM.


413-419: Nice convenience API.

from_user usage avoids call-site duplication. LGTM.


420-435: Removal by id is clean and leverages Eq/Hash semantics.

Good use of filter + cloned set. LGTM.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/handlers/http/rbac.rs (2)

262-307: Add UPDATE_LOCK to serialize role-add mutations.

Align with other mutating endpoints; prevents concurrent lost updates.

 pub async fn add_roles_to_user(
     userid: web::Path<String>,
     roles_to_add: web::Json<HashSet<String>>,
 ) -> Result<String, RBACError> {
     let userid = userid.into_inner();
     let roles_to_add = roles_to_add.into_inner();
+    let _guard = UPDATE_LOCK.lock().await;

314-371: Add UPDATE_LOCK to serialize role-remove mutations.

Same rationale as add.

 pub async fn remove_roles_from_user(
     userid: web::Path<String>,
     roles_to_remove: web::Json<HashSet<String>>,
 ) -> Result<String, RBACError> {
     let userid = userid.into_inner();
     let roles_to_remove = roles_to_remove.into_inner();
+    let _guard = UPDATE_LOCK.lock().await;
🧹 Nitpick comments (8)
src/rbac/user.rs (3)

87-99: Prefer preferred_username -> name -> email for display.

Small UX tweak to follow common OIDC precedence and avoid extra clone.

Apply:

-            let user_info = oauth.user_info.clone();
-            user_info.name.clone().unwrap_or_else(|| {
-                user_info
-                    .email
-                    .clone()
-                    .unwrap_or_else(|| oauth.userid.clone())
-            })
+            let user_info = &oauth.user_info;
+            user_info
+                .preferred_username
+                .clone()
+                .or_else(|| user_info.name.clone())
+                .or_else(|| user_info.email.clone())
+                .unwrap_or_else(|| oauth.userid.clone())

243-249: Normalize method checks; avoid case/typo sensitivity.

is_oauth() currently relies on exact string match. Make it resilient to input variants.

-    pub fn is_oauth(&self) -> bool {
-        self.method == "oauth"
-    }
+    pub fn is_oauth(&self) -> bool {
+        self.method.eq_ignore_ascii_case("oauth")
+    }

Optionally, centralize method literals (const METHOD_OAUTH: &str = "oauth"; const METHOD_NATIVE: &str = "native";) to avoid typos across modules.


251-273: Use preferred_username first when deriving display name for OAuth.

Aligns with typical IdP semantics.

-                let display_username = user_info
-                    .name
-                    .clone()
-                    .or_else(|| user_info.email.clone())
-                    .unwrap_or_else(|| userid.clone()); // fallback to userid if nothing else available
+                let display_username = user_info
+                    .preferred_username
+                    .clone()
+                    .or_else(|| user_info.name.clone())
+                    .or_else(|| user_info.email.clone())
+                    .unwrap_or_else(|| userid.clone()); // fallback to userid if nothing else available
src/handlers/http/modal/query/querier_rbac.rs (3)

73-81: Remove duplicate existence check (also satisfies Clippy).

Condition repeats Users.contains(&username) and obscures intent.

-    let _guard = UPDATE_LOCK.lock().await;
-    if Users.contains(&username) && Users.contains(&username)
-        || metadata.users.iter().any(|user| match &user.ty {
-            UserType::Native(basic) => basic.username == username,
-            UserType::OAuth(_) => false, // OAuth users should be created differently
-        })
-    {
+    let _guard = UPDATE_LOCK.lock().await;
+    if Users.contains(&username)
+        || metadata.users.iter().any(|user| {
+            matches!(&user.ty, UserType::Native(basic) if basic.username == username)
+        })
+    {
         return Err(RBACError::UserExists);
     }

127-153: Nit: avoid redundant user lookup and branching; use the already-known userid.

You already have the canonical userid; reuse it instead of re-deriving via match.

-            let userid = match &user.ty {
-                UserType::Native(basic) => basic.username.clone(),
-                UserType::OAuth(oauth) => oauth.userid.clone(),
-            };
-            ug.remove_users_by_user_ids(HashSet::from_iter([userid]))?;
+            let uid = userid.clone();
+            ug.remove_users_by_user_ids(HashSet::from_iter([uid]))?;

182-194: Spelling/consistency: use non_existent_roles.

Minor naming fix; improves readability and keeps error messages consistent across modules.

-    let mut non_existant_roles = Vec::new();
+    let mut non_existent_roles = Vec::new();
@@
-    if !non_existant_roles.is_empty() {
-        return Err(RBACError::RolesDoNotExist(non_existant_roles));
+    if !non_existent_roles.is_empty() {
+        return Err(RBACError::RolesDoNotExist(non_existent_roles));

Apply similarly in remove_roles_from_user.

Also applies to: 236-247

src/handlers/http/rbac.rs (2)

127-135: Collapse duplicate existence check.

Same issue here—double Users.contains(&username).

-    let _guard = UPDATE_LOCK.lock().await;
-    if Users.contains(&username) && Users.contains(&username)
-        || metadata.users.iter().any(|user| match &user.ty {
-            UserType::Native(basic) => basic.username == username,
-            UserType::OAuth(_) => false, // OAuth users should be created differently
-        })
-    {
+    let _guard = UPDATE_LOCK.lock().await;
+    if Users.contains(&username)
+        || metadata.users.iter().any(|user| {
+            matches!(&user.ty, UserType::Native(basic) if basic.username == username)
+        })
+    {
         return Err(RBACError::UserExists);
     }

470-475: Incorrect serde attribute; use rename_all or drop it.

#[serde(rename = "camelCase")] renames the type, not fields. If the intent is camelCase fields, switch to rename_all. Otherwise, remove it.

-#[serde(rename = "camelCase")]
+#[serde(rename_all = "camelCase")]
 pub struct RolesResponse {
     #[serde(rename = "roles")]
     pub direct_roles: HashMap<String, Vec<DefaultPrivilege>>,
     pub group_roles: HashMap<String, HashMap<String, Vec<DefaultPrivilege>>>,
 }

If clients expect group_roles (snake_case), keep the current field name by removing the struct-level attribute instead.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 33ccd47 and f4019b4.

📒 Files selected for processing (3)
  • src/handlers/http/modal/query/querier_rbac.rs (9 hunks)
  • src/handlers/http/rbac.rs (14 hunks)
  • src/rbac/user.rs (7 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-05T06:46:24.562Z
Learnt from: nikhilsinhaparseable
PR: parseablehq/parseable#1422
File: src/rbac/user.rs:359-369
Timestamp: 2025-09-05T06:46:24.562Z
Learning: In the Parseable codebase, user existence validation for UserGroup operations is handled by the caller methods, not within the UserGroup methods themselves. The add_users() method in UserGroup trusts that callers have already validated user existence.

Applied to files:

  • src/rbac/user.rs
🧬 Code graph analysis (3)
src/handlers/http/rbac.rs (6)
src/rbac/map.rs (3)
  • read_user_groups (42-48)
  • roles (74-80)
  • users (58-64)
src/rbac/user.rs (4)
  • roles (106-108)
  • username (235-237)
  • userid (80-85)
  • userid (239-241)
src/rbac/mod.rs (2)
  • get_role (76-81)
  • delete_user (83-86)
src/handlers/http/modal/query/querier_rbac.rs (3)
  • delete_user (103-161)
  • add_roles_to_user (164-215)
  • remove_roles_from_user (218-281)
src/handlers/http/modal/ingest/ingestor_rbac.rs (3)
  • delete_user (58-82)
  • add_roles_to_user (85-133)
  • remove_roles_from_user (136-197)
src/handlers/http/role.rs (2)
  • get_metadata (143-151)
  • put_metadata (153-157)
src/handlers/http/modal/query/querier_rbac.rs (4)
src/handlers/http/rbac.rs (4)
  • Users (71-71)
  • delete_user (226-255)
  • add_roles_to_user (258-307)
  • remove_roles_from_user (310-371)
src/rbac/user.rs (5)
  • roles (106-108)
  • username (235-237)
  • userid (80-85)
  • userid (239-241)
  • new (339-341)
src/rbac/map.rs (3)
  • roles (74-80)
  • users (58-64)
  • write_user_groups (50-56)
src/handlers/http/cluster/mod.rs (2)
  • sync_user_deletion_with_ingestors (285-322)
  • sync_users_with_roles_with_ingestors (224-282)
src/rbac/user.rs (2)
src/rbac/map.rs (2)
  • users (58-64)
  • mut_sessions (98-104)
src/rbac/mod.rs (1)
  • is_oauth (68-70)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: Build Default aarch64-unknown-linux-gnu
  • GitHub Check: Build Default aarch64-apple-darwin
  • GitHub Check: Build Default x86_64-unknown-linux-gnu
  • GitHub Check: Build Default x86_64-pc-windows-msvc
  • GitHub Check: Build Default x86_64-apple-darwin
  • GitHub Check: Build Kafka aarch64-apple-darwin
  • GitHub Check: Build Kafka x86_64-unknown-linux-gnu
  • GitHub Check: coverage
  • GitHub Check: Quest Smoke and Load Tests for Distributed deployments
  • GitHub Check: Quest Smoke and Load Tests for Standalone deployments
🔇 Additional comments (7)
src/rbac/user.rs (3)

69-78: Good: OAuth userid uses stable OIDC sub.

Using user_info.sub.clone().unwrap_or(userid) avoids falling back to display fields and keeps identity stable.


80-86: LGTM: unified userid accessor.

The accessor correctly returns the canonical ID for both native and OAuth users.


214-233: Correct: Eq/Hash by userid only prevents duplicates on display-name change.

Manual PartialEq/Eq/Hash scoped to userid is the right call.

src/handlers/http/modal/query/querier_rbac.rs (1)

30-36: Good: reusing shared UPDATE_LOCK from rbac.

This removes duplicate locks and preserves atomicity across modules.

src/handlers/http/rbac.rs (3)

45-46: Good: single, shared async UPDATE_LOCK.

Exposing a shared lock point is the right primitive for serializing metadata + in-memory updates.


185-193: Fix validated: path param extracted before use.

let userid = userid.into_inner(); resolves the earlier type mismatch.


225-255: Good: lock held across membership check and deletion.

This closes the TOCTOU window during user deletion.

coderabbitai[bot]
coderabbitai bot previously approved these changes Sep 5, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/handlers/http/modal/query/querier_rbac.rs (1)

73-100: Release UPDATE_LOCK before network calls and nested handler to avoid deadlocks/lock contention.

Lock is held across put_metadata (await), sync_user_creation_with_ingestors (await), and a nested call to add_roles_to_user. If add_roles_to_user ever acquires UPDATE_LOCK (it should), this can deadlock. Narrow the critical section and avoid awaiting under the lock.

Apply:

@@
-    let _guard = UPDATE_LOCK.lock().await;
-    if Users.contains(&username)
+    // Prepare user before locking to allow releasing the lock prior to network calls
+    let (user, password) = user::User::new_basic(username.clone());
+    let created_role = user_roles.clone();
+
+    {
+        let _guard = UPDATE_LOCK.lock().await;
+        if Users.contains(&username)
             || metadata
                 .users
                 .iter()
                 .any(|user| matches!(&user.ty, UserType::Native(basic) if basic.username == username))
         {
             return Err(RBACError::UserExists);
         }
 
-    let (user, password) = user::User::new_basic(username.clone());
-    metadata.users.push(user.clone());
-    put_metadata(&metadata).await?;
-    let created_role = user_roles.clone();
-    Users.put_user(user.clone());
+        metadata.users.push(user.clone());
+        put_metadata(&metadata).await?;
+        Users.put_user(user.clone());
+    } // release UPDATE_LOCK before external I/O and nested handler
 
-    sync_user_creation_with_ingestors(user, &Some(user_roles)).await?;
+    sync_user_creation_with_ingestors(user, &Some(user_roles)).await?;
     add_roles_to_user(
         web::Path::<String>::from(username.clone()),
         web::Json(created_role),
     )
     .await?;
♻️ Duplicate comments (1)
src/handlers/http/modal/query/querier_rbac.rs (1)

168-181: Acquire UPDATE_LOCK while mutating metadata and Users in add_roles_to_user.

Without the lock, concurrent add/remove can cause lost updates in parseable.json and in-memory Users.

 pub async fn add_roles_to_user(
     userid: web::Path<String>,
     roles_to_add: web::Json<HashSet<String>>,
 ) -> Result<String, RBACError> {
     let userid = userid.into_inner();
     let roles_to_add = roles_to_add.into_inner();
 
+    let _guard = UPDATE_LOCK.lock().await;

Note: With the change above, ensure post_user releases UPDATE_LOCK before calling add_roles_to_user (see earlier diff).

🧹 Nitpick comments (3)
src/handlers/http/modal/query/querier_rbac.rs (3)

124-140: Avoid repeated locks/lookups; compute member_id once and hold the user-group write guard once.

We fetch the same user repeatedly and acquire/release the user-group write guard inside the loop. This increases contention and the else branch is unreachable while UPDATE_LOCK is held.

-    let user_groups = Users.get_user_groups(&userid);
-    let mut groups_to_update = Vec::new();
-    for user_group in user_groups {
-        if let Some(ug) = write_user_groups().get_mut(&user_group)
-            && let Some(user) = users().get(&userid)
-        {
-            let userid = match &user.ty {
-                UserType::Native(basic) => basic.username.clone(),
-                UserType::OAuth(oauth) => oauth.userid.clone(),
-            };
-            ug.remove_users_by_user_ids(HashSet::from_iter([userid]))?;
-            groups_to_update.push(ug.clone());
-        } else {
-            // User not found, skip or log as needed
-            continue;
-        }
-    }
+    let user_groups = Users.get_user_groups(&userid);
+    let member_id = users()
+        .get(&userid)
+        .map(|u| u.userid().to_string())
+        .ok_or(RBACError::UserDoesNotExist)?;
+    let mut groups_to_update = Vec::new();
+    {
+        let mut ug_guard = write_user_groups();
+        for group_name in user_groups {
+            if let Some(ug) = ug_guard.get_mut(&group_name) {
+                ug.remove_users_by_user_ids(HashSet::from([member_id.clone()]))?;
+                groups_to_update.push(ug.clone());
+            }
+        }
+    }

60-71: Spelling nit: non_existant_roles → non_existent_roles.

Pure readability; no behavior change.

-    let mut non_existant_roles = Vec::new();
+    let mut non_existent_roles = Vec::new();
@@
-    if !non_existant_roles.is_empty() {
-        return Err(RBACError::RolesDoNotExist(non_existant_roles));
+    if !non_existent_roles.is_empty() {
+        return Err(RBACError::RolesDoNotExist(non_existent_roles));

Also applies to: 182-189, 239-246


102-161: Optional: avoid awaiting under UPDATE_LOCK throughout handlers.

Shorten lock scopes to cover only state mutations; perform network syncs after releasing the lock to improve throughput and reduce risk of future deadlocks.

Also applies to: 163-215, 217-283

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 978a85e and c9028ef.

📒 Files selected for processing (1)
  • src/handlers/http/modal/query/querier_rbac.rs (9 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/handlers/http/modal/query/querier_rbac.rs (5)
src/rbac/user.rs (5)
  • roles (106-108)
  • username (235-237)
  • userid (80-85)
  • userid (239-241)
  • new (339-341)
src/rbac/map.rs (3)
  • roles (74-80)
  • users (58-64)
  • write_user_groups (50-56)
src/handlers/http/modal/ingest/ingestor_rbac.rs (3)
  • delete_user (58-82)
  • add_roles_to_user (85-133)
  • remove_roles_from_user (136-197)
src/rbac/mod.rs (1)
  • delete_user (83-86)
src/handlers/http/cluster/mod.rs (2)
  • sync_user_deletion_with_ingestors (285-322)
  • sync_users_with_roles_with_ingestors (224-282)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: Build Default aarch64-apple-darwin
  • GitHub Check: Build Default x86_64-pc-windows-msvc
  • GitHub Check: Build Default x86_64-apple-darwin
  • GitHub Check: Build Default x86_64-unknown-linux-gnu
  • GitHub Check: Build Default aarch64-unknown-linux-gnu
  • GitHub Check: Build Kafka aarch64-apple-darwin
  • GitHub Check: Build Kafka x86_64-unknown-linux-gnu
  • GitHub Check: coverage
  • GitHub Check: Quest Smoke and Load Tests for Distributed deployments
  • GitHub Check: Quest Smoke and Load Tests for Standalone deployments
🔇 Additional comments (3)
src/handlers/http/modal/query/querier_rbac.rs (3)

30-36: Imports look correct and align with the userid-centered model.

Bringing in UPDATE_LOCK, users(), and UserType is consistent with the refactor direction.


225-283: Good: Locking present during role removal.

UPDATE_LOCK is taken; metadata and Users updates are consistent.


293-316: Password reset flow is consistent.

Locking, metadata update, in-memory update, and cluster sync are in the right order.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/handlers/http/rbac.rs (1)

309-371: Add UPDATE_LOCK earlier in remove_roles_from_user.

The UPDATE_LOCK should be acquired before any checks that could lead to mutations, not in the middle of the function logic. This ensures atomicity throughout the operation.

Apply this diff to move the lock acquisition:

 pub async fn remove_roles_from_user(
     userid: web::Path<String>,
     roles_to_remove: web::Json<HashSet<String>>,
 ) -> Result<impl Responder, RBACError> {
     let userid = userid.into_inner();
     let roles_to_remove = roles_to_remove.into_inner();
+    let _guard = UPDATE_LOCK.lock().await;

     if !Users.contains(&userid) {
♻️ Duplicate comments (4)
src/handlers/http/rbac.rs (1)

127-136: Fix the redundant Users.contains(&username) check.

The condition has Users.contains(&username) && Users.contains(&username) which is redundant.

Apply this diff to fix the redundancy:

-    if Users.contains(&username) && Users.contains(&username)
-        || metadata.users.iter().any(|user| match &user.ty {
-            UserType::Native(basic) => basic.username == username,
-            UserType::OAuth(_) => false, // OAuth users should be created differently
-        })
+    if Users.contains(&username)
+        || metadata.users.iter().any(|user| match &user.ty {
+            UserType::Native(basic) => basic.username == username,
+            UserType::OAuth(_) => false, // OAuth users should be created differently
+        })
src/handlers/http/modal/query/querier_rbac.rs (3)

163-215: Hold UPDATE_LOCK and fix sync call in add_roles_to_user.

Two issues:

  1. Missing UPDATE_LOCK for atomic operations
  2. Sync API expects username, not userid

Apply this diff to fix both issues:

 pub async fn add_roles_to_user(
     userid: web::Path<String>,
     roles_to_add: web::Json<HashSet<String>>,
 ) -> Result<String, RBACError> {
     let userid = userid.into_inner();
     let roles_to_add = roles_to_add.into_inner();
+    let _guard = UPDATE_LOCK.lock().await;

     if !Users.contains(&userid) {
     // ... existing code ...

-    sync_users_with_roles_with_ingestors(&userid, &roles_to_add, "add").await?;
+    sync_users_with_roles_with_ingestors(&username, &roles_to_add, "add").await?;

217-283: Fix sync call in remove_roles_from_user.

The sync API expects username but the code passes userid, which will fail for OAuth users.

Apply this diff to fix the sync call:

-    sync_users_with_roles_with_ingestors(&userid, &roles_to_remove, "remove").await?;
+    sync_users_with_roles_with_ingestors(&username, &roles_to_remove, "remove").await?;

156-157: Sync with ingestors uses userid but the API expects username.

Based on the codebase analysis, sync_user_deletion_with_ingestors expects a username parameter and constructs the endpoint as /user/{username}/sync. Passing userid will fail for OAuth users where userid != username.

Apply this diff to fix the parameter:

-    sync_user_deletion_with_ingestors(&userid).await?;
+    sync_user_deletion_with_ingestors(&username).await?;
🧹 Nitpick comments (1)
src/handlers/http/rbac.rs (1)

257-307: Add UPDATE_LOCK for consistency in add_roles_to_user.

While the current implementation may work, for consistency with other mutation operations and to prevent potential race conditions, consider acquiring the UPDATE_LOCK.

Apply this diff to add the lock:

 pub async fn add_roles_to_user(
     userid: web::Path<String>,
     roles_to_add: web::Json<HashSet<String>>,
 ) -> Result<impl Responder, RBACError> {
     let userid = userid.into_inner();
     let roles_to_add = roles_to_add.into_inner();
+    let _guard = UPDATE_LOCK.lock().await;

     if !Users.contains(&userid) {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c9028ef and 19967fd.

📒 Files selected for processing (3)
  • src/handlers/http/modal/ingest/ingestor_rbac.rs (8 hunks)
  • src/handlers/http/modal/query/querier_rbac.rs (10 hunks)
  • src/handlers/http/rbac.rs (14 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/handlers/http/modal/query/querier_rbac.rs (5)
src/handlers/http/rbac.rs (4)
  • Users (71-71)
  • delete_user (226-255)
  • add_roles_to_user (258-307)
  • remove_roles_from_user (310-371)
src/rbac/user.rs (5)
  • roles (106-108)
  • username (235-237)
  • userid (80-85)
  • userid (239-241)
  • new (339-341)
src/rbac/map.rs (3)
  • roles (74-80)
  • users (58-64)
  • write_user_groups (50-56)
src/rbac/mod.rs (1)
  • delete_user (83-86)
src/handlers/http/cluster/mod.rs (2)
  • sync_user_deletion_with_ingestors (285-322)
  • sync_users_with_roles_with_ingestors (224-282)
src/handlers/http/modal/ingest/ingestor_rbac.rs (6)
src/handlers/http/role.rs (1)
  • get_metadata (143-151)
src/storage/object_storage.rs (1)
  • get_metadata (620-636)
src/handlers/http/rbac.rs (4)
  • Users (71-71)
  • delete_user (226-255)
  • add_roles_to_user (258-307)
  • remove_roles_from_user (310-371)
src/rbac/user.rs (4)
  • roles (106-108)
  • userid (80-85)
  • userid (239-241)
  • username (235-237)
src/rbac/map.rs (2)
  • roles (74-80)
  • users (58-64)
src/storage/store_metadata.rs (1)
  • put_staging_metadata (286-301)
src/handlers/http/rbac.rs (6)
src/rbac/map.rs (3)
  • read_user_groups (42-48)
  • roles (74-80)
  • users (58-64)
src/rbac/user.rs (4)
  • roles (106-108)
  • username (235-237)
  • userid (80-85)
  • userid (239-241)
src/rbac/utils.rs (1)
  • to_prism_user (31-85)
src/rbac/mod.rs (2)
  • get_role (76-81)
  • delete_user (83-86)
src/handlers/http/modal/ingest/ingestor_rbac.rs (1)
  • delete_user (58-82)
src/handlers/http/modal/query/querier_rbac.rs (1)
  • delete_user (103-161)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: Build Kafka x86_64-unknown-linux-gnu
  • GitHub Check: Build Default aarch64-apple-darwin
  • GitHub Check: Build Default x86_64-unknown-linux-gnu
  • GitHub Check: Build Default x86_64-apple-darwin
  • GitHub Check: Build Default x86_64-pc-windows-msvc
  • GitHub Check: Build Default aarch64-unknown-linux-gnu
  • GitHub Check: Build Kafka aarch64-apple-darwin
  • GitHub Check: coverage
  • GitHub Check: Quest Smoke and Load Tests for Standalone deployments
  • GitHub Check: Quest Smoke and Load Tests for Distributed deployments
🔇 Additional comments (18)
src/handlers/http/modal/ingest/ingestor_rbac.rs (8)

21-30: LGTM! Clean refactoring to use shared UPDATE_LOCK.

The migration to use the shared UPDATE_LOCK from the rbac module aligns well with the centralized locking strategy, ensuring consistent atomic operations across RBAC handlers.


57-65: LGTM! Proper lock acquisition order prevents race conditions.

The lock is acquired early (Line 60) before checking user existence, which prevents race conditions where a user could be deleted after the check but before acquiring the lock. This is the correct pattern.


66-72: LGTM! Good approach for userid-to-username resolution.

The code properly handles both native users (where userid == username) and OAuth users (where they differ) by using the username_by_userid() method. This ensures correct display names in response messages.


73-82: LGTM! Consistent userid-based operations.

The changes correctly use userid for both metadata filtering and in-memory state deletion, maintaining consistency throughout the system. The response includes the human-readable username which improves user experience.


84-102: LGTM! Comprehensive role existence validation.

The function properly validates all requested roles before making any changes, collecting non-existent roles to provide informative error messages. The userid-based lookups and username resolution for display are correctly implemented.


115-132: LGTM! Consistent userid-based role management.

The metadata update correctly uses userid for finding users, and the in-memory update maintains consistency. Good practice to use clone() for the userid parameter to avoid moving it.


153-196: LGTM! Thorough role removal validation.

The function properly:

  1. Validates role existence
  2. Verifies the user actually has the roles to be removed
  3. Updates both metadata and in-memory state using userid
  4. Returns an appropriate message with the username for clarity

198-221: LGTM! Consistent response type change.

The password generation handler correctly returns HttpResponse with JSON payload, maintaining consistency with other endpoints in the file.

src/handlers/http/rbac.rs (5)

45-46: LGTM! Well-structured centralized locking.

Making UPDATE_LOCK public at the crate level (pub(crate)) is the right approach for sharing it across RBAC modules while maintaining encapsulation.


54-66: LGTM! Consistent userid-based identity model.

The User struct correctly uses userid() for the id field, which properly handles both native users (userid == username) and OAuth users (userid != username).


83-95: LGTM! Well-implemented user lookup endpoint.

The new get_prism_user endpoint correctly validates user existence and returns enriched user information using the to_prism_user utility.


225-254: LGTM! Well-structured userid-based deletion.

The delete_user handler correctly:

  1. Acquires the lock early to prevent TOCTOU issues
  2. Checks group membership before allowing deletion
  3. Resolves userid to username for display
  4. Returns a JSON response with the human-readable username

373-382: LGTM! Comprehensive InvalidUserGroupError structure.

The new fields (non_existent_users, roles_not_in_group, users_not_in_group) and camelCase serialization align well with the PR's objective to enhance usergroup handling with userid/username/method.

src/handlers/http/modal/query/querier_rbac.rs (5)

73-81: LGTM! Proper lock acquisition and validation.

The lock is correctly acquired before any mutations, and the user existence check properly handles both in-memory and metadata validation to ensure consistency.


102-118: LGTM! Proper userid-based deletion with username resolution.

The handler correctly:

  1. Holds the lock throughout the operation
  2. Validates user existence
  3. Resolves userid to username for display purposes
  4. Properly cleans up metadata

123-140: LGTM! Proper userid extraction for group removal.

The code correctly extracts the userid/username based on user type (Native vs OAuth) before calling remove_users_by_user_ids, which resolves the issue from previous reviews where OAuth users weren't being properly removed from groups.


142-154: LGTM! Efficient group update logic.

The code efficiently updates existing groups in-place or appends new ones as needed, maintaining consistency in the metadata structure.


285-316: LGTM! Proper password reset with sync.

The password generation handler correctly:

  1. Acquires the lock before mutations
  2. Updates both metadata and in-memory state
  3. Syncs with ingestors using the correct username parameter

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/handlers/http/modal/ingest/ingestor_rbac.rs (3)

45-52: Bug: roles added under username key; must use userid.

For OAuth users, username != userid. Adding roles with username corrupts in-memory state. Also consider serializing mutations with UPDATE_LOCK.

-    let metadata = get_metadata().await?;
-    if let Some(body) = body {
-        let user: ParseableUser = serde_json::from_value(body.into_inner())?;
-        let _ = storage::put_staging_metadata(&metadata);
-        let created_role = user.roles.clone();
-        Users.put_user(user.clone());
-        Users.add_roles(&username, created_role.clone());
-    }
+    let metadata = get_metadata().await?;
+    if let Some(body) = body {
+        let user: ParseableUser = serde_json::from_value(body.into_inner())?;
+        let userid = user.userid().to_string();
+        let _guard = UPDATE_LOCK.lock().await;
+        Users.put_user(user.clone());
+        Users.add_roles(&userid, user.roles.clone());
+        // Optional: log staging errors instead of silently discarding
+        if let Err(e) = storage::put_staging_metadata(&metadata) {
+            tracing::warn!("failed to write staging metadata: {e}");
+        }
+    }

77-118: Serialize role-add updates and avoid clones.

  • Acquire UPDATE_LOCK to make the check/update atomic.
  • &userid suffices; avoid cloning.
  • Consider logging put_staging_metadata failures.
 pub async fn add_roles_to_user(
     userid: web::Path<String>,
     roles_to_add: web::Json<HashSet<String>>,
 ) -> Result<HttpResponse, RBACError> {
-    let userid = userid.into_inner();
+    let userid = userid.into_inner();
+    let _guard = UPDATE_LOCK.lock().await;
@@
-    let _ = storage::put_staging_metadata(&metadata);
+    if let Err(e) = storage::put_staging_metadata(&metadata) {
+        tracing::warn!("failed to write staging metadata: {e}");
+    }
     // update in mem table
-    Users.add_roles(&userid.clone(), roles_to_add.clone());
+    Users.add_roles(&userid, roles_to_add.clone());

121-175: Serialize role-remove updates; minor cleanups.

  • Acquire UPDATE_LOCK for atomicity with metadata + in-memory updates.
  • Avoid cloning userid.
  • Log staging errors rather than discarding.
 pub async fn remove_roles_from_user(
     userid: web::Path<String>,
     roles_to_remove: web::Json<HashSet<String>>,
 ) -> Result<HttpResponse, RBACError> {
-    let userid = userid.into_inner();
+    let userid = userid.into_inner();
     let roles_to_remove = roles_to_remove.into_inner();
+    let _guard = UPDATE_LOCK.lock().await;
@@
-    let _ = storage::put_staging_metadata(&metadata);
+    if let Err(e) = storage::put_staging_metadata(&metadata) {
+        tracing::warn!("failed to write staging metadata: {e}");
+    }
     // update in mem table
-    Users.remove_roles(&userid.clone(), roles_to_remove.clone());
+    Users.remove_roles(&userid, roles_to_remove.clone());
♻️ Duplicate comments (1)
src/handlers/http/modal/ingest/ingestor_rbac.rs (1)

120-120: Fix comment: this is the remove-roles handler.

-// Handler PATCH /user/{userid}/role/sync/add => Add roles to a user
+// Handler PATCH /user/{userid}/role/sync/remove => Remove roles from a user
🧹 Nitpick comments (3)
src/handlers/http/modal/ingest/ingestor_rbac.rs (3)

21-22: Drop redundant StatusCode usage and the import.

HttpResponse::Ok() already sets 200; .status(StatusCode::OK) is unnecessary. Removing it also lets you drop the http::StatusCode import.

- use http::StatusCode;
+ // use http::StatusCode; // no longer needed

- Ok(HttpResponse::Ok().status(StatusCode::OK).finish())
+ Ok(HttpResponse::Ok().finish())

Also applies to: 54-55, 74-75, 117-118, 174-175, 199-200


57-75: Deletion path looks correct; handle staging write errors.

You correctly switch to userid and hold UPDATE_LOCK. Please don’t discard errors from put_staging_metadata—log or propagate them to avoid silent desyncs.

-    let _ = storage::put_staging_metadata(&metadata);
+    if let Err(e) = storage::put_staging_metadata(&metadata) {
+        tracing::warn!("failed to write staging metadata: {e}");
+    }

179-200: Password sync: scope and resilience.

Assuming this ingestor endpoint “pulls” the latest hash from metadata (generated by the querier), the logic is fine. Add UPDATE_LOCK and don’t ignore staging write errors. Optional: rename new_hash to password_hash to avoid confusion.

 pub async fn post_gen_password(username: web::Path<String>) -> Result<HttpResponse, RBACError> {
     let username = username.into_inner();
-    let mut new_hash = String::default();
+    let mut new_hash = String::default();
     let mut metadata = get_metadata().await?;
 
-    let _ = storage::put_staging_metadata(&metadata);
+    let _guard = UPDATE_LOCK.lock().await;
+    if let Err(e) = storage::put_staging_metadata(&metadata) {
+        tracing::warn!("failed to write staging metadata: {e}");
+    }
@@
     Users.change_password_hash(&username, &new_hash);
     Ok(HttpResponse::Ok().finish())
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 19967fd and 0fe2ca6.

📒 Files selected for processing (2)
  • src/handlers/http/modal/ingest/ingestor_rbac.rs (9 hunks)
  • src/handlers/http/modal/ingest_server.rs (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/handlers/http/modal/ingest_server.rs
🧰 Additional context used
🧬 Code graph analysis (1)
src/handlers/http/modal/ingest/ingestor_rbac.rs (7)
src/handlers/http/role.rs (1)
  • get_metadata (143-151)
src/handlers/http/oidc.rs (1)
  • get_metadata (445-453)
src/storage/object_storage.rs (1)
  • get_metadata (620-636)
src/handlers/http/modal/query/querier_rbac.rs (4)
  • delete_user (103-161)
  • add_roles_to_user (164-215)
  • remove_roles_from_user (218-283)
  • post_gen_password (287-316)
src/rbac/mod.rs (1)
  • delete_user (83-86)
src/rbac/user.rs (3)
  • userid (80-85)
  • userid (239-241)
  • username (235-237)
src/storage/store_metadata.rs (1)
  • put_staging_metadata (286-301)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: Build Default x86_64-unknown-linux-gnu
  • GitHub Check: Build Default x86_64-pc-windows-msvc
  • GitHub Check: Build Default aarch64-apple-darwin
  • GitHub Check: Build Default x86_64-apple-darwin
  • GitHub Check: Build Default aarch64-unknown-linux-gnu
  • GitHub Check: Build Kafka x86_64-unknown-linux-gnu
  • GitHub Check: Build Kafka aarch64-apple-darwin
  • GitHub Check: Quest Smoke and Load Tests for Standalone deployments
  • GitHub Check: Quest Smoke and Load Tests for Distributed deployments
  • GitHub Check: coverage

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/handlers/http/cluster/mod.rs (2)

239-251: Percent-encode userid when embedding in the URL path.

OAuth/user IDs can contain reserved chars (e.g., |, @, +, spaces). Without percent-encoding, requests can hit the wrong route or 404. Encode the path segment before formatting the URL.

Apply:

-    let userid = userid.to_owned();
+    let userid = urlencoding::encode(userid).into_owned();
...
         let url = format!(
             "{}{}/user/{}/role/sync/{}",
             ingestor.domain_name,
             base_path_without_preceding_slash(),
-            userid,
+            userid,
             op
         );

No import is needed if you call urlencoding::encode(...) fully-qualified. Ensure urlencoding = "2" is in Cargo.toml.


239-251: Encode userid in all URL path segments
Interpolating raw userid into format! risks invalid or malicious URLs—percent-encode the segment (e.g., via the percent_encoding crate or URL builder).
Affected occurrences in src/handlers/http/cluster/mod.rs:

  • lines 243–247 (.../user/{}/role/sync/{})
  • lines 288–292 (.../user/{}/sync)
  • lines 343–347 (.../user/{}/sync)
  • lines 387–391 (.../user/{}/generate-new-password/sync)
🧹 Nitpick comments (1)
src/handlers/http/cluster/mod.rs (1)

234-237: Minor: avoid needless clone on role serialization.

to_vec(&role.clone()) allocates unnecessarily.

-    let role_data = to_vec(&role.clone()).map_err(|err| {
+    let role_data = to_vec(role).map_err(|err| {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9ce5124 and 0837819.

📒 Files selected for processing (2)
  • src/handlers/http/cluster/mod.rs (5 hunks)
  • src/handlers/http/modal/ingest/ingestor_rbac.rs (9 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/handlers/http/modal/ingest/ingestor_rbac.rs
🧰 Additional context used
🧬 Code graph analysis (1)
src/handlers/http/cluster/mod.rs (3)
src/rbac/user.rs (2)
  • userid (80-85)
  • userid (239-241)
src/handlers/http/modal/mod.rs (2)
  • domain_name (572-572)
  • domain_name (579-581)
src/handlers/http/mod.rs (1)
  • base_path_without_preceding_slash (78-80)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: Build Default x86_64-pc-windows-msvc
  • GitHub Check: Build Default aarch64-unknown-linux-gnu
  • GitHub Check: Build Default x86_64-unknown-linux-gnu
  • GitHub Check: Build Default x86_64-apple-darwin
  • GitHub Check: Build Default aarch64-apple-darwin
  • GitHub Check: Build Kafka x86_64-unknown-linux-gnu
  • GitHub Check: Build Kafka aarch64-apple-darwin
  • GitHub Check: coverage
  • GitHub Check: Quest Smoke and Load Tests for Distributed deployments
  • GitHub Check: Quest Smoke and Load Tests for Standalone deployments
🔇 Additional comments (1)
src/handlers/http/cluster/mod.rs (1)

225-232: Approved: PATCH routes for role sync confirmed
Ingestor-side routes for /role/sync/add and /role/sync/remove use web::patch().

@nitisht nitisht merged commit e980283 into parseablehq:main Sep 5, 2025
13 checks passed
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.

2 participants