Skip to content

core: group api#1168

Merged
undefined-moe merged 5 commits into
hydro-dev:masterfrom
renbaoshuo:group-api
Jun 13, 2026
Merged

core: group api#1168
undefined-moe merged 5 commits into
hydro-dev:masterfrom
renbaoshuo:group-api

Conversation

@renbaoshuo

@renbaoshuo renbaoshuo commented May 26, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Added group search with case-insensitive matching, optional names filter, per-user listing, and an optional result limit (default 20, max 100).
  • Improvements

    • Filtering moved server-side for faster, more efficient responses.
  • Bug Fixes

    • Removed previously returned spurious extra group entries.

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

DomainApi.groups argument schema now accepts domainId, uid, names, search, and limit. The resolver checks permissions and calls UserModel.listGroup(domainId, uid, names, search, args.limit ?? 20). UserModel.listGroup builds a MongoDB filter from domainId, optionally includes uid(s), name {$in: names}, and a case-insensitive escaped regex for search, applies limit when provided, and returns the matched groups.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • undefined-moe
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'core: group api' directly relates to the main changes, which involve updating the DomainApi.groups query and UserModel.listGroup method to support enhanced group filtering and retrieval functionality.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

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

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.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


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

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/hydrooj/src/handler/domain.ts`:
- Around line 447-455: The permission checks currently use ctx.user but the
resource being queried is args.domainId; update the code so authorization is
performed against the target domain's context before returning groups: obtain
the domain-specific user/authorization context for args.domainId (e.g., call a
helper like ctx.forDomain(args.domainId) / ctx.getDomainUser(args.domainId) or
use an existing domain authorization method), then call hasPerm/hasPriv on that
domain-scoped user instead of ctx.user, and only after that call user.listGroup,
user.searchGroups or user.listGroup with args.domainId; apply the same change to
the other similar block that calls user.listGroup/user.searchGroups to ensure
checks use the target domain context.
🪄 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: f4e0a869-3bfa-4dea-be58-969853e8808d

📥 Commits

Reviewing files that changed from the base of the PR and between cefe2d6 and 2664562.

📒 Files selected for processing (2)
  • packages/hydrooj/src/handler/domain.ts
  • packages/hydrooj/src/model/user.ts

Comment on lines 447 to +455
if (!ctx.user.hasPerm(PERM.PERM_VIEW) && !ctx.user.hasPriv(PRIV.PRIV_VIEW_ALL_DOMAIN)) throw new PermissionError(PERM.PERM_VIEW);
const groups = await user.listGroup(args.domainId);
if (args.names?.length) {
return groups.filter((g) => args.names.includes(g.name));
return user.listGroup(args.domainId, undefined, args.names);
}
if (args.search) {
const searchLower = args.search.toLowerCase();
return groups.filter((g) => g.name.toLowerCase().includes(searchLower));
return user.searchGroups(args.domainId, args.search, args.limit ?? 10);
}
return groups;
return user.listGroup(args.domainId);
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Authorize using the target domain context before returning groups.

Line 447 and Line 463 validate permissions on ctx.user, but the queried resource is args.domainId. This can expose another domain’s groups/user-groups when a caller passes a different domainId.

🔒 Suggested fix
 async (ctx, args) => {
-    if (!ctx.user.hasPerm(PERM.PERM_VIEW) && !ctx.user.hasPriv(PRIV.PRIV_VIEW_ALL_DOMAIN)) throw new PermissionError(PERM.PERM_VIEW);
+    const targetUser = await user.getById(args.domainId, ctx.user._id);
+    if (!targetUser || (!targetUser.hasPerm(PERM.PERM_VIEW) && !targetUser.hasPriv(PRIV.PRIV_VIEW_ALL_DOMAIN))) {
+        throw new PermissionError(PERM.PERM_VIEW);
+    }
     if (args.names?.length) {
         return user.listGroup(args.domainId, undefined, args.names);
     }
@@
 async (ctx, args) => {
-    if (!ctx.user.hasPerm(PERM.PERM_VIEW) && !ctx.user.hasPriv(PRIV.PRIV_VIEW_ALL_DOMAIN)) throw new PermissionError(PERM.PERM_VIEW);
+    const targetUser = await user.getById(args.domainId, ctx.user._id);
+    if (!targetUser || (!targetUser.hasPerm(PERM.PERM_VIEW) && !targetUser.hasPriv(PRIV.PRIV_VIEW_ALL_DOMAIN))) {
+        throw new PermissionError(PERM.PERM_VIEW);
+    }
     return user.listGroup(args.domainId, args.uid);
 },

Also applies to: 463-465

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/hydrooj/src/handler/domain.ts` around lines 447 - 455, The
permission checks currently use ctx.user but the resource being queried is
args.domainId; update the code so authorization is performed against the target
domain's context before returning groups: obtain the domain-specific
user/authorization context for args.domainId (e.g., call a helper like
ctx.forDomain(args.domainId) / ctx.getDomainUser(args.domainId) or use an
existing domain authorization method), then call hasPerm/hasPriv on that
domain-scoped user instead of ctx.user, and only after that call user.listGroup,
user.searchGroups or user.listGroup with args.domainId; apply the same change to
the other similar block that calls user.listGroup/user.searchGroups to ensure
checks use the target domain context.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
packages/hydrooj/src/model/user.ts (1)

