Skip to content

feat: Introduce User Groups to Parseable #1366

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

parmesant
Copy link
Contributor

@parmesant parmesant commented Jul 4, 2025

  • Added support for user groups
  • Migrated PUT /user/{username}/role to PATCH /user/{username}/role/add and PATCH /user/{username}/role/remove

Fixes #XXXX.

Description


This PR has:

  • been tested to ensure log ingestion and log query works.
  • added comments explaining the "why" and the intent of the code wherever would not be obvious for an unfamiliar reader.
  • added documentation for new or modified features or behaviors.

Summary by CodeRabbit

  • New Features

    • Introduced user groups, allowing users to inherit roles and permissions from groups.
    • Added endpoints to add or remove roles from users separately, replacing the previous combined role update.
    • User role management now supports distinct routes for retrieving, adding, and removing roles.
    • Role and permission queries include both direct user roles and roles inherited from user groups.
  • Improvements

    • Enhanced validation for roles and user groups with detailed error reporting.
    • User deletion now removes users from all associated user groups to ensure consistency.
    • Role deletion checks for references in both users and user groups before removal.
    • Authorization checks aggregate permissions from both user roles and user group roles.
    • Metadata migration and storage updated to support user groups and new versioning.
    • Authorization middleware updated to support resource-based context including user groups and LLM IDs.
    • Permission model updated to unify resource specification and include user group actions.
    • Role synchronization URLs now specify add or remove operations explicitly.
  • Bug Fixes

    • Corrected error message typos and improved error response formatting for role and group operations.
  • Chores

    • Refactored permission model to use unified resource types instead of separate stream and tag fields.
    • Updated metadata migration logic to allow in-place updates during migration steps.
    • Simplified authorization middleware method names and enhanced context parameter detection.

Copy link
Contributor

coderabbitai bot commented Jul 4, 2025

Walkthrough

This update introduces user group support to the RBAC system, enabling users to inherit roles through group memberships. Role assignment endpoints are refactored to separate add/remove operations, and metadata migration is extended to support user groups. Permission checks, API responses, and storage structures are updated to reflect group-based role inheritance and improved validation.

Changes

File(s) Change Summary
src/handlers/http/cluster/mod.rs Updated role sync function to accept an operation parameter ("add"/"remove"), validating it and forwarding to ingestors with the operation in the URL.
src/handlers/http/modal/ingest/ingestor_rbac.rs Replaced single role assignment with separate add/remove role handlers; removed old put_role logic.
src/handlers/http/modal/ingest_server.rs Replaced PUT role sync endpoint with PATCH endpoints for add/remove roles, updating route configuration.
src/handlers/http/modal/query/querier_rbac.rs Enhanced role validation, added add/remove role handlers, improved user deletion to remove from groups, and updated role sync logic.
src/handlers/http/modal/query_server.rs
src/handlers/http/modal/server.rs
Changed user role endpoints: replaced PUT with GET for retrieval and PATCH for add/remove; updated metadata migration to use mutable JSON.
src/handlers/http/oidc.rs Expanded user struct destructuring to include user_groups in update logic.
src/handlers/http/rbac.rs Improved role validation, error reporting, and user-group-role consistency; added group-aware role retrieval and add/remove role endpoints; extended error types and response structures.
src/handlers/http/role.rs Extended role deletion to check and update user groups referencing the role, aborting if still in use.
src/migration/metadata_migration.rs
src/migration/mod.rs
Added migration step from v5 to v6 for user groups; migration pipeline now updates JSON in place and logs version changes.
src/rbac/map.rs Introduced global user group map, initialization, and permission checks incorporating group roles.
src/rbac/mod.rs Added user group membership and group-based role/permission aggregation to Users struct and APIs; split put_role into add_roles/remove_roles.
src/rbac/role.rs Added user group actions and resource-scoped permissions; updated role builder and permission generation logic.
src/rbac/user.rs Introduced UserGroup struct, group membership on User, group validation, and group management methods.
src/rbac/utils.rs Extended user-to-prism conversion to include group roles and group memberships in API responses.
src/storage/store_metadata.rs Updated metadata version to v6, added user_groups field to StorageMetadata struct, and fixed typo in error message.
src/utils/mod.rs Simplified table extraction logic by removing redundant into_inner() call.
src/handlers/http/middleware.rs Renamed authorization method from stream-based to resource-based; extended authorization context extraction to support usergroup and llmid parameters.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant API
    participant RBAC
    participant Metadata
    participant UserGroups

    Client->>API: PATCH /user/{username}/role/add (roles)
    API->>RBAC: add_roles_to_user(username, roles)
    RBAC->>Metadata: Update user roles in metadata
    RBAC->>UserGroups: Update user group memberships if needed
    RBAC-->>API: Success/Error
    API-->>Client: Response

    Client->>API: PATCH /user/{username}/role/remove (roles)
    API->>RBAC: remove_roles_from_user(username, roles)
    RBAC->>Metadata: Update user roles in metadata
    RBAC->>UserGroups: Update user group memberships if needed
    RBAC-->>API: Success/Error
    API-->>Client: Response

    Client->>API: GET /user/{username}/role
    API->>RBAC: get_role(username)
    RBAC->>UserGroups: Fetch group roles for user
    RBAC-->>API: RolesResponse (direct + group roles)
    API-->>Client: RolesResponse
Loading

Possibly related PRs

  • LLM related actions #1287: Adds LLM-related actions to the Action enum in src/rbac/role.rs, related by file and enum changes but focusing on different action sets than user groups.

Suggested labels

for next release

Suggested reviewers

  • nikhilsinhaparseable

Poem

🐇 In the garden where permissions bloom,
User groups now share their room.
Roles add, roles remove with care,
Metadata dances, fresh and fair.
APIs split paths, clear and bright,
RBAC’s future shines in light! 🌟

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Clippy (1.86.0)
Updating crates.io index
Updating git repository `https://github.com/parseablehq/opentelemetry-rust`

error: failed to get opentelemetry-proto as a dependency of package parseable v2.3.5 ()

Caused by:
failed to load source for dependency opentelemetry-proto

Caused by:
Unable to update https://github.com/parseablehq/opentelemetry-rust?branch=fix-metrics-u64-serialization#7e84c98d

Caused by:
failed to create directory /usr/local/git/db/opentelemetry-rust-e71049053dacb49f

Caused by:
Permission denied (os error 13)


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bcf72d1 and eb9de1d.

