Skip to content

feat: Support dynamic updating of users' groupings based on Oauth inf… - #4088

Closed
vinson-zhang wants to merge 3 commits into
QuantumNous:mainfrom
vinson-zhang:feat_OauthAddGroup
Closed

feat: Support dynamic updating of users' groupings based on Oauth inf…#4088
vinson-zhang wants to merge 3 commits into
QuantumNous:mainfrom
vinson-zhang:feat_OauthAddGroup

Conversation

@vinson-zhang

@vinson-zhang vinson-zhang commented Apr 4, 2026

Copy link
Copy Markdown

Summary by CodeRabbit

  • New Features
    • Custom OAuth providers now support automatic user group assignment. Admins can configure a group field path to extract group information from OAuth responses, enabling users to be automatically assigned to matching groups on login or profile sync.

@coderabbitai

coderabbitai Bot commented Apr 4, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

This pull request adds OAuth group field extraction and automatic user group synchronization. It introduces a configurable GroupField path for extracting group/role information from OAuth provider responses, validates groups against system configuration, and synchronizes groups for both new and existing users.

Changes

Cohort / File(s) Summary
Model & Database Schema
model/custom_oauth_provider.go
Added GroupField field to store the JSON path for extracting group information from OAuth user-info responses.
Controller API Layer
controller/custom_oauth.go
Extended request and response structs with GroupField parameter for create and update operations, enabling API-level configuration of group extraction paths.
OAuth User Extraction
oauth/types.go, oauth/generic.go
Added Group field to OAuthUser struct and implemented extraction logic to parse group values from OAuth provider's user-info JSON payload, supporting both array and direct value formats.
Group Synchronization Logic
controller/oauth.go
Implemented conditional group synchronization: for existing users, updates group if it differs from OAuth response and passes ratio_setting validation; for new users, auto-assigns group from OAuth response if configured and valid.
Frontend UI
web/src/components/settings/CustomOAuthSetting.jsx
Added form input field for configuring group_field with guidance text for JSONPath syntax and array handling.
Dependency Update
go.mod
Updated github.com/jackc/pgx/v5 from v5.7.1 to v5.9.1.

Sequence Diagram(s)

sequenceDiagram
    participant OAuthProvider as OAuth Provider
    participant ExtractLogic as Group Extraction
    participant Validation as Ratio Setting
    participant UserDB as User Database
    
    rect rgba(100, 150, 200, 0.5)
    Note over OAuthProvider,UserDB: Existing User Flow
    OAuthProvider->>ExtractLogic: User Info + GroupField path
    ExtractLogic->>ExtractLogic: Extract group from JSON
    ExtractLogic->>Validation: Check if group exists in config
    alt Group valid & differs from current
        Validation->>UserDB: Update user.Group
        UserDB-->>UserDB: Persist changes
    else Group invalid or matches current
        Validation-->>ExtractLogic: Skip update (log reason)
    end
    end
    
    rect rgba(150, 100, 200, 0.5)
    Note over OAuthProvider,UserDB: New User Flow
    OAuthProvider->>ExtractLogic: New User Info + GroupField path
    ExtractLogic->>ExtractLogic: Extract group from JSON
    ExtractLogic->>Validation: Check if group exists in config
    alt Group is valid
        Validation->>UserDB: Create user with assigned group
    else Group not found in config
        Validation->>UserDB: Create user with default group (log warning)
    end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

Suggested reviewers

  • seefs001

Poem

🐰 A rabbit hops through OAuth's gate,
Extracting groups at just the right rate,
With GroupField paths both old and new,
Each user finds where they belong too! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main feature: supporting dynamic updating of users' groupings based on OAuth information, which aligns with changes across controller, model, OAuth, and UI layers.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 (1)
oauth/generic.go (1)

273-274: Consider using logger.LogDebug instead of common.SysLog for consistency.

This changes the log level from DEBUG to INFO (SysLog). Logging successful user info retrieval on every OAuth login may be noisy in production. Additionally, the group value could contain sensitive role/permission information.