490-494: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Apply the active filters and cap to the synthetic uid group too.

The fallback { name: uid.toString() } entry is appended after the Mongo query, so search, names, and limit are not enforced on that item. For example, a filtered search can still return the uid group even when it does not match, and the final array can exceed limit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/hydrooj/src/model/user.ts` around lines 490 - 494, The synthetic
fallback group currently unconditionally pushes an entry (using uid, groups,
ObjectId, domainId, uids, name) after the Mongo query, bypassing the same
search/names filtering and the limit cap; modify the logic so you only add this
synthetic group if it passes the same filters (search/names predicates used for
the query) and only if groups.length is still below the requested limit, i.e.,
evaluate the same match predicate(s) against the synthetic { _id: new
ObjectId(), domainId, uids: [uid], name: uid.toString() } and push it only when
it matches and groups.length < limit.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/hydrooj/src/model/user.ts`:
- Around line 485-486: The code overwrites filter.name when both names and
search are provided; update the logic so both constraints are applied instead of
dropped: if both names and search are present, set filter.$and = [{ name: { $in:
names } }, { name: { $regex: escapeRegExp(search), $options: 'i' } }], otherwise
keep the existing single-condition assignments (the identifiers to change are
filter, names, search, and escapeRegExp).
- Around line 487-488: The current check uses "if (limit)" so limit = 0 is
treated as no limit; update the code that builds the Mongo cursor (the
collGroup.find(filter) and subsequent .limit() call) to treat non-positive
values as invalid by checking if (typeof limit === 'number' && limit > 0) before
calling cursor.limit(limit), or clamp negative/zero to a safe minimum (e.g., 1)
or explicitly throw/bail; ensure the change references the same
variables/functions (collGroup.find, cursor, limit, .limit()) so callers cannot
supply 0 to create an unbounded query.

---

Outside diff comments:
In `@packages/hydrooj/src/model/user.ts`:
- Around line 490-494: The synthetic fallback group currently unconditionally
pushes an entry (using uid, groups, ObjectId, domainId, uids, name) after the
Mongo query, bypassing the same search/names filtering and the limit cap; modify
the logic so you only add this synthetic group if it passes the same filters
(search/names predicates used for the query) and only if groups.length is still
below the requested limit, i.e., evaluate the same match predicate(s) against
the synthetic { _id: new ObjectId(), domainId, uids: [uid], name: uid.toString()
} and push it only when it matches and groups.length < limit.
🪄 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: 2e4d3d6e-8c44-4ba3-a433-93f571493446