📒 Files selected for processing (8)
  • src/handlers/http/middleware.rs (3 hunks)
  • src/handlers/http/modal/ingest_server.rs (3 hunks)
  • src/handlers/http/modal/query_server.rs (6 hunks)
  • src/handlers/http/modal/server.rs (13 hunks)
  • src/rbac/map.rs (6 hunks)
  • src/rbac/role.rs (11 hunks)
  • src/rbac/user.rs (6 hunks)
  • src/utils/mod.rs (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/utils/mod.rs
  • src/handlers/http/modal/query_server.rs
  • src/rbac/user.rs
  • src/handlers/http/middleware.rs
  • src/handlers/http/modal/ingest_server.rs
  • src/handlers/http/modal/server.rs
🧰 Additional context used
🧠 Learnings (1)
src/rbac/map.rs (2)
Learnt from: nikhilsinhaparseable
PR: parseablehq/parseable#1305
File: src/handlers/http/users/dashboards.rs:0-0
Timestamp: 2025-05-01T10:27:56.858Z
Learning: The `add_tile()` function in `src/handlers/http/users/dashboards.rs` should use `get_dashboard_by_user(dashboard_id, &user_id)` instead of `get_dashboard(dashboard_id)` to ensure proper authorization checks when modifying a dashboard.
Learnt from: nikhilsinhaparseable
PR: parseablehq/parseable#1340
File: src/query/mod.rs:64-66
Timestamp: 2025-06-18T06:39:04.775Z
Learning: In src/query/mod.rs, QUERY_SESSION_STATE and QUERY_SESSION serve different architectural purposes: QUERY_SESSION_STATE is used for stats calculation and allows dynamic registration of individual parquet files from the staging path (files created every minute), while QUERY_SESSION is used for object store queries with the global schema provider. Session contexts with schema providers don't support registering individual tables/parquets, so both session objects are necessary for their respective use cases.
⏰ Context from checks skipped due to timeout of 90000ms (10)
  • GitHub Check: Build Default x86_64-pc-windows-msvc
  • 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: Build Default x86_64-apple-darwin
  • GitHub Check: coverage
  • GitHub Check: Build Kafka 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 (11)
src/rbac/map.rs (6)

19-40: LGTM: Imports and static variable declaration are consistent.

The new imports and USER_GROUPS static variable follow the established patterns in the codebase.


42-56: LGTM: Helper functions follow established patterns.

The helper functions for user groups are consistent with the existing pattern used for users and roles.


112-112: LGTM: User groups initialization follows the established pattern.

The initialization of user groups from metadata is consistent with how users and roles are initialized.


145-147: LGTM: Static variable initialization is consistent.

The USER_GROUPS static variable initialization follows the same pattern as other static variables in the function.


260-278: LGTM: Resource-based permission matching is correctly implemented.

The updated permission matching logic properly handles the new ParseableResourceType enum and provides appropriate authorization checks for different resource types.


320-341: LGTM: UserGroups struct follows established patterns.

The UserGroups struct implementation is consistent with the existing Users struct pattern, including the use of derive_more traits and the same insert/from implementations.

src/rbac/role.rs (5)

23-26: LGTM: User group actions are appropriately defined.

The new user group management actions (CreateUserGroup, GetUserGroup, ModifyUserGroup, DeleteUserGroup) follow the established naming convention and cover the necessary CRUD operations.


80-88: LGTM: ParseableResourceType enum provides clean abstraction.

The new enum properly abstracts different resource types with appropriate serde annotations. The variants (Stream, Llm, All) cover the necessary resource types for the permission system.


90-97: LGTM: Permission enum updated to use resource-based model.

The Permission enum is correctly updated to use ParseableResourceType instead of stream-specific variants, providing better abstraction for different resource types.


100-135: LGTM: RoleBuilder updated for resource-based permissions.

The RoleBuilder struct is correctly updated to use ParseableResourceType instead of separate stream and tag fields, providing better abstraction and consistency.


217-252: LGTM: DefaultPrivilege enum updated for resource-based model.

The DefaultPrivilege enum is correctly updated to use ParseableResourceType instead of stream-specific fields, and the From implementation properly handles the new structure.

✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 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.
    • Explain this complex logic.
    • 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. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • 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 src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

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

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai 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

Documentation and Community

  • 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: 8

🔭 Outside diff range comments (1)
src/handlers/http/modal/ingest/ingestor_rbac.rs (1)

109-137: Add role existence validation for consistency.

The add_roles_to_user handler is missing role existence validation that's present in the main RBAC handlers (src/handlers/http/rbac.rs lines 321-328). This could allow non-existent roles to be added to users.

Add role validation before processing:

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

     if !Users.contains(&username) {
         return Err(RBACError::UserDoesNotExist);
     };
+    
+    // check if all roles exist
+    let mut non_existent_roles = Vec::new();
+    roles_to_add.iter().for_each(|r| {
+        if roles().get(r).is_none() {
+            non_existent_roles.push(r.clone());
+        }
+    });
+    
+    if !non_existent_roles.is_empty() {
+        return Err(RBACError::RolesDoNotExist(non_existent_roles));
+    }
     
     // update parseable.json first
🧹 Nitpick comments (6)
src/migration/mod.rs (1)

107-107: Fix comment capitalization for consistency.

-                //remove querier endpoint and token from storage metadata
+                // Remove querier endpoint and token from storage metadata
src/rbac/mod.rs (1)

119-137: Correctly implements group-based permission inheritance.

The method properly aggregates permissions from both direct roles and roles inherited through user groups.

Consider extracting the group permission aggregation logic into a helper method for better readability:

fn get_group_permissions(&self, username: &str) -> Vec<Permission> {
    let user_groups = self.get_user_groups(username);
    let mut permissions = Vec::new();
    
    for group in user_groups {
        if let Some(group) = read_user_groups().get(&group) {
            for role in &group.roles {
                if let Some(privilege_list) = roles().get(role) {
                    for privilege in privilege_list {
                        permissions.extend(RoleBuilder::from(privilege).build());
                    }
                }
            }
        }
    }
    permissions
}
src/handlers/http/modal/query/querier_rbac.rs (2)

113-120: Simplify role validation logic.

The map().for_each(drop) pattern is unnecessarily complex for this use case.

-        user_roles
-            .iter()
-            .map(|r| {
-                if !roles().contains_key(r) {
-                    non_existant_roles.push(r.clone());
-                }
-            })
-            .for_each(drop);
+        for r in user_roles.iter() {
+            if !roles().contains_key(r) {
+                non_existant_roles.push(r.clone());
+            }
+        }

181-192: Simplify user group removal logic.

The map().for_each(drop) pattern is unnecessarily complex and reduces readability.

-    [&username]
-        .iter()
-        .map(|user| {
-            if let Some(user) = mut_users().get_mut(*user) {
-                for group in groups_to_update.iter() {
-                    user.user_groups.remove(&group.name);
-                }
-
-                metadata.users.retain(|u| u.username() != user.username());
-                metadata.users.push(user.clone());
-            }
-        })
-        .for_each(drop);
+    if let Some(user) = mut_users().get_mut(&username) {
+        for group in groups_to_update.iter() {
+            user.user_groups.remove(&group.name);
+        }
+
+        metadata.users.retain(|u| u.username() != user.username());
+        metadata.users.push(user.clone());
+    }
src/handlers/http/rbac.rs (2)

119-126: Simplify role validation logic.

Use a standard for loop instead of the complex map().for_each(drop) pattern for better readability.

-        user_roles
-            .iter()
-            .map(|r| {
-                if !roles().contains_key(r) {
-                    non_existant_roles.push(r.clone());
-                }
-            })
-            .for_each(drop);
+        for r in user_roles.iter() {
+            if !roles().contains_key(r) {
+                non_existant_roles.push(r.clone());
+            }
+        }

254-266: Simplify user metadata update logic.

The map().for_each(drop) pattern is unnecessarily complex for updating a single user.

-    [&username]
-        .iter()
-        .map(|user| {
-            if let Some(user) = mut_users().get_mut(*user) {
-                for group in groups_to_update.iter() {
-                    user.user_groups.remove(&group.name);
-                }
-
-                metadata.users.retain(|u| u.username() != user.username());
-                metadata.users.push(user.clone());
-            }
-        })
-        .for_each(drop);
+    if let Some(user) = mut_users().get_mut(&username) {
+        for group in groups_to_update.iter() {
+            user.user_groups.remove(&group.name);
+        }
+
+        metadata.users.retain(|u| u.username() != user.username());
+        metadata.users.push(user.clone());
+    }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 33ed89c and 8f6279b.

📒 Files selected for processing (18)
  • src/handlers/http/cluster/mod.rs (1 hunks)
  • src/handlers/http/modal/ingest/ingestor_rbac.rs (3 hunks)
  • src/handlers/http/modal/ingest_server.rs (1 hunks)
  • src/handlers/http/modal/query/querier_rbac.rs (5 hunks)
  • src/handlers/http/modal/query_server.rs (2 hunks)
  • src/handlers/http/modal/server.rs (2 hunks)
  • src/handlers/http/oidc.rs (1 hunks)
  • src/handlers/http/rbac.rs (9 hunks)
  • src/handlers/http/role.rs (3 hunks)
  • src/migration/metadata_migration.rs (1 hunks)
  • src/migration/mod.rs (4 hunks)
  • src/rbac/map.rs (6 hunks)
  • src/rbac/mod.rs (6 hunks)
  • src/rbac/role.rs (16 hunks)
  • src/rbac/user.rs (6 hunks)
  • src/rbac/utils.rs (3 hunks)
  • src/storage/store_metadata.rs (5 hunks)
  • src/utils/mod.rs (1 hunks)
🧰 Additional context used
🧠 Learnings (7)
src/utils/mod.rs (1)
Learnt from: nikhilsinhaparseable
PR: parseablehq/parseable#1340
File: src/query/mod.rs:64-66
Timestamp: 2025-06-18T06:39:04.775Z
Learning: In src/query/mod.rs, QUERY_SESSION_STATE and QUERY_SESSION serve different architectural purposes: QUERY_SESSION_STATE is used for stats calculation and allows dynamic registration of individual parquet files from the staging path (files created every minute), while QUERY_SESSION is used for object store queries with the global schema provider. Session contexts with schema providers don't support registering individual tables/parquets, so both session objects are necessary for their respective use cases.
src/handlers/http/cluster/mod.rs (2)
Learnt from: nikhilsinhaparseable
PR: parseablehq/parseable#1346
File: src/handlers/http/health_check.rs:81-90
Timestamp: 2025-06-16T02:04:58.990Z
Learning: In the shutdown function in src/handlers/http/health_check.rs, the design approach is to log errors from sync operations rather than propagate them. This is intentional because the shutdown function is called on SIGTERM/SIGINT signals, and the goal is to perform best-effort cleanup (syncing pending files to object storage) while allowing the shutdown to proceed regardless of sync failures. Logging provides debugging information without blocking the shutdown process.
Learnt from: nikhilsinhaparseable
PR: parseablehq/parseable#1305
File: src/handlers/http/users/dashboards.rs:0-0
Timestamp: 2025-05-01T10:27:56.858Z
Learning: The `add_tile()` function in `src/handlers/http/users/dashboards.rs` should use `get_dashboard_by_user(dashboard_id, &user_id)` instead of `get_dashboard(dashboard_id)` to ensure proper authorization checks when modifying a dashboard.
src/handlers/http/modal/query_server.rs (3)
Learnt from: nikhilsinhaparseable
PR: parseablehq/parseable#1305
File: src/handlers/http/users/dashboards.rs:0-0
Timestamp: 2025-05-01T10:27:56.858Z
Learning: The `add_tile()` function in `src/handlers/http/users/dashboards.rs` should use `get_dashboard_by_user(dashboard_id, &user_id)` instead of `get_dashboard(dashboard_id)` to ensure proper authorization checks when modifying a dashboard.
Learnt from: de-sh
PR: parseablehq/parseable#1185
File: src/handlers/http/logstream.rs:255-261
Timestamp: 2025-02-14T09:49:25.818Z
Learning: In Parseable's logstream handlers, stream existence checks must be performed for both query and standalone modes. The pattern `!PARSEABLE.streams.contains(&stream_name) && (PARSEABLE.options.mode != Mode::Query || !PARSEABLE.create_stream_and_schema_from_storage(&stream_name).await?)` ensures proper error handling in both modes.
Learnt from: nikhilsinhaparseable
PR: parseablehq/parseable#1346
File: src/parseable/streams.rs:319-331
Timestamp: 2025-06-16T09:50:38.636Z
Learning: In Parseable's Ingest or Query mode, the node_id is always available because it's generated during server initialization itself, before the get_node_id_string() function in streams.rs would be called. This makes the .expect() calls on QUERIER_META.get() and INGESTOR_META.get() safe in this context.
src/handlers/http/modal/ingest/ingestor_rbac.rs (1)
Learnt from: nikhilsinhaparseable
PR: parseablehq/parseable#1305
File: src/handlers/http/users/dashboards.rs:0-0
Timestamp: 2025-05-01T10:27:56.858Z
Learning: The `add_tile()` function in `src/handlers/http/users/dashboards.rs` should use `get_dashboard_by_user(dashboard_id, &user_id)` instead of `get_dashboard(dashboard_id)` to ensure proper authorization checks when modifying a dashboard.
src/rbac/map.rs (1)
Learnt from: nikhilsinhaparseable
PR: parseablehq/parseable#1340
File: src/query/mod.rs:64-66
Timestamp: 2025-06-18T06:39:04.775Z
Learning: In src/query/mod.rs, QUERY_SESSION_STATE and QUERY_SESSION serve different architectural purposes: QUERY_SESSION_STATE is used for stats calculation and allows dynamic registration of individual parquet files from the staging path (files created every minute), while QUERY_SESSION is used for object store queries with the global schema provider. Session contexts with schema providers don't support registering individual tables/parquets, so both session objects are necessary for their respective use cases.
src/handlers/http/modal/query/querier_rbac.rs (1)
Learnt from: nikhilsinhaparseable
PR: parseablehq/parseable#1305
File: src/handlers/http/users/dashboards.rs:0-0
Timestamp: 2025-05-01T10:27:56.858Z
Learning: The `add_tile()` function in `src/handlers/http/users/dashboards.rs` should use `get_dashboard_by_user(dashboard_id, &user_id)` instead of `get_dashboard(dashboard_id)` to ensure proper authorization checks when modifying a dashboard.
src/handlers/http/rbac.rs (1)
Learnt from: nikhilsinhaparseable
PR: parseablehq/parseable#1305
File: src/handlers/http/users/dashboards.rs:0-0
Timestamp: 2025-05-01T10:27:56.858Z
Learning: The `add_tile()` function in `src/handlers/http/users/dashboards.rs` should use `get_dashboard_by_user(dashboard_id, &user_id)` instead of `get_dashboard(dashboard_id)` to ensure proper authorization checks when modifying a dashboard.
🧬 Code Graph Analysis (3)
src/utils/mod.rs (1)
src/handlers/http/query.rs (1)
  • query (121-171)
src/handlers/http/modal/ingest/ingestor_rbac.rs (4)
src/handlers/http/rbac.rs (3)
  • Users (72-72)
  • add_roles_to_user (314-356)
  • remove_roles_from_user (359-413)
src/rbac/user.rs (1)
  • username (80-88)
src/storage/store_metadata.rs (1)
  • put_staging_metadata (286-301)
src/handlers/http/oidc.rs (1)
  • get_metadata (368-376)
src/rbac/mod.rs (3)
src/rbac/map.rs (8)
  • mut_sessions (97-103)
  • mut_users (65-71)
  • read_user_groups (41-47)
  • roles (73-79)
  • sessions (89-95)
  • users (57-63)
  • from (299-307)
  • from (322-330)
src/rbac/user.rs (5)
  • roles (94-96)
  • username (80-88)
  • add_roles (267-279)
  • remove_roles (300-310)
  • from (188-197)
src/rbac/role.rs (1)
  • from (220-243)
⏰ Context from checks skipped due to timeout of 90000ms (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 aarch64-apple-darwin
  • GitHub Check: Build Default x86_64-unknown-linux-gnu
  • GitHub Check: Build Kafka x86_64-unknown-linux-gnu
  • GitHub Check: Build Default aarch64-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 (27)
src/handlers/http/oidc.rs (1)

336-340: LGTM! Correct pattern matching update for user groups.

The destructuring pattern correctly includes the new user_groups field with an ignore pattern, ensuring the match remains exhaustive while not affecting the existing logic.

src/utils/mod.rs (1)

96-96: LGTM! Correct extraction of tables from visitor.

The .into_inner() call correctly extracts the tables vector from the TableScanVisitor, matching the expected type for user_auth_for_datasets.

src/migration/metadata_migration.rs (1)

164-185: LGTM! Well-structured migration for user groups.

The migration correctly:

  • Updates version from v5 to v6
  • Adds top-level user_groups field if missing
  • Adds user_groups field to each user object if missing
  • Uses appropriate default values (empty arrays)

The defensive checks ensure idempotency and prevent errors.

src/storage/store_metadata.rs (5)

35-35: LGTM! Correct import for user groups support.

Adding UserGroup import is necessary for the new user_groups field in the StorageMetadata struct.


45-45: LGTM! Version bump aligns with migration.

Updating to "v6" is consistent with the new v5_v6 migration function that adds user groups support.


63-63: LGTM! User groups field addition is correct.

The new user_groups field follows the same pattern as the existing users field and supports the user groups feature.


82-82: LGTM! Consistent default initialization.

Initializing user_groups to an empty Vec is consistent with how other collection fields are handled in the Default implementation.


94-94: Nice typo fix!

Correcting "gloabal" to "global" improves code quality.

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

201-219: LGTM! API endpoint refactoring aligns with PR objectives.

The splitting of role management into separate add and remove endpoints is well-implemented:

  • Appropriate use of PATCH method for partial updates
  • Consistent authorization (Action::PutUserRoles) and middleware (DisAllowRootUser)
  • Clear, descriptive handler function names
  • Proper route structure following RESTful conventions

This provides more granular control over role operations compared to the previous monolithic PUT endpoint.

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

91-104: Fix logic error in role usage validation.

The current logic checks if any group contains the role being deleted and returns RoleInUse error, but then continues to collect group names regardless. This creates inconsistent behavior where the function should either fail early or continue processing.

Apply this diff to fix the logic:

-    // also delete from user groups
-    let groups = read_user_groups().keys().cloned().collect_vec();
-    let mut group_names = Vec::new();
-
-    for user_group in groups {
-        if let Some(ug) = read_user_groups().get(&user_group) {
-            if ug.roles.contains(&name) {
-                return Err(RoleError::RoleInUse);
-            }
-            group_names.push(ug.name.clone());
-        } else {
-            continue;
-        };
-    }
+    // check if any user group is using this role
+    let groups = read_user_groups();
+    for group in groups.values() {
+        if group.roles.contains(&name) {
+            return Err(RoleError::RoleInUse);
+        }
+    }

Likely an incorrect or invalid review comment.

src/handlers/http/cluster/mod.rs (1)

163-186: LGTM: Clean implementation of operation-specific role synchronization.

The addition of the operation parameter with proper validation and URL path construction correctly supports the new split role synchronization endpoints. The error handling follows the existing pattern and the validation ensures only valid operations are processed.

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

97-98: LGTM: Correct mutation for metadata migration.

Making parseable_json mutable before passing to the migration function allows for in-place updates, which aligns with the updated migration logic that adds user group support.


211-236: LGTM: Well-structured role endpoint refactoring.

The refactoring from a single PUT endpoint to separate GET/PATCH add/remove endpoints follows REST principles and provides better granular control over role operations. The authorization configuration is appropriate for each endpoint type.

src/rbac/utils.rs (1)

41-78: LGTM: Comprehensive user group role integration.

The enhancement to include user group roles in the UsersPrism response is well-implemented. The code properly:

  • Collects roles from user groups the user belongs to
  • Handles missing group references gracefully
  • Organizes data into logical structures (direct roles vs group roles)
  • Maintains consistency with existing role collection patterns
src/migration/mod.rs (2)

46-49: LGTM!

The parameter change to mutable reference enables in-place metadata updates, allowing the migration results to be propagated back to the caller.


75-79: Well-structured migration flow.

The consistent addition of the v5_v6 migration step across all version paths and the pattern of serializing results back to the mutable reference ensures proper metadata updates.

Also applies to: 87-91, 98-102, 110-115, 118-122

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

117-118: LGTM!

The change correctly propagates the mutable reference requirement from the migration module.


611-636: Role endpoints updated; old PUT handler is commented out
Verified that the previous put_role handler is no longer active and has been commented out in:

  • src/handlers/http/rbac.rs
  • src/handlers/http/modal/query/querier_rbac.rs
  • src/handlers/http/modal/ingest/ingestor_rbac.rs

No further changes are required.

src/rbac/user.rs (3)

28-35: LGTM!

The User struct is properly extended with the user_groups field, and all constructors correctly initialize it as an empty set.

Also applies to: 48-50, 63-64, 76-77, 160-161


200-262: Well-designed UserGroup struct with comprehensive validation.

The validation method properly checks for:

  • Valid group name format using regex
  • Existence of the group (preventing duplicates)
  • Existence of all referenced roles and users
  • Provides detailed error information

300-331: LGTM!

The remove methods correctly use set operations and optimize for no-op cases. The update_in_metadata method properly replaces the group entry.

src/rbac/mod.rs (3)

32-32: LGTM!

The get_user_groups method correctly retrieves user group memberships with proper error handling for missing users.

Also applies to: 57-63


100-113: Clean separation of role operations.

The split into add_roles and remove_roles provides clearer semantics than a single update method. Session invalidation correctly ensures permission changes take immediate effect.


207-224: LGTM!

The UsersPrism struct comprehensively represents users with:

  • Direct roles
  • Group-inherited roles
  • Group memberships

The camelCase serialization maintains API consistency.

src/rbac/map.rs (2)

39-55: LGTM!

The USER_GROUPS map follows the established pattern for global state management with proper initialization and thread-safe accessors.

Also applies to: 111-111, 144-146


310-331: LGTM!

The UserGroups struct implementation is clean and follows the established pattern from the Users struct.

src/rbac/role.rs (1)

23-26: Well-structured RBAC model extensions.

The additions properly extend the RBAC system to support:

  • User group management actions
  • Resource-scoped permissions with optional resource type and ID
  • Consistent builder pattern implementation

The integration with LLM actions and the DefaultPrivilege enum is clean and follows existing patterns.

Also applies to: 85-85, 95-96, 111-115, 167-175, 213-216

Comment on lines +106 to +124
let mut groups_to_update = Vec::new();
for user_group in group_names {
if let Some(ug) = write_user_groups().get_mut(&user_group) {
ug.remove_roles(HashSet::from_iter([name.clone()]))
.map_err(|e| RoleError::Anyhow(anyhow::Error::msg(e.to_string())))?;
groups_to_update.push(ug.clone());
// ug.update_in_metadata().await?;
} else {
continue;
};
}

// update in metadata
metadata
.user_groups
.retain(|x| !groups_to_update.contains(x));
metadata.user_groups.extend(groups_to_update);
put_metadata(&metadata).await?;

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Optimize user group role removal logic.

The current implementation has redundant operations and potential inconsistencies. The group collection and role removal can be simplified and made more robust.

Apply this diff to improve the logic:

-    let mut groups_to_update = Vec::new();
-    for user_group in group_names {
-        if let Some(ug) = write_user_groups().get_mut(&user_group) {
-            ug.remove_roles(HashSet::from_iter([name.clone()]))
-                .map_err(|e| RoleError::Anyhow(anyhow::Error::msg(e.to_string())))?;
-            groups_to_update.push(ug.clone());
-            // ug.update_in_metadata().await?;
-        } else {
-            continue;
-        };
-    }
-
-    // update in metadata
-    metadata
-        .user_groups
-        .retain(|x| !groups_to_update.contains(x));
-    metadata.user_groups.extend(groups_to_update);
+    // remove role from all user groups that have it
+    let mut groups_to_update = Vec::new();
+    for group in write_user_groups().values_mut() {
+        if group.roles.remove(&name) {
+            groups_to_update.push(group.clone());
+        }
+    }
+    
+    // update metadata only if there are changes
+    if !groups_to_update.is_empty() {
+        metadata
+            .user_groups
+            .retain(|x| !groups_to_update.contains(x));
+        metadata.user_groups.extend(groups_to_update);
+    }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/handlers/http/role.rs around lines 106 to 124, the current logic for
removing roles from user groups is redundant and can cause inconsistencies by
cloning and manually managing group updates. Refactor by iterating over
group_names, directly removing roles from the mutable user groups obtained from
write_user_groups(), and then updating metadata.user_groups in a single,
consistent step without cloning. Ensure the removal and metadata update are done
atomically to avoid stale data and simplify the flow.

Comment on lines +140 to 170
pub async fn remove_roles_from_user(
username: web::Path<String>,
roles_to_remove: web::Json<HashSet<String>>,
) -> Result<String, RBACError> {
let username = username.into_inner();
let roles_to_remove = roles_to_remove.into_inner();

if !Users.contains(&username) {
return Err(RBACError::UserDoesNotExist);
};
// update parseable.json first
let mut metadata = get_metadata().await?;
if let Some(user) = metadata
.users
.iter_mut()
.find(|user| user.username() == username)
{
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);
}

let _ = storage::put_staging_metadata(&metadata);
// update in mem table
Users.remove_roles(&username.clone(), roles_to_remove.clone());

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

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add comprehensive validation for role removal.

The remove_roles_from_user handler is missing both role existence validation and checks to ensure the roles are actually assigned to the user before removal.

Add proper validation:

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

     if !Users.contains(&username) {
         return Err(RBACError::UserDoesNotExist);
     };
+    
+    // check if all roles exist
+    let mut non_existent_roles = Vec::new();
+    roles_to_remove.iter().for_each(|r| {
+        if roles().get(r).is_none() {
+            non_existent_roles.push(r.clone());
+        }
+    });
+    
+    if !non_existent_roles.is_empty() {
+        return Err(RBACError::RolesDoNotExist(non_existent_roles));
+    }
+    
+    // check that user actually has these roles
+    let user_roles: HashSet<String> = HashSet::from_iter(Users.get_role(&username));
+    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)));
+    }
     
     // update parseable.json first