♻️ Suggested change to maintain debug-level logging
-	common.SysLog(fmt.Sprintf("[OAuth-Generic-%s] GetUserInfo success: id=%s, username=%s, name=%s, email=%s, group=%s",
-		p.config.Slug, userId, username, displayName, email, group))
+	logger.LogDebug(ctx, "[OAuth-Generic-%s] GetUserInfo success: id=%s, username=%s, name=%s, email=%s, group=%s",
+		p.config.Slug, userId, username, displayName, email, group)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@oauth/generic.go` around lines 273 - 274, Replace the call to common.SysLog
in the OAuth success path with a debug-level logger call (e.g., logger.LogDebug
or the package logger instance used in this file) so the message remains at
DEBUG level; include p.config.Slug, userId, username, displayName and email but
do not log the raw group value (either omit it or redact/mask it) to avoid
verbose/noisy INFO logs and protect sensitive role/permission data—update the
single statement that currently calls common.SysLog(...) to the debug logger and
remove or redact the group variable in the formatted message.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@controller/custom_oauth.go`:
- Line 289: Change the GroupField field from string to *string in the
request/struct where GroupField is defined (to match WellKnown, AccessPolicy
patterns) so you can distinguish "not provided" vs "set to empty"; then update
any handlers and assignment logic that reference GroupField (e.g., where you
currently check if req.GroupField != "" and where you copy to the model) to use
nil checks (if req.GroupField != nil) and allow setting the target to
*req.GroupField (including empty string) to support clearing the existing value
— also update the corresponding JSON unmarshalling usage and any code paths
around GroupField at the other occurrence noted so they use the pointer
semantics.

In `@controller/oauth.go`:
- Around line 213-234: The current flow uses FillUserByProviderID then calls
user.Update(false), which does a full-row overwrite and can clobber concurrent
changes to UsedQuota and RequestCount; instead change the update to a targeted
column update that only modifies the Group field (or add a new method like
UpdateGroup / UpdateColumns that issues an UPDATE users SET group=... WHERE
id=...), and invoke that from the OAuth path where you currently call
user.Update(false) (keep the ratio_setting.ContainsGroupRatio check and logging,
but replace the full-row Update with a column-specific update to avoid
overwriting UsedQuota/RequestCount).
- Line 218: The code incorrectly uses
ratio_setting.ContainsGroupRatio(oauthUser.Group) to decide OAuth group
assignment; replace this with a new dedicated OAuth group validation that reads
from a separate configuration (e.g., oauth_allowed_groups or
OAuthGroupAllowlist) rather than group_ratio_setting. Add a function like
OAuthGroupAllowed(group string) bool (or a method on a new OAuthConfig struct)
and call that where oauthUser.Group is validated, ensure the new config is
loaded/validated at startup, and remove dependence on
ratio_setting.ContainsGroupRatio so pricing tiers do not control OAuth
assignment.

---

Nitpick comments:
In `@oauth/generic.go`:
- Around line 273-274: Replace the call to common.SysLog in the OAuth success
path with a debug-level logger call (e.g., logger.LogDebug or the package logger
instance used in this file) so the message remains at DEBUG level; include
p.config.Slug, userId, username, displayName and email but do not log the raw
group value (either omit it or redact/mask it) to avoid verbose/noisy INFO logs
and protect sensitive role/permission data—update the single statement that
currently calls common.SysLog(...) to the debug logger and remove or redact the
group variable in the formatted message.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9dbeddf0-f5b7-487a-8f17-a99661b43e02

📥 Commits

Reviewing files that changed from the base of the PR and between bb5b9ea and a65baeb.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (7)
  • controller/custom_oauth.go
  • controller/oauth.go
  • go.mod
  • model/custom_oauth_provider.go
  • oauth/generic.go
  • oauth/types.go
  • web/src/components/settings/CustomOAuthSetting.jsx

UsernameField string `json:"username_field"`
DisplayNameField string `json:"display_name_field"`
EmailField string `json:"email_field"`
GroupField string `json:"group_field"`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

GroupField cannot be cleared once set due to non-pointer type.

Unlike other optional fields (WellKnown, AccessPolicy, etc.) which use *string to distinguish "not provided" from "set to empty", GroupField is a plain string. The condition if req.GroupField != "" prevents clearing an existing value.

🔧 Proposed fix to allow clearing GroupField
 type UpdateCustomOAuthProviderRequest struct {
 	...
-	GroupField            string  `json:"group_field"`
+	GroupField            *string `json:"group_field"`
 	...
 }

And update the handler:

-	if req.GroupField != "" {
-		provider.GroupField = req.GroupField
+	if req.GroupField != nil {
+		provider.GroupField = *req.GroupField
 	}

Also applies to: 376-378

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/custom_oauth.go` at line 289, Change the GroupField field from
string to *string in the request/struct where GroupField is defined (to match
WellKnown, AccessPolicy patterns) so you can distinguish "not provided" vs "set
to empty"; then update any handlers and assignment logic that reference
GroupField (e.g., where you currently check if req.GroupField != "" and where
you copy to the model) to use nil checks (if req.GroupField != nil) and allow
setting the target to *req.GroupField (including empty string) to support
clearing the existing value — also update the corresponding JSON unmarshalling
usage and any code paths around GroupField at the other occurrence noted so they
use the pointer semantics.

Comment thread controller/oauth.go
Comment on lines +213 to +234

// Update user's group if OAuth provides a different group and it's in available groups
if oauthUser.Group != "" && oauthUser.Group != user.Group {
common.SysLog(fmt.Sprintf("[OAuth] User %d current group: '%s', OAuth group: '%s'", user.Id, user.Group, oauthUser.Group))
// Check if group exists in group ratio settings
if ratio_setting.ContainsGroupRatio(oauthUser.Group) {
user.Group = oauthUser.Group
if err := user.Update(false); err != nil {
common.SysError(fmt.Sprintf("[OAuth] Failed to update user %d group to '%s': %s", user.Id, oauthUser.Group, err.Error()))
} else {
common.SysLog(fmt.Sprintf("[OAuth] Updated user %d group to '%s' from OAuth provider", user.Id, oauthUser.Group))
}
} else {
common.SysLog(fmt.Sprintf("[OAuth] OAuth group '%s' not in group ratio settings for user %d, keeping current group '%s'", oauthUser.Group, user.Id, user.Group))
}
} else {
if oauthUser.Group == "" {
common.SysLog(fmt.Sprintf("[OAuth] User %d OAuth group is empty, skipping group update", user.Id))
} else if oauthUser.Group == user.Group {
common.SysLog(fmt.Sprintf("[OAuth] User %d group '%s' already matches OAuth group, no update needed", user.Id, user.Group))
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Race condition: user.Update(false) can overwrite concurrent quota changes.

The Update(false) method (see model/user.go:494-510) performs a full-row update using the entire user struct. Between FillUserByProviderID (line 205) and Update (line 220), concurrent requests may have modified UsedQuota or RequestCount. The update will blindly overwrite those changes.

Consider using a targeted column update instead:

🔧 Proposed fix using targeted update
 		if ratio_setting.ContainsGroupRatio(oauthUser.Group) {
-			user.Group = oauthUser.Group
-			if err := user.Update(false); err != nil {
+			if err := model.DB.Model(user).Update("group", oauthUser.Group).Error; err != nil {
 				common.SysError(fmt.Sprintf("[OAuth] Failed to update user %d group to '%s': %s", user.Id, oauthUser.Group, err.Error()))
 			} else {
+				user.Group = oauthUser.Group // Update local copy after successful DB write
 				common.SysLog(fmt.Sprintf("[OAuth] Updated user %d group to '%s' from OAuth provider", user.Id, oauthUser.Group))
 			}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/oauth.go` around lines 213 - 234, The current flow uses
FillUserByProviderID then calls user.Update(false), which does a full-row
overwrite and can clobber concurrent changes to UsedQuota and RequestCount;
instead change the update to a targeted column update that only modifies the
Group field (or add a new method like UpdateGroup / UpdateColumns that issues an
UPDATE users SET group=... WHERE id=...), and invoke that from the OAuth path
where you currently call user.Update(false) (keep the
ratio_setting.ContainsGroupRatio check and logging, but replace the full-row
Update with a column-specific update to avoid overwriting
UsedQuota/RequestCount).

Comment thread controller/oauth.go
if oauthUser.Group != "" && oauthUser.Group != user.Group {
common.SysLog(fmt.Sprintf("[OAuth] User %d current group: '%s', OAuth group: '%s'", user.Id, user.Group, oauthUser.Group))
// Check if group exists in group ratio settings
if ratio_setting.ContainsGroupRatio(oauthUser.Group) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how groups are typically validated elsewhere in the codebase
rg -n --type go -C3 'ContainsGroupRatio|groupRatioMap' --glob '!*_test.go'

Repository: QuantumNous/new-api

Length of output: 4535


🏁 Script executed:

#!/bin/bash
# Find group configuration files and what groups are defined
find . -type f -name "*.go" | xargs grep -l "defaultGroup" | head -10

Repository: QuantumNous/new-api

Length of output: 101


🏁 Script executed:

#!/bin/bash
# Check what groups are defined in the ratio setting
rg -n "defaultGroupRatio|defaultGroupSpecialUsableGroup" --type go -A 10 | head -80

Repository: QuantumNous/new-api

Length of output: 3317


🏁 Script executed:

#!/bin/bash
# Check if there's any OAuth-specific group configuration
rg -n "oauth.*group|OAuth.*[Gg]roup" --type go | grep -i config

Repository: QuantumNous/new-api

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Look for other group validation patterns in the codebase
rg -n "user\.Group\s*=|Group.*=" --type go controller/ | head -20

Repository: QuantumNous/new-api

Length of output: 1408


🏁 Script executed:

#!/bin/bash
# Check the oauth.go file context around lines 215-295 to understand the group assignment flow
sed -n '210,300p' controller/oauth.go

Repository: QuantumNous/new-api

Length of output: 3629


🏁 Script executed:

#!/bin/bash
# Check if there's OAuth provider configuration for allowed groups
rg -n "oauth.*config|oauthUser|GetUserInfoFromOAuth" --type go -B 2 -A 5 | head -100

Repository: QuantumNous/new-api

Length of output: 6025


🏁 Script executed:

#!/bin/bash
# Look for OAuthUser struct definition to understand what Group field represents
rg -n "type.*OAuthUser|oauthUser.*Group" --type go -B 2 -A 8 | head -80

Repository: QuantumNous/new-api

Length of output: 4314


🏁 Script executed:

#!/bin/bash
# Check the full OAuthUser struct definition to see if there's any group-related field
rg -n "type OAuthUser" --type go -A 15 oauth/

Repository: QuantumNous/new-api

Length of output: 944


🏁 Script executed:

#!/bin/bash
# Look for where OAuthUser.Group is populated from OAuth provider
rg -n "\.Group\s*=" --type go oauth/ | head -20

Repository: QuantumNous/new-api

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Check if there's any OAuth provider configuration related to allowed groups
rg -n "config.*[Gg]roup|allowedGroup|groupMapping" --type go -i | head -30

Repository: QuantumNous/new-api

Length of output: 537


🏁 Script executed:

#!/bin/bash
# Check the GenericOAuthProvider config to understand GroupField better
rg -n "type.*Provider|GroupField" --type go oauth/ -B 2 -A 2 | head -50

Repository: QuantumNous/new-api

Length of output: 1828


🏁 Script executed:

#!/bin/bash
# Look for GenericOAuthProvider struct and its config
rg -n "type GenericOAuthProvider|type.*Config" --type go oauth/generic.go -A 20 | head -80

Repository: QuantumNous/new-api

Length of output: 582


🏁 Script executed:

#!/bin/bash
# Check what GroupField is used for in the generic provider
sed -n '240,280p' oauth/generic.go

Repository: QuantumNous/new-api

Length of output: 1670


Replace ratio_setting.ContainsGroupRatio() with dedicated OAuth group validation.

Using ContainsGroupRatio() to validate OAuth-provided groups is semantically incorrect—it checks pricing tier configuration rather than authorization policy. This couples billing configuration to user assignment logic, creating maintenance risks:

  • Adding a pricing tier implicitly makes it OAuth-assignable
  • Removing a tier silently breaks existing OAuth group assignments
  • The validation doesn't reflect its actual purpose

Introduce a dedicated "OAuth allowed groups" configuration (separate from group_ratio_setting) to explicitly control which groups can be auto-assigned via OAuth, independent of pricing tiers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/oauth.go` at line 218, The code incorrectly uses
ratio_setting.ContainsGroupRatio(oauthUser.Group) to decide OAuth group
assignment; replace this with a new dedicated OAuth group validation that reads
from a separate configuration (e.g., oauth_allowed_groups or
OAuthGroupAllowlist) rather than group_ratio_setting. Add a function like
OAuthGroupAllowed(group string) bool (or a method on a new OAuthConfig struct)
and call that where oauthUser.Group is validated, ensure the new config is
loaded/validated at startup, and remove dependence on
ratio_setting.ContainsGroupRatio so pricing tiers do not control OAuth
assignment.

@vinson-zhang
vinson-zhang deleted the feat_OauthAddGroup branch April 21, 2026 01:06
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