Skip to content

Conversation

nikhilsinhaparseable
Copy link
Contributor

@nikhilsinhaparseable nikhilsinhaparseable commented Aug 5, 2025

Summary by CodeRabbit

  • New Features

    • Added a stable user ID cookie after login for more reliable user recognition.
    • Adopted provider “subject” as the primary identity for new users.
    • Improved username derivation from profile (name/email/subject) for consistent display.
  • Bug Fixes

    • Ensures users always receive at least a default role on login.
    • Seamlessly migrates existing accounts to the new identity scheme without user action, preserving access and settings.

Copy link
Contributor

coderabbitai bot commented Aug 5, 2025

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent 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 bf29783 and 133994a.

📒 Files selected for processing (1)
  • src/handlers/http/oidc.rs (8 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/handlers/http/oidc.rs
⏰ 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: Quest Smoke and Load Tests for Distributed deployments
  • GitHub Check: Quest Smoke and Load Tests for Standalone deployments
  • 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 x86_64-unknown-linux-gnu
  • GitHub Check: Build Kafka aarch64-apple-darwin
  • GitHub Check: Build Kafka x86_64-unknown-linux-gnu
  • GitHub Check: coverage
✨ 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 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.

coderabbitai[bot]
coderabbitai bot previously approved these changes Aug 5, 2025
coderabbitai[bot]
coderabbitai bot previously approved these changes Aug 6, 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

🧹 Nitpick comments (3)
src/rbac/user.rs (1)

72-76: Align OAuth userid fallback with email as secondary

To stay consistent with the OIDC login fallback (name → email → sub), consider including email in the userid fallback chain. While sub should be present, this makes the constructor resilient and consistent with reply_login.

-                userid: user_info
-                    .sub
-                    .clone()
-                    .or_else(|| user_info.name.clone())
-                    .unwrap_or(username),
+                userid: user_info
+                    .sub
+                    .clone()
+                    .or_else(|| user_info.name.clone())
+                    .or_else(|| user_info.email.clone())
+                    .unwrap_or(username),
src/handlers/http/oidc.rs (2)

244-264: User lookup: consider preferred_username as an additional fallback

Great migration helper. For completeness, also try preferred_username between name and email.

     if let Some(name) = &user_info.name {
         if let Some(user) = Users.get_user(name) {
             return Some(user);
         }
     }

+    if let Some(preferred) = &user_info.preferred_username {
+        if let Some(user) = Users.get_user(preferred) {
+            return Some(user);
+        }
+    }
+
     if let Some(email) = &user_info.email {
         if let Some(user) = Users.get_user(email) {
             return Some(user);
         }
     }

335-341: Harden user_id cookie: set HttpOnly (and optionally Secure)

Unless the frontend must read the user_id, make it HttpOnly to reduce exposure. Consider Secure in production.

 pub fn cookie_userid(user_id: &str) -> Cookie<'static> {
     Cookie::build(USER_ID_COOKIE_NAME, user_id.to_string())
         .max_age(time::Duration::days(COOKIE_AGE_DAYS as i64))
         .same_site(SameSite::Strict)
+        .http_only(true)
+        // .secure(true) // consider enabling under HTTPS
         .path("/")
         .finish()
 }
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 5699ff9 and df6ef56.