🤖 Prompt for AI Agents
In src/handlers/http/modal/ingest/ingestor_rbac.rs between lines 140 and 170,
the remove_roles_from_user function lacks validation to check if the roles to be
removed actually exist and if they are currently assigned to the user. To fix
this, first verify that each role in roles_to_remove exists in the system's role
list, returning an error if any do not exist. Then check that all roles to
remove are currently assigned to the user before proceeding; if any are not
assigned, return an appropriate error. Implement these validations before
modifying the user's roles and updating metadata.

@@ -62,6 +62,7 @@ pub async fn run_metadata_migration(
.and_then(|version| version.as_str())
}

warn!(verion=?get_version(storage_metadata.as_ref().unwrap()));
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix typo in log field name.

-    warn!(verion=?get_version(storage_metadata.as_ref().unwrap()));
+    warn!(version=?get_version(storage_metadata.as_ref().unwrap()));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
warn!(verion=?get_version(storage_metadata.as_ref().unwrap()));
warn!(version=?get_version(storage_metadata.as_ref().unwrap()));
🤖 Prompt for AI Agents
In src/migration/mod.rs at line 65, there is a typo in the log field name
"verion". Correct the typo by changing "verion" to "version" in the warn! macro
call to ensure the log field name is accurate.

src/rbac/user.rs Outdated
Comment on lines 281 to 298
pub async fn add_users(&mut self, users: HashSet<String>) -> Result<(), RBACError> {
// ensure that the users add the user group to their map
let mut metadata = get_metadata().await?;

users
.iter()
.map(|user| {
if let Some(user) = mut_users().get_mut(user) {
user.user_groups.insert(self.name.clone());
metadata.users.retain(|u| u.username() != user.username());
metadata.users.push(user.clone());
}
})
.for_each(drop);
put_metadata(&metadata).await?;
self.users.extend(users);
Ok(())
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider improving error handling for consistency.

The add_users method modifies the global user map before attempting to persist metadata. If put_metadata fails, the in-memory state will be inconsistent with the persisted state.

Consider either:

  1. Clone the users before modification and only update the global map after successful metadata persistence
  2. Add a rollback mechanism if metadata update fails
  3. Document this behavior as a known limitation

Example approach:

pub async fn add_users(&mut self, users: HashSet<String>) -> Result<(), RBACError> {
    let mut metadata = get_metadata().await?;
    
    // Update metadata first
    for user in &users {
        if let Some(user_meta) = metadata.users.iter_mut().find(|u| u.username() == user) {
            // Update in metadata
            // Note: This assumes user_groups field exists in metadata user representation
        }
    }
    
    // Persist metadata
    put_metadata(&metadata).await?;
    
    // Only then update in-memory state
    for user in &users {
        if let Some(user) = mut_users().get_mut(user) {
            user.user_groups.insert(self.name.clone());
        }
    }
    
    self.users.extend(users);
    Ok(())
}
🤖 Prompt for AI Agents
In src/rbac/user.rs around lines 281 to 298, the add_users method updates the
global user map before persisting metadata, risking inconsistency if
put_metadata fails. To fix this, first update the metadata users collection with
the new user group information, then call put_metadata to persist changes. Only
after successful persistence, update the in-memory global user map and
self.users. This ensures in-memory state remains consistent with persisted
metadata or consider adding rollback if persistence fails.

src/rbac/map.rs Outdated
Comment on lines 228 to 254
// if user is a part of any user groups, then add permissions
let perms: HashSet<Permission> =
if !users().0.get(username).unwrap().user_groups.is_empty() {
let groups = users().0.get(username).unwrap().user_groups.clone();
let all_groups_roles = groups
.iter()
.filter(|id| (read_user_groups().0.contains_key(*id)))
.map(|id| read_user_groups().0.get(id).unwrap().roles.clone())
.reduce(|mut acc, e| {
acc.extend(e);
acc
})
.unwrap_or_default();
let mut privilege_list = Vec::new();
all_groups_roles
.iter()
.filter_map(|role| roles().get(role).cloned())
.for_each(|privileges| privilege_list.extend(privileges));

let mut perms = HashSet::from_iter(perms.clone());
for privs in privilege_list {
perms.extend(RoleBuilder::from(&privs).build())
}
perms
} else {
HashSet::from_iter(perms.clone())
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Address potential panic and improve readability.

The permission aggregation logic has issues that need attention:

  1. Potential panic: Line 230 uses unwrap() which could panic if the user is not found
  2. Redundant lookups: The user is looked up multiple times
  3. Complex nesting: The logic is hard to follow

Suggested refactor:

// Extract group permission aggregation to a helper
fn aggregate_group_permissions(username: &str) -> HashSet<Permission> {
    let mut group_perms = HashSet::new();
    
    let Some(user) = users().get(username) else {
        return group_perms;
    };
    
    if user.user_groups.is_empty() {
        return group_perms;
    }
    
    for group_name in &user.user_groups {
        let Some(group) = read_user_groups().get(group_name) else {
            continue;
        };
        
        for role_name in &group.roles {
            let Some(privileges) = roles().get(role_name) else {
                continue;
            };
            
            for privilege in privileges {
                group_perms.extend(RoleBuilder::from(privilege).build());
            }
        }
    }
    
    group_perms
}

// In check_auth:
let mut perms = HashSet::from_iter(perms.clone());
perms.extend(aggregate_group_permissions(username));
🤖 Prompt for AI Agents
In src/rbac/map.rs around lines 228 to 254, the current permission aggregation
code uses unwrap() on user lookups which can panic if the user is missing,
performs redundant user lookups, and has complex nested logic that reduces
readability. Refactor by extracting the group permission aggregation into a
helper function that safely checks for user existence using pattern matching
(e.g., let Some(user) = users().get(username) else { return empty set; }),
iterates over user groups and their roles with safe lookups using pattern
matching to avoid panics, and accumulates permissions into a HashSet. Then, in
the main function, initialize perms from the existing set and extend it with the
helper's result. This improves safety and clarity.

- Added support for user groups
- Migrated `PUT /user/{username}/role` to `PATCH /user/{username}/role/add` and `PATCH /user/{username}/role/remove`
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 (1)
src/rbac/map.rs (1)

228-254: Address potential panic and improve readability.

The permission aggregation logic has issues that need attention:

  1. Potential panic: Line 234 uses unwrap() which could panic if the user group is not found
  2. Redundant lookups: The user group is looked up multiple times
  3. Complex nesting: The logic is hard to follow
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1fe5dfd and b5d1fac.

📒 Files selected for processing (3)
  • src/handlers/http/modal/query/querier_rbac.rs (5 hunks)
  • src/handlers/http/rbac.rs (9 hunks)
  • src/rbac/map.rs (6 hunks)
🧰 Additional context used
🧠 Learnings (3)
src/handlers/http/modal/query/querier_rbac.rs (1)
Learnt from: nikhilsinhaparseable
PR: parseablehq/parseable#1305
File: src/handlers/http/users/dashboards.rs:0-0
Timestamp: 2025-05-01T10:27:56.858Z
Learning: The `add_tile()` function in `src/handlers/http/users/dashboards.rs` should use `get_dashboard_by_user(dashboard_id, &user_id)` instead of `get_dashboard(dashboard_id)` to ensure proper authorization checks when modifying a dashboard.
src/rbac/map.rs (2)
Learnt from: nikhilsinhaparseable
PR: parseablehq/parseable#1305
File: src/handlers/http/users/dashboards.rs:0-0
Timestamp: 2025-05-01T10:27:56.858Z
Learning: The `add_tile()` function in `src/handlers/http/users/dashboards.rs` should use `get_dashboard_by_user(dashboard_id, &user_id)` instead of `get_dashboard(dashboard_id)` to ensure proper authorization checks when modifying a dashboard.
Learnt from: nikhilsinhaparseable
PR: parseablehq/parseable#1340
File: src/query/mod.rs:64-66
Timestamp: 2025-06-18T06:39:04.775Z
Learning: In src/query/mod.rs, QUERY_SESSION_STATE and QUERY_SESSION serve different architectural purposes: QUERY_SESSION_STATE is used for stats calculation and allows dynamic registration of individual parquet files from the staging path (files created every minute), while QUERY_SESSION is used for object store queries with the global schema provider. Session contexts with schema providers don't support registering individual tables/parquets, so both session objects are necessary for their respective use cases.
src/handlers/http/rbac.rs (1)
Learnt from: nikhilsinhaparseable
PR: parseablehq/parseable#1305
File: src/handlers/http/users/dashboards.rs:0-0
Timestamp: 2025-05-01T10:27:56.858Z
Learning: The `add_tile()` function in `src/handlers/http/users/dashboards.rs` should use `get_dashboard_by_user(dashboard_id, &user_id)` instead of `get_dashboard(dashboard_id)` to ensure proper authorization checks when modifying a dashboard.
⏰ Context from checks skipped due to timeout of 90000ms (10)
  • GitHub Check: coverage
  • 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-apple-darwin
  • GitHub Check: Build Kafka aarch64-apple-darwin
  • GitHub Check: Build Default aarch64-unknown-linux-gnu
  • GitHub Check: Build Kafka x86_64-unknown-linux-gnu
  • GitHub Check: Quest Smoke and Load Tests for Standalone deployments
  • GitHub Check: Quest Smoke and Load Tests for Distributed deployments
🔇 Additional comments (11)
src/rbac/map.rs (3)

39-55: LGTM: User groups integration follows established patterns.

The new USER_GROUPS static and accessor functions follow the same safe pattern used for other RBAC maps in this module. The initialization and read/write guards are consistent with existing code.


111-111: LGTM: Proper initialization of user groups from metadata.

The integration of user groups initialization in the init function is well-placed and follows the same pattern as other RBAC components.

Also applies to: 144-146


310-331: LGTM: Clean UserGroups implementation.

The UserGroups struct implementation follows the same pattern as the existing Users struct, providing consistent API design across the RBAC module.

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

62-75: LGTM: Improved role validation prevents invalid user creation.

The addition of role existence validation before user creation is a valuable improvement that prevents creating users with non-existent roles. The error handling appropriately returns a list of invalid roles.


96-100: LGTM: Consistent with new role management API.

The switch from put_role to add_roles_to_user aligns with the new granular role management approach and maintains consistency across the codebase.


160-205: LGTM: Well-implemented granular role management.

Both add_roles_to_user and remove_roles_from_user handlers include comprehensive validation:

  • Role existence validation
  • User existence validation
  • Role assignment validation (for removal)
  • Proper metadata synchronization with ingestors

The separation of add/remove operations provides better granular control and clearer API semantics.

Also applies to: 207-264

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

117-130: LGTM: Consistent role validation across handlers.

The role existence validation in post_user matches the implementation in the querier module, ensuring consistent behavior across the RBAC API.


195-226: Excellent enhancement: Comprehensive role information.

The enhanced get_role handler now provides both direct user roles and inherited group roles in a well-structured RolesResponse. This gives clients complete visibility into a user's effective permissions, which is crucial for RBAC systems.


383-444: LGTM: Comprehensive error handling for user groups.

The new error variants and InvalidUserGroupError struct provide detailed error information that will help clients understand and correct validation issues. The structured approach to error reporting is well-designed.


467-489: LGTM: Enhanced error responses with structured JSON.

The improved error response handling returns structured JSON for role-related errors, making the API more client-friendly and easier to parse programmatically.


491-497: LGTM: Well-designed response structure.

The RolesResponse struct clearly separates direct roles from group roles while maintaining the role-to-privileges mapping, providing comprehensive role information in a logical structure.

Comment on lines 117 to 151
// also delete from user groups
let user_groups = Users.get_user_groups(&username);
let mut groups_to_update = Vec::new();
for user_group in user_groups {
if let Some(ug) = write_user_groups().get_mut(&user_group) {
ug.remove_users(HashSet::from_iter([username.clone()]))?;
groups_to_update.push(ug.clone());
// ug.update_in_metadata().await?;
} else {
continue;
};
}

// ensure that the users remove the user group from their map
[&username]
.iter()
.map(|user| {
if let Some(user) = mut_users().get_mut(*user) {
for group in groups_to_update.iter() {
user.user_groups.remove(&group.name);
}

metadata.users.retain(|u| u.username() != user.username());
metadata.users.push(user.clone());
}
})
.for_each(drop);
put_metadata(&metadata).await?;

// update in metadata user group
metadata
.user_groups
.retain(|x| !groups_to_update.contains(x));
metadata.user_groups.extend(groups_to_update);
put_metadata(&metadata).await?;
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Simplify user group cleanup logic in delete_user.

The user group cleanup logic is overly complex and could be simplified. The current implementation performs multiple metadata updates and has nested logic that's hard to follow.

Consider this simplified approach:

 // also delete from user groups
 let user_groups = Users.get_user_groups(&username);
-let mut groups_to_update = Vec::new();
-for user_group in user_groups {
-    if let Some(ug) = write_user_groups().get_mut(&user_group) {
-        ug.remove_users(HashSet::from_iter([username.clone()]))?;
-        groups_to_update.push(ug.clone());
-        // ug.update_in_metadata().await?;
-    } else {
-        continue;
-    };
-}
-
-// ensure that the users remove the user group from their map
-[&username]
-    .iter()
-    .map(|user| {
-        if let Some(user) = mut_users().get_mut(*user) {
-            for group in groups_to_update.iter() {
-                user.user_groups.remove(&group.name);
-            }
-
-            metadata.users.retain(|u| u.username() != user.username());
-            metadata.users.push(user.clone());
-        }
-    })
-    .for_each(drop);
-put_metadata(&metadata).await?;
-
-// update in metadata user group
-metadata
-    .user_groups
-    .retain(|x| !groups_to_update.contains(x));
-metadata.user_groups.extend(groups_to_update);
+for user_group in user_groups {
+    if let Some(ug) = write_user_groups().get_mut(&user_group) {
+        ug.remove_users(HashSet::from_iter([username.clone()]))?;
+    }
+}
+
+// Update user groups in metadata
+for group in write_user_groups().values() {
+    metadata.user_groups.retain(|ug| ug.name != group.name);
+    metadata.user_groups.push(group.clone());
+}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/handlers/http/modal/query/querier_rbac.rs lines 117 to 151, the user
group cleanup logic in delete_user is overly complex with multiple metadata
updates and nested loops. Simplify by consolidating the removal of the user from
user groups and updating metadata in a single pass. Remove redundant cloning and
repeated calls to put_metadata by batching changes and updating metadata once
after all modifications are done. Refactor the iteration to directly update user
groups and users without unnecessary intermediate collections or nested mapping.

Comment on lines 240 to 274
// also delete from user groups
let user_groups = Users.get_user_groups(&username);
let mut groups_to_update = Vec::new();
for user_group in user_groups {
if let Some(ug) = write_user_groups().get_mut(&user_group) {
ug.remove_users(HashSet::from_iter([username.clone()]))?;
groups_to_update.push(ug.clone());
// ug.update_in_metadata().await?;
} else {
continue;
};
}

// ensure that the users remove the user group from their map
[&username]
.iter()
.map(|user| {
if let Some(user) = mut_users().get_mut(*user) {
for group in groups_to_update.iter() {
user.user_groups.remove(&group.name);
}

metadata.users.retain(|u| u.username() != user.username());
metadata.users.push(user.clone());
}
})
.for_each(drop);
put_metadata(&metadata).await?;

// update in metadata user groups
metadata
.user_groups
.retain(|x| !groups_to_update.contains(x));
metadata.user_groups.extend(groups_to_update);
put_metadata(&metadata).await?;
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Similar complexity issue as in querier module.

The user group cleanup logic here has the same complexity issues as identified in the querier module. Consider applying a similar simplification approach.

Refer to the refactoring suggestion provided for the querier module's delete_user handler.

🤖 Prompt for AI Agents
In src/handlers/http/rbac.rs between lines 240 and 274, the user group cleanup
logic has high complexity due to nested loops and repeated metadata updates.
Simplify by collecting all necessary changes first, then perform a single update
to metadata. Avoid cloning and multiple retains by using more direct data
structure operations, similar to the approach used in the querier module's
delete_user handler refactoring. This will reduce complexity and improve
readability.

- Introduce resource types (stream, llm, all) for privileges
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

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

228-255: Address potential panic and improve readability.

The permission aggregation logic has issues that need attention as previously identified.

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

240-257: Similar complexity issue as in querier module.

The user group cleanup logic here has the same complexity issues as identified in the querier module.

🧹 Nitpick comments (4)
src/rbac/role.rs (2)

93-94: Remove commented-out code.

These commented lines represent the old implementation and should be removed to maintain code clarity:

  • Lines 93-94: Old Permission variants
  • Lines 103-105: Old RoleBuilder fields
  • Lines 121-135: Old RoleBuilder methods
 pub enum Permission {
     Unit(Action),
-    // Stream(Action, String),
-    // StreamWithTag(Action, String, Option<String>),
     Resource(Action, ParseableResourceType),
     SelfUser,
 }

 pub struct RoleBuilder {
     actions: Vec<Action>,
-    // stream: Option<String>,
-    // tag: Option<String>,
-    // resource_id: Option<String>,
     resource_type: Option<ParseableResourceType>,
 }

-    // pub fn with_stream(mut self, stream: String) -> Self {
-    //     self.stream = Some(stream);
-    //     self
-    // }
-
-    // pub fn with_tag(mut self, tag: String) -> Self {
-    //     self.tag = Some(tag);
-    //     self
-    // }
-
-    // pub fn with_resource(mut self, resource_id: String, resource_type: ParseableResourceType) -> Self {
-    //     self.resource_id = Some(resource_id);
-    //     self.resource_type = Some(resource_type);
-    //     self
-    // }

Also applies to: 103-105, 121-135


258-261: Remove more commented-out code.

Additional commented lines from the old implementation should be removed:

         RoleBuilder {
             actions: vec![Action::All],
-            // stream: Some("*".to_string()),
-            // tag: None,
             resource_type: Some(ParseableResourceType::All),
-            // resource_id: Some("*".to_string()),
         }

             ],
-            // stream: Some("*".to_string()),
-            // tag: None,
-            // resource_id: Some("*".to_string()),
             resource_type: Some(ParseableResourceType::All),

             ],
-            // stream: None,
-            // tag: None,
-            // resource_id: None,
             resource_type: None,

             ],
-            // stream: None,
-            // tag: None,
-            // resource_id: None,
             resource_type: None,

             actions: vec![Action::Ingest],
-            // stream: None,
-            // tag: None,
-            // resource_id: None,
             resource_type: None,

Also applies to: 307-310, 350-353, 386-389, 396-399

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

119-127: Use more idiomatic code for role validation.

The current implementation uses .map() with .for_each(drop) which is unconventional. Consider a more idiomatic approach:

-        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);
+        let non_existant_roles: Vec<String> = user_roles
+            .iter()
+            .filter(|r| !roles().contains_key(*r))
+            .cloned()
+            .collect();

277-288: Extract role validation to reduce duplication.

The role existence validation is duplicated between add_roles_to_user and remove_roles_from_user. Consider extracting to a helper function:

fn validate_roles_exist(roles: &HashSet<String>) -> Result<(), RBACError> {
    let non_existant_roles: Vec<String> = roles
        .iter()
        .filter(|r| roles().get(*r).is_none())
        .cloned()
        .collect();
    
    if !non_existant_roles.is_empty() {
        return Err(RBACError::RolesDoNotExist(non_existant_roles));
    }
    Ok(())
}

Then use it in both handlers:

validate_roles_exist(&roles_to_add)?;

Also applies to: 322-333

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b5d1fac and 3e24e08.

📒 Files selected for processing (6)
  • src/handlers/http/modal/query/querier_rbac.rs (5 hunks)
  • src/handlers/http/rbac.rs (9 hunks)
  • src/migration/mod.rs (2 hunks)
  • src/rbac/map.rs (6 hunks)
  • src/rbac/role.rs (10 hunks)
  • src/utils/mod.rs (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/utils/mod.rs
  • src/migration/mod.rs
  • src/handlers/http/modal/query/querier_rbac.rs
🧰 Additional context used
🧠 Learnings (2)
src/rbac/map.rs (2)
Learnt from: nikhilsinhaparseable
PR: parseablehq/parseable#1305
File: src/handlers/http/users/dashboards.rs:0-0
Timestamp: 2025-05-01T10:27:56.858Z
Learning: The `add_tile()` function in `src/handlers/http/users/dashboards.rs` should use `get_dashboard_by_user(dashboard_id, &user_id)` instead of `get_dashboard(dashboard_id)` to ensure proper authorization checks when modifying a dashboard.
Learnt from: nikhilsinhaparseable
PR: parseablehq/parseable#1340
File: src/query/mod.rs:64-66
Timestamp: 2025-06-18T06:39:04.775Z
Learning: In src/query/mod.rs, QUERY_SESSION_STATE and QUERY_SESSION serve different architectural purposes: QUERY_SESSION_STATE is used for stats calculation and allows dynamic registration of individual parquet files from the staging path (files created every minute), while QUERY_SESSION is used for object store queries with the global schema provider. Session contexts with schema providers don't support registering individual tables/parquets, so both session objects are necessary for their respective use cases.
src/handlers/http/rbac.rs (1)
Learnt from: nikhilsinhaparseable
PR: parseablehq/parseable#1305
File: src/handlers/http/users/dashboards.rs:0-0
Timestamp: 2025-05-01T10:27:56.858Z
Learning: The `add_tile()` function in `src/handlers/http/users/dashboards.rs` should use `get_dashboard_by_user(dashboard_id, &user_id)` instead of `get_dashboard(dashboard_id)` to ensure proper authorization checks when modifying a dashboard.
⏰ Context from checks skipped due to timeout of 90000ms (9)
  • 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 Default x86_64-unknown-linux-gnu
  • GitHub Check: Build Default aarch64-apple-darwin
  • 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: Build Kafka aarch64-apple-darwin
🔇 Additional comments (5)
src/rbac/map.rs (3)

40-56: LGTM!

The USER_GROUPS static and accessor functions follow the established patterns in the codebase.


268-269: Clarify the permission logic when context_resource is None.

The comment "WHEN IS THIS VALID??" indicates uncertainty about this code path. When context_resource is None, the code grants permission regardless of the resource_id. This could be a security concern if not intentional.

Please verify:

  1. Under what conditions is context_resource expected to be None?
  2. Is it correct to grant permission when no resource context is provided?
  3. Should this case log a warning or require explicit handling?

319-340: LGTM!

The UserGroups struct implementation follows the established pattern used for Users.

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

195-225: LGTM!

The implementation correctly returns both direct roles and roles inherited from user groups, providing a comprehensive view of user permissions.


452-471: LGTM!

The structured JSON error responses provide clear feedback to API consumers, improving the developer experience.

Comment on lines +195 to +199
| Action::All => Permission::Resource(
action,
self.resource_type.clone().unwrap(),
// self.resource_id.clone().unwrap(),
),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix potential panic from unwrap().

The unwrap() on line 197 will panic if resource_type is None. This is unsafe since not all code paths ensure resource_type is set before calling build().

                 | Action::All => Permission::Resource(
                     action,
-                    self.resource_type.clone().unwrap(),
+                    self.resource_type.clone().unwrap_or(ParseableResourceType::All),
                 ),

Alternatively, consider making resource_type a required parameter for actions that need it, or return a Result from build() to handle the error case properly.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| Action::All => Permission::Resource(
action,
self.resource_type.clone().unwrap(),
// self.resource_id.clone().unwrap(),
),
| Action::All => Permission::Resource(
action,
self.resource_type.clone().unwrap_or(ParseableResourceType::All),
// self.resource_id.clone().unwrap(),
),
🤖 Prompt for AI Agents
In src/rbac/role.rs around lines 195 to 199, the use of unwrap() on
resource_type can cause a panic if resource_type is None. To fix this, avoid
using unwrap() directly; instead, handle the None case safely by either
returning a Result from the build() function to propagate the error or by making
resource_type a required parameter for actions that need it, ensuring it is
always set before calling build().

- roles don't need any migration
- auth flow modified to account for resource type
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.

1 participant