📥 Commits

Reviewing files that changed from the base of the PR and between 4be82ba and f019d35.

📒 Files selected for processing (2)
  • packages/hydrooj/src/handler/domain.ts
  • packages/hydrooj/src/model/user.ts

Comment thread packages/hydrooj/src/model/user.ts Outdated
Comment on lines +487 to +488
let cursor = collGroup.find(filter);
if (limit) cursor = cursor.limit(limit);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Treat 0 as invalid instead of "no limit".

This truthy check makes limit = 0 skip .limit() entirely, so a caller can turn the new search path into an unbounded query. Reject or clamp non-positive values before building the cursor.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/hydrooj/src/model/user.ts` around lines 487 - 488, The current check
uses "if (limit)" so limit = 0 is treated as no limit; update the code that
builds the Mongo cursor (the collGroup.find(filter) and subsequent .limit()
call) to treat non-positive values as invalid by checking if (typeof limit ===
'number' && limit > 0) before calling cursor.limit(limit), or clamp
negative/zero to a safe minimum (e.g., 1) or explicitly throw/bail; ensure the
change references the same variables/functions (collGroup.find, cursor, limit,
.limit()) so callers cannot supply 0 to create an unbounded query.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
packages/hydrooj/src/model/user.ts (1)

492-496: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Synthetic group violates limit and bypasses names/search filters.

The synthetic group is appended after the cursor limit is applied and doesn't respect the names or search filters:

  • Limit violation: When the database returns the maximum number of groups (up to limit), appending the synthetic group causes the result to exceed the requested limit. For example, if limit=20 and the DB returns 20 groups, the caller receives 21 groups.
  • Filter bypass: When names or search are provided, the synthetic group (with name: uid.toString()) is appended regardless of whether it matches those filter criteria.
Suggested fix

Option 1: Only append the synthetic group when no filters or limits are provided:

 const groups = await cursor.toArray();
-if (uid) {
+if (uid && !names && !search && !limit) {
     groups.push({
         _id: new ObjectId(), domainId, uids: [uid], name: uid.toString(),
     });
 }

Option 2: Count the synthetic group toward the limit and validate it against filters:

 const groups = await cursor.toArray();
 if (uid) {
+    const syntheticName = uid.toString();
+    const matchesFilters = (!names || names.includes(syntheticName))
+        && (!search || syntheticName.match(new RegExp(escapeRegExp(search), 'i')));
+    const withinLimit = !limit || groups.length < limit;
+    if (matchesFilters && withinLimit) {
-    groups.push({
-        _id: new ObjectId(), domainId, uids: [uid], name: uid.toString(),
-    });
+        groups.push({
+            _id: new ObjectId(), domainId, uids: [uid], name: syntheticName,
+        });
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/hydrooj/src/model/user.ts` around lines 492 - 496, The synthetic
group appended in the block that creates an ObjectId for uid must respect the
original query filters and the requested limit: change the logic around the
groups.push({ _id: new ObjectId(), domainId, uids: [uid], name: uid.toString()
}) so that you only append the synthetic group when it passes the same `names`
and `search` predicates used to query the DB and when doing so will not exceed
the caller's `limit` (i.e., consider the number of groups already returned and
count the synthetic group toward the limit); alternatively apply the filters to
the synthetic group's name first and append it only if (1) it matches and (2)
currentResultCount < limit, ensuring `names`/`search` and `limit` are honored.
♻️ Duplicate comments (1)
packages/hydrooj/src/handler/domain.ts (1)

448-448: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Authorize using the target domain context, not the current user's domain.

The permission check validates ctx.user.hasPerm(PERM.PERM_VIEW), but ctx.user's permissions are scoped to ctx.domain (the current request domain), not args.domainId (the target domain being queried). This allows a user to query groups from any domain they specify, bypassing domain-specific access controls.