📒 Files selected for processing (5)
  • src/handlers/http/oidc.rs (8 hunks)
  • src/handlers/mod.rs (1 hunks)
  • src/rbac/mod.rs (1 hunks)
  • src/rbac/user.rs (3 hunks)
  • src/rbac/utils.rs (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/handlers/mod.rs
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-05-01T10:27:56.858Z
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.

Applied to files:

  • src/handlers/http/oidc.rs
⏰ 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 Default x86_64-unknown-linux-gnu
  • 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 Distributed deployments
  • GitHub Check: Quest Smoke and Load Tests for Standalone deployments
  • GitHub Check: coverage
🔇 Additional comments (7)
src/rbac/user.rs (2)

174-176: Good addition: capture OIDC subject (sub) in UserInfo

Capturing sub is essential for stable identity. LGTM.


195-202: Populate sub from OIDC userinfo

Mapping sub through Fromopenid::Userinfo is correct. LGTM.

src/rbac/mod.rs (1)

213-217: UsersPrism API adds a new username field

The UsersPrism struct in src/rbac/mod.rs (lines 212–217) now serializes both

  • id (still the JWT sub claim) and
  • a new username field for display.

This alters the JSON shape returned by:

  • to_prism_user in src/rbac/utils.rs
  • the GET /api/v1/user/{username} handler in src/handlers/http/rbac.rs

Please verify all downstream consumers (UI, CLI, other services) that parse this JSON:

  • Use the new username field for display or labels
  • Continue to treat id purely as the subject identifier
src/rbac/utils.rs (1)

75-78: LGTM: propagate new username to UsersPrism

Populating the new field looks correct for both Native and OAuth paths.

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

105-111: LGTM: also set user_id cookie for Basic Auth exchange

Issuing both username and user_id cookies maintains consistency across auth methods.


195-221: Default role handling: ensure at least one role or fail fast

Current logic still allows empty roles if DEFAULT_ROLE is unset. Confirm intended behavior; otherwise, fail the login when no roles resolve.

Proposed guard (optional):

if final_roles.is_empty() {
    // No valid roles and no default configured – reject login explicitly
    return Err(OIDCError::Unauthorized);
}

223-225: LGTM: new users keyed by user_id (sub)

Persisting by stable subject identifier is the right call.

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/handlers/http/oidc.rs (2)

167-176: Avoid panics on missing claims; derive user_id robustly using ID token sub

.expect(...) will panic if a provider omits optional fields in userinfo. Prefer falling back to claims.sub (required in ID Token) and return a controlled error if ever absent.

Apply:

-    let username = user_info
-        .name
-        .clone()
-        .or_else(|| user_info.email.clone())
-        .or_else(|| user_info.sub.clone())
-        .expect("OIDC provider did not return a usable identifier (name, email or sub)");
-    let user_id = user_info
-        .sub
-        .clone()
-        .expect("OIDC provider did not return a usable identifier (sub)");
+    let username = user_info
+        .name
+        .clone()
+        .or_else(|| user_info.email.clone())
+        // fall back to ID Token `sub` (required by spec)
+        .unwrap_or_else(|| claims.sub.clone());
+    let user_id = user_info
+        .sub
+        .clone()
+        .unwrap_or_else(|| claims.sub.clone());

390-416: When migrating to sub, also update group memberships and stale sessions

You migrate the stored user object and persist it, but references in metadata.user_groups still point to the old id; stale sessions may also point to the old id. Update both during migration.

Augment the metadata update:

-    oauth_user.user_info.clone_from(&user_info);
+    oauth_user.user_info.clone_from(&user_info);
     *roles = group;
 
     // Update userid to use sub if available (migration from name-based to sub-based identification)
     if let Some(ref sub) = user_info.sub {
         oauth_user.userid.clone_from(sub);
     }
@@
-    if let Some(entry) = metadata
+    if let Some(entry) = metadata
         .users
         .iter_mut()
         .find(|x| x.username() == old_username)
     {
         entry.clone_from(&user);
+        // Also migrate user references inside user groups
+        for grp in metadata.user_groups.iter_mut() {
+            if grp.users.remove(&old_username) {
+                grp.users.insert(user.username().to_string());
+            }
+        }
         put_metadata(&metadata).await?;
     }

For sessions: if the in-memory session map stores the old id, either:

  • Migrate session owner ids from old_usernameuser.username(), or
  • Invalidate sessions for old_username to avoid orphaned sessions.

If you want, I can draft a small API addition in Users to migrate or purge sessions for a given user id.

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

195-201: Handle poisoned DEFAULT_ROLE lock gracefully

lock().unwrap() can panic if the mutex is poisoned. Fall back safely.

-    let default_role = if let Some(default_role) = DEFAULT_ROLE.lock().unwrap().clone() {
-        HashSet::from([default_role])
-    } else {
-        HashSet::new()
-    };
+    let default_role = match DEFAULT_ROLE.lock() {
+        Ok(guard) => guard
+            .clone()
+            .map(|role| HashSet::from([role]))
+            .unwrap_or_default(),
+        Err(_) => HashSet::new(),
+    };
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between df6ef56 and 5d3c187.

📒 Files selected for processing (5)
  • src/handlers/http/oidc.rs (8 hunks)
  • src/handlers/mod.rs (1 hunks)
  • src/rbac/mod.rs (1 hunks)
  • src/rbac/user.rs (3 hunks)
  • src/rbac/utils.rs (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/handlers/mod.rs
  • src/rbac/mod.rs
  • src/rbac/user.rs
  • src/rbac/utils.rs
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-05-01T10:27:56.858Z
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.

Applied to files:

  • src/handlers/http/oidc.rs
⏰ 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: coverage
  • GitHub Check: Build Default aarch64-apple-darwin
  • GitHub Check: Build Default x86_64-pc-windows-msvc
  • GitHub Check: Quest Smoke and Load Tests for Standalone deployments
  • GitHub Check: Build Default x86_64-apple-darwin
  • GitHub Check: Build Default x86_64-unknown-linux-gnu
  • 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
🔇 Additional comments (7)
src/handlers/http/oidc.rs (7)

105-111: USER_ID cookie for basic auth uses username — confirm client expectations

For native users, setting user_id = username is reasonable; verify the UI treats user_id as “internal identifier” generically (OIDC sub for OAuth, username for native).

Would you like me to scan the UI code for reads of the user-id cookie to confirm no provider-specific assumptions?


201-222: Role resolution logic is sound; default never empty

Merging existing roles with valid OIDC roles and falling back to default avoids empty role sets. LGTM.


236-241: Include USER_ID cookie in post-login redirect

Cookie set sequencing looks correct. LGTM.


244-264: User lookup by sub → name → email is correct and migration-friendly

This prevents duplicate users and enables smooth ID migration. LGTM.


419-424: Lookup by old id during persistence is correct — ensure no duplicate entries

Good that you locate by old_username; confirm there’s no separate entry for the new id to avoid duplicates. If duplicates are possible, dedupe by removing the old entry after cloning the new data in.

I can generate a quick scan to detect duplicate user records by id in metadata during startup if helpful.


35-35: Confirm USER_ID_COOKIE_NAME definition and front-end alignment

  • Verified that USER_ID_COOKIE_NAME is declared in src/handlers/mod.rs (line 41) and correctly imported in src/handlers/http/oidc.rs.
  • Ensure the literal "user_id" matches any client-side (frontend) expectations or documentation so there are no mismatches in cookie handling.

224-225: No other async put_user invocations detected
Verified that the only call to the free put_user(&str, HashSet<String>, UserInfo) function is in this match arm (src/handlers/http/oidc.rs:224). All other put_user occurrences are calls to the Users::put_user(User) method and remain unchanged.

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

♻️ Duplicate comments (3)
src/handlers/http/oidc.rs (3)

322-336: Harden session/username cookies in builders

Session cookie should be secure and http_only. Username cookie should be secure; keep http_only(false) only if UI reads it. See concrete diffs provided in the cookie_userid comment.


167-176: Avoid panics on missing claims; derive username/user_id robustly (fallback to ID Token sub)

Using .expect(...) will crash the handler if an OIDC provider omits fields in userinfo. Prefer falling back to the (required) ID Token sub, and return a controlled error if even that is missing.

Apply this diff to make the derivation robust:

-    let username = user_info
-        .name
-        .clone()
-        .or_else(|| user_info.email.clone())
-        .or_else(|| user_info.sub.clone())
-        .expect("OIDC provider did not return a usable identifier (name, email or sub)");
-    let user_id = user_info
-        .sub
-        .clone()
-        .expect("OIDC provider did not return a usable identifier (sub)");
+    let username = user_info
+        .name
+        .clone()
+        .or_else(|| user_info.email.clone())
+        // fall back to ID Token `sub` (required by spec)
+        .unwrap_or_else(|| claims.sub.clone());
+    let user_id = user_info
+        .sub
+        .clone()
+        // fall back to `claims.sub` if userinfo.sub is absent
+        .unwrap_or_else(|| claims.sub.clone());

Additionally (outside this hunk), verify that when userinfo.sub is present, it matches claims.sub as required by the spec, otherwise reject the login:

// place before: let user_info: user::UserInfo = user_info.into();
if let Some(ref ui_sub) = user_info.sub {
    if ui_sub != &claims.sub {
        return Err(OIDCError::Unauthorized);
    }
}

338-344: Harden user_id cookie: set secure; consider http_only

Mark cookies as secure so they’re only sent over HTTPS. Keep http_only(false) only if the frontend must read this value.

Apply this diff:

 pub fn cookie_userid(user_id: &str) -> Cookie<'static> {
     Cookie::build(USER_ID_COOKIE_NAME, user_id.to_string())
         .max_age(time::Duration::days(COOKIE_AGE_DAYS as i64))
+        .secure(true)
         .same_site(SameSite::Strict)
         .path("/")
         .finish()
 }

Also consider:

  • In cookie_session: add .secure(true).http_only(true)
  • In cookie_username: add .secure(true) (keeping it readable if the UI needs it)

For reference (outside this hunk), suggested builders:

pub fn cookie_session(id: Ulid) -> Cookie<'static> {
    Cookie::build(SESSION_COOKIE_NAME, id.to_string())
        .max_age(time::Duration::days(COOKIE_AGE_DAYS as i64))
        .secure(true)
        .http_only(true)
        .same_site(SameSite::Strict)
        .path("/")
        .finish()
}

pub fn cookie_username(username: &str) -> Cookie<'static> {
    Cookie::build(USER_COOKIE_NAME, username.to_string())
        .max_age(time::Duration::days(COOKIE_AGE_DAYS as i64))
        .secure(true)
        .same_site(SameSite::Strict)
        .path("/")
        .finish()
}
🧹 Nitpick comments (5)
src/handlers/http/oidc.rs (5)

195-200: Default role fallback: OK; minor: avoid locking on each request

The logic to materialize a HashSet from DEFAULT_ROLE is fine. If this path is hot, consider caching the value in an OnceCell<Option<String>> (or similar) to avoid mutex contention. Optional.


247-267: Simplify find_existing_user for compatibility; avoid let-chains

The if let ... && let ... pattern depends on let_chains. For broader MSRV compatibility and readability, use nested if let.

Apply this diff:

-fn find_existing_user(user_info: &user::UserInfo) -> Option<User> {
-    if let Some(sub) = &user_info.sub
-        && let Some(user) = Users.get_user(sub)
-    {
-        return Some(user);
-    }
-
-    if let Some(name) = &user_info.name
-        && let Some(user) = Users.get_user(name)
-    {
-        return Some(user);
-    }
-
-    if let Some(email) = &user_info.email
-        && let Some(user) = Users.get_user(email)
-    {
-        return Some(user);
-    }
-
-    None
-}
+fn find_existing_user(user_info: &user::UserInfo) -> Option<User> {
+    if let Some(sub) = &user_info.sub {
+        if let Some(user) = Users.get_user(sub) {
+            return Some(user);
+        }
+    }
+    if let Some(name) = &user_info.name {
+        if let Some(user) = Users.get_user(name) {
+            return Some(user);
+        }
+    }
+    if let Some(email) = &user_info.email {
+        if let Some(user) = Users.get_user(email) {
+            return Some(user);
+        }
+    }
+    None
+}