🔒 Recommended fix

Obtain the user's permissions in the target domain context before authorizing:

 async (ctx, args) => {
-    if (!ctx.user.hasPerm(PERM.PERM_VIEW) && !ctx.user.hasPriv(PRIV.PRIV_VIEW_ALL_DOMAIN)) throw new PermissionError(PERM.PERM_VIEW);
+    const targetUser = await user.getById(args.domainId, ctx.user._id);
+    if (!targetUser || (!targetUser.hasPerm(PERM.PERM_VIEW) && !targetUser.hasPriv(PRIV.PRIV_VIEW_ALL_DOMAIN))) {
+        throw new PermissionError(PERM.PERM_VIEW);
+    }
     return user.listGroup(args.domainId, args.uid, args.names, args.search, args.limit ?? 20);
 },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/hydrooj/src/handler/domain.ts` at line 448, The current check uses
ctx.user.hasPerm(PERM.PERM_VIEW) which checks permissions in ctx.domain (request
domain) instead of the target args.domainId; change the authorization to
evaluate the user's permissions in the target domain context (args.domainId) and
keep the global privilege check. Concretely, replace the existing check with one
that obtains the user's target-domain principal (e.g., something like const
targetUser = ctx.user.forDomain(args.domainId) or otherwise call
ctx.user.hasPerm(PERM.PERM_VIEW, args.domainId)) and then throw PermissionError
if !targetUser.hasPerm(PERM.PERM_VIEW) &&
!ctx.user.hasPriv(PRIV.PRIV_VIEW_ALL_DOMAIN). Ensure you reference and use
args.domainId and preserve the existing PRIV.PRIV_VIEW_ALL_DOMAIN check.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/hydrooj/src/model/user.ts`:
- Around line 492-496: The synthetic group appended in the block that creates an
ObjectId for uid must respect the original query filters and the requested
limit: change the logic around the groups.push({ _id: new ObjectId(), domainId,
uids: [uid], name: uid.toString() }) so that you only append the synthetic group
when it passes the same `names` and `search` predicates used to query the DB and
when doing so will not exceed the caller's `limit` (i.e., consider the number of
groups already returned and count the synthetic group toward the limit);
alternatively apply the filters to the synthetic group's name first and append
it only if (1) it matches and (2) currentResultCount < limit, ensuring
`names`/`search` and `limit` are honored.

---

Duplicate comments:
In `@packages/hydrooj/src/handler/domain.ts`:
- Line 448: The current check uses ctx.user.hasPerm(PERM.PERM_VIEW) which checks
permissions in ctx.domain (request domain) instead of the target args.domainId;
change the authorization to evaluate the user's permissions in the target domain
context (args.domainId) and keep the global privilege check. Concretely, replace
the existing check with one that obtains the user's target-domain principal
(e.g., something like const targetUser = ctx.user.forDomain(args.domainId) or
otherwise call ctx.user.hasPerm(PERM.PERM_VIEW, args.domainId)) and then throw
PermissionError if !targetUser.hasPerm(PERM.PERM_VIEW) &&
!ctx.user.hasPriv(PRIV.PRIV_VIEW_ALL_DOMAIN). Ensure you reference and use
args.domainId and preserve the existing PRIV.PRIV_VIEW_ALL_DOMAIN check.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7dde73e4-c612-455e-aba7-17a81ee7f518

📥 Commits

Reviewing files that changed from the base of the PR and between 201a8c9 and 87fa87f.

📒 Files selected for processing (2)
  • packages/hydrooj/src/handler/domain.ts
  • packages/hydrooj/src/model/user.ts

@undefined-moe undefined-moe merged commit 81cf241 into hydro-dev:master Jun 13, 2026
6 checks passed
@renbaoshuo renbaoshuo deleted the group-api branch June 15, 2026 05:40
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