481-486: Redirect URL check is very strict

Comparing full “host/” to the redirect (sans scheme) forces redirects only to “/”. If intentional, ignore. If the intention is to allow same-origin paths, consider checking only the host portion.

Would you like me to propose a stricter-but-flexible validator that ensures same-origin while allowing arbitrary paths and query strings?


232-245: Unit coverage: add tests for username/user_id fallback and migration

Consider adding tests that verify:

  • username fallback order: name → email → claims.sub
  • user_id fallback: userinfo.sub → claims.sub
  • migration path updates userid and preserves roles; when groups are present, memberships are migrated.

I can draft these tests against the RBAC/Users harness if you’d like.


355-361: Consider verifying userinfo.sub equals claims.sub

You already decode/validate the ID Token. Add a check that userinfo.sub (if present) matches claims.sub and reject otherwise to prevent token mix-up.

📜 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 4fb1661 and 56de397.

📒 Files selected for processing (5)
  • src/handlers/http/oidc.rs (8 hunks)
  • src/handlers/mod.rs (1 hunks)
  • src/rbac/mod.rs (1 hunks)
  • src/rbac/user.rs (3 hunks)
  • src/rbac/utils.rs (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/handlers/mod.rs
  • src/rbac/mod.rs
  • src/rbac/user.rs
  • src/rbac/utils.rs
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/handlers/http/oidc.rs (1)
src/rbac/user.rs (1)
  • username (84-92)
⏰ 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). (3)
  • 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/handlers/http/oidc.rs (7)

35-35: Import looks correct and aligned with new cookie

Bringing USER_ID_COOKIE_NAME into scope matches the new helper and downstream usage.


105-111: Set user_id cookie for BasicAuth path — OK

Using username as user_id for native users is consistent with the OAuth sub-as-id model. No functional concerns here.


201-222: Role resolution logic is reasonable

  • Existing users: union current roles with valid OIDC groups.
  • New users: use OIDC roles or fall back to default.
  • Ensures non-empty final set.

Looks good.


227-228: Creating users keyed by sub is correct

Switching to user_id (OIDC sub) as the canonical key for new OAuth users is the right move.


146-152: Logout path OK

Conditional provider logout for OAuth users is sound.


97-114: Basic auth flow: password verification and cookie issuance OK

The flow and checks remain correct after adding the new user_id cookie.


481-486: Incorrect review comment – unrelated migration verification requested
The verification script targets user_groups in metadata, but this snippet (is_valid_redirect_url in src/handlers/http/oidc.rs) solely handles URL scheme stripping and comparison. Please remove or ignore the migration-related request here.

Likely an incorrect or invalid review comment.

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

♻️ Duplicate comments (4)
src/rbac/utils.rs (1)

32-44: Make display name fallback to email when name is missing (aligns with login cookie)

Currently, OAuth display_name falls back to the opaque id if name is missing. Prefer name → email → id to match the login flow and improve UX.

-        UserType::OAuth(oauth) => {
-            let username = user.username();
-            let display_name = oauth.user_info.name.as_deref().unwrap_or(username);
+        UserType::OAuth(oauth) => {
+            let username = user.username(); // id (sub)
+            let display_name = oauth
+                .user_info
+                .name
+                .as_deref()
+                .or_else(|| oauth.user_info.email.as_deref())
+                .unwrap_or(username);
             (
                 username,
                 display_name,
                 "oauth",
                 oauth.user_info.email.clone(),
                 oauth.user_info.picture.clone(),
             )
         }
src/handlers/http/oidc.rs (3)

167-176: Avoid panics on missing claims; fall back to ID token sub

Using .expect(...) will crash the handler if userinfo omits fields. Use the ID token’s sub (required) as a fallback and return Unauthorized if still unavailable.

-    let username = user_info
-        .name
-        .clone()
-        .or_else(|| user_info.email.clone())
-        .or_else(|| user_info.sub.clone())
-        .expect("OIDC provider did not return a usable identifier (name, email or sub)");
-    let user_id = user_info
-        .sub
-        .clone()
-        .expect("OIDC provider did not return a usable identifier (sub)");
+    let username = user_info
+        .name
+        .clone()
+        .or_else(|| user_info.email.clone())
+        // fall back to ID Token `sub`
+        .unwrap_or_else(|| claims.sub.clone());
+    let user_id = match user_info.sub.clone().or_else(|| Some(claims.sub.clone())) {
+        Some(id) => id,
+        None => return Err(OIDCError::Unauthorized),
+    };

336-342: Harden user_id cookie: add .secure(true) (and consider HttpOnly based on UI needs)

Mark cookies Secure so they’re sent only over HTTPS. Keep HttpOnly false only if the frontend must read user_id.

 pub fn cookie_userid(user_id: &str) -> Cookie<'static> {
     Cookie::build(USER_ID_COOKIE_NAME, user_id.to_string())
         .max_age(time::Duration::days(COOKIE_AGE_DAYS as i64))
+        .secure(true)
         .same_site(SameSite::Strict)
         .path("/")
         .finish()
 }

Additionally (outside this hunk):

  • In cookie_session(): add .secure(true).http_only(true)
  • In cookie_username(): add .secure(true) (keep .http_only(false) only if the UI reads it)

Example (outside selected lines):

pub fn cookie_session(id: Ulid) -> Cookie<'static> {
    Cookie::build(SESSION_COOKIE_NAME, id.to_string())
        .max_age(time::Duration::days(COOKIE_AGE_DAYS as i64))
        .secure(true)
        .http_only(true)
        .same_site(SameSite::Strict)
        .path("/")
        .finish()
}

pub fn cookie_username(username: &str) -> Cookie<'static> {
    Cookie::build(USER_COOKIE_NAME, username.to_string())
        .max_age(time::Duration::days(COOKIE_AGE_DAYS as i64))
        .secure(true)
        .same_site(SameSite::Strict)
        .path("/")
        .finish()
}

391-429: Complete metadata migration when userid changes; tolerate missing entry; update group memberships

When migrating from name/email → sub, also:

  • Insert a new entry if the user wasn’t present in metadata (edge case).
  • Update any group memberships that reference the old id, otherwise group roles won’t apply.
  • Persist metadata after updating it (move put_metadata after modifications).
-    // Find the user entry using the old username (before migration)
-    if let Some(entry) = metadata
-        .users
-        .iter_mut()
-        .find(|x| x.username() == old_username)
-    {
-        entry.clone_from(&user);
-        put_metadata(&metadata).await?;
-    }
-    Users.delete_user(&old_username);
-    Users.put_user(user.clone());
+    // Find the user entry using the old username (before migration)
+    if let Some(entry) = metadata
+        .users
+        .iter_mut()
+        .find(|x| x.username() == old_username)
+    {
+        entry.clone_from(&user);
+    } else {
+        // User not present in metadata; insert
+        metadata.users.push(user.clone());
+    }
+    // Migrate user references inside user groups (if present)
+    if let Some(user_groups) = metadata.user_groups.as_mut() {
+        for group in user_groups.iter_mut() {
+            if group.users.remove(&old_username) {
+                group.users.insert(user.username().to_string());
+            }
+        }
+    }
+    put_metadata(&metadata).await?;
+    // Refresh in-memory cache and drop old key
+    Users.delete_user(&old_username);
+    Users.put_user(user.clone());

If metadata.user_groups isn’t optional in your model, drop the Option guard accordingly.

🧹 Nitpick comments (3)
src/rbac/mod.rs (1)

213-217: Clarify semantics of id vs username; consider fixing serde rename_all

  • The new username field is good. Please clarify in the comment that id is “sub for OAuth; username for native,” otherwise the “sub” comment will mislead API consumers for native users.
  • Also, note that #[serde(rename = "camelCase")] on the struct doesn’t do what you likely intend. If the goal is camelCase field names, switch to #[serde(rename_all = "camelCase")] at the container level. Not a blocker, but worth correcting.

Apply this minimal doc tweak:

-    // sub
+    // sub (OAuth) or username (native)
src/rbac/utils.rs (1)

75-84: Field naming nit: username in UsersPrism is actually a display name

Given OAuth maps username to a display name (name/email/id), consider renaming the field to display_name on the API in a future breaking change to avoid confusion. Out of scope for this PR, but worth tracking.

I can open a follow-up issue and propose a backwards-compatible transition plan if helpful.

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

235-242: Cookies on redirect: OK; ensure hardened cookie flags in builders

Setting session, username, and user_id cookies here looks correct. See cookie builder hardening suggestion below to add .secure(true) (and review .http_only(...) needs).

📜 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 56de397 and 1e0f70c.

📒 Files selected for processing (5)
  • src/handlers/http/oidc.rs (8 hunks)
  • src/handlers/mod.rs (1 hunks)
  • src/rbac/mod.rs (1 hunks)
  • src/rbac/user.rs (3 hunks)
  • src/rbac/utils.rs (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/handlers/mod.rs
  • src/rbac/user.rs
🧰 Additional context used
🧬 Code Graph Analysis (2)
src/handlers/http/oidc.rs (2)
src/rbac/user.rs (2)
  • username (84-92)
  • from (193-203)
src/rbac/mod.rs (1)
  • put_user (52-55)
src/rbac/utils.rs (1)
src/rbac/user.rs (1)
  • username (84-92)
⏰ 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: coverage
  • GitHub Check: Build Default aarch64-apple-darwin
  • 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 x86_64-pc-windows-msvc
  • 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 (6)
src/handlers/http/oidc.rs (6)

35-35: LGTM: new USER_ID cookie wiring

Import looks correct and isolates the name in a single place.


105-111: LGTM: add user_id cookie for Basic Auth exchange

Mirrors the OAuth path so client code can rely on both cookies consistently.


195-200: LGTM: sensible default role fallback

Good to ensure a non-empty role set with a configurable default when OIDC group mapping yields nothing.


201-221: LGTM: role merge logic for existing/new users

  • Existing users keep roles and gain new valid OIDC roles.
  • New users get valid OIDC roles or fall back to the default.
  • Final guard ensures no-empty roles.

223-226: LGTM: store new OAuth users keyed by user_id (sub)

Switching to sub as the primary key removes ambiguity when name/email change.


245-265: LGTM: robust existing-user lookup order

Lookup by sub first, then name, then email is reasonable and covers migration scenarios.

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 (3)
src/handlers/http/oidc.rs (3)

339-345: Cookie security flags: keep HTTP support, but consider conditional hardening

Good addition of cookie_userid mirroring other cookies. For production, consider .http_only(true) and .secure(true) when served over HTTPS, but keep it off for HTTP to avoid browser rejection. Make this conditional via env/config.

Example approach (outside this hunk):

fn apply_cookie_security(mut c: Cookie<'static>) -> Cookie<'static> {
    if std::env::var("COOKIE_SECURE").as_deref() == Ok("true") {
        c.set_secure(true);
    }
    if std::env::var("COOKIE_HTTP_ONLY").as_deref() != Ok("false") {
        c.set_http_only(true);
    }
    c
}

167-179: Avoid panics; derive username and user_id robustly using ID Token sub as fallback

Using .expect(...) on Line 172 can panic if name, email, and userinfo.sub are all absent. Also, some providers omit sub in userinfo, but ID Token sub is required by the spec. Fall back to claims.sub for both username and user_id to prevent handler crashes and unnecessary Unauthorized errors.

Apply this diff:

-    let username = user_info
-        .name
-        .clone()
-        .or_else(|| user_info.email.clone())
-        .or_else(|| user_info.sub.clone())
-        .expect("OIDC provider did not return a usable identifier (name, email or sub)");
-    let user_id = match user_info.sub.clone() {
-        Some(id) => id,
-        None => {
-            tracing::error!("OIDC provider did not return a sub");
-            return Err(OIDCError::Unauthorized);
-        }
-    };
+    let username = user_info
+        .name
+        .clone()
+        .or_else(|| user_info.email.clone())
+        // Fall back to ID Token `sub` (required by spec)
+        .unwrap_or_else(|| claims.sub.clone());
+    // Prefer userinfo.sub, but fall back to ID Token `sub` to avoid failing when userinfo omits it
+    let user_id = user_info
+        .sub
+        .clone()
+        .unwrap_or_else(|| claims.sub.clone());

423-437: Persist user even if old entry is missing; always apply group migrations and write metadata

If the user isn’t found in metadata (Line 423), we currently skip persistence and group migration, leading to stale/absent metadata and inconsistent state after restart. Insert on miss, always migrate group refs, and write metadata once.

Apply this diff:

-    // Find the user entry using the old username (before migration)
-    if let Some(entry) = metadata
-        .users
-        .iter_mut()
-        .find(|x| x.username() == old_username)
-    {
-        entry.clone_from(&user);
-        // migrate user references inside user groups
-        for group in metadata.user_groups.iter_mut() {
-            if group.users.remove(&old_username) {
-                group.users.insert(user.username().to_string());
-            }
-        }
-        put_metadata(&metadata).await?;
-    }
+    // Update existing entry or insert if missing
+    if let Some(entry) = metadata
+        .users
+        .iter_mut()
+        .find(|x| x.username() == old_username)
+    {
+        entry.clone_from(&user);
+    } else {
+        metadata.users.push(user.clone());
+    }
+    // Migrate user references inside user groups
+    for group in metadata.user_groups.iter_mut() {
+        if group.users.remove(&old_username) {
+            group.users.insert(user.username().to_string());
+        }
+    }
+    put_metadata(&metadata).await?;
🧹 Nitpick comments (2)
src/handlers/http/oidc.rs (2)

226-229: Nit: Parameter naming mismatch in put_user can confuse maintainers

At the call site we pass &user_id (Line 228), but put_user’s parameter is named username. Since OAuth users are keyed by sub, consider renaming the parameter to userid for clarity.

Example (outside this hunk):

pub async fn put_user(
    userid: &str,
    group: HashSet<String>,
    user_info: user::UserInfo,
) -> Result<User, ObjectStorageError> {
    // ... use `userid` consistently
}

240-244: Consider making user_id cookie HttpOnly (conditionally)

You’re now setting cookie_userid(&user_id) (Lines 241–244). If the UI does not need to read user_id from JavaScript, prefer HttpOnly to reduce exposure. Per our prior discussion, only set Secure in HTTPS deployments to avoid breakage on HTTP.

📜 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 1e0f70c and bf29783.

📒 Files selected for processing (1)
  • src/handlers/http/oidc.rs (8 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-19T09:44:26.115Z
Learnt from: nikhilsinhaparseable
PR: parseablehq/parseable#1399
File: src/handlers/http/oidc.rs:336-342
Timestamp: 2025-08-19T09:44:26.115Z
Learning: Secure cookies with .secure(true) cannot be set in HTTP contexts as browsers will reject them. Cookie security attributes should be conditionally applied based on whether the application is running over HTTPS or made configurable for different deployment environments.

Applied to files:

  • src/handlers/http/oidc.rs
🧬 Code Graph Analysis (1)
src/handlers/http/oidc.rs (5)
src/rbac/user.rs (2)
  • username (84-92)
  • from (193-203)
src/handlers/http/rbac.rs (1)
  • from (55-65)
src/rbac/map.rs (2)
  • from (294-302)
  • from (346-354)
src/rbac/mod.rs (1)
  • put_user (52-55)
src/handlers/http/role.rs (2)
  • get_metadata (143-151)
  • put_metadata (153-157)
⏰ 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 Kafka 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 Default aarch64-apple-darwin
  • GitHub Check: Build Default aarch64-unknown-linux-gnu
  • GitHub Check: coverage
  • GitHub Check: Build Kafka aarch64-apple-darwin
  • GitHub Check: Quest Smoke and Load Tests for Distributed deployments
  • GitHub Check: Quest Smoke and Load Tests for Standalone deployments
🔇 Additional comments (6)
src/handlers/http/oidc.rs (6)

105-111: LGTM: Add user_id cookie for BasicAuth flow

Adding cookie_userid(&username) (Line 105) and including it in the redirect (Line 110) keeps the contract consistent across native and OIDC logins. For native users, user_id == username, which is reasonable.


198-203: LGTM: Sensible default role fallback

Computing a default role set (Lines 198–203) and deferring to it when OIDC groups don’t map to roles is a solid defensive default.


204-225: LGTM: Role merge logic for existing/new users

  • Existing user: merge new OIDC-mapped roles into existing set (Lines 205–211).
  • New user: use OIDC-mapped roles or fall back to default (Lines 213–219).
  • Safety net to ensure non-empty roles (Lines 221–224).

This avoids accidentally creating users with no permissions.


394-407: LGTM: Robust migration gating and user update

  • Storing old_username before mutation is necessary for metadata and cache updates (Lines 394–396).
  • needs_userid_migration correctly triggers even when only the sub changed (Lines 401–407).
  • Updating user_info, roles, and migrating userid to sub when present is correct (Lines 413–420).

Also applies to: 413-420


438-440: LGTM: Refresh in-memory cache and drop old key

Deleting the old key (Line 438) and re-inserting the updated user ensures the in-memory map stays coherent post-migration.


35-35: No action needed: USER_ID_COOKIE_NAME is correctly available
The constant USER_ID_COOKIE_NAME is defined in src/handlers/mod.rs (line 41). Because oidc.rs resides in the handlers module’s descendant tree, it can import that private constant directly—no public re-export is required.

@nitisht nitisht merged commit e7d7217 into parseablehq:main Aug 19, 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