Skip to content

Select partition members by user name - #810

Merged
hedhoud merged 8 commits into
developfrom
agent/partition-member-picker
Jul 29, 2026
Merged

Select partition members by user name#810
hedhoud merged 8 commits into
developfrom
agent/partition-member-picker

Conversation

@hedhoud

@hedhoud hedhoud commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Why

Partition access is granted to people, but partition owners usually know a name rather than an internal numeric ID. Requiring raw IDs makes the workflow difficult to verify and increases the risk of granting access to the wrong account.

What changed

The add-member dialog now supports targeted search by display-name prefix and matching ASCII user ID. Results include email when available so duplicate names can be distinguished, selected identities remain visible, and one role can be assigned to multiple people.

Candidate search is paginated and remains stable while memberships change. Concurrent additions cannot overwrite an existing role, and refresh, paging, and per-user failures remain visible and retryable.

Compatibility

Adding a member is now insert-only. Posting an existing member returns 409 and preserves their current role; callers must use the existing role-update endpoint to change it.

Trust model

Search gating and pagination prevent the UI from loading the complete user directory at once. They are load and usability controls, not a privacy boundary. Partition owners are trusted to discover the identities needed to manage access.

Validation

  • Admin UI: 179 tests passed
  • Backend unit suite: 2234 tests passed
  • Production Admin UI build
  • Python and Admin UI lint
  • Alembic migration graph has one head

Closes #777

Related to #757 and #809.

@coderabbitai

coderabbitai Bot commented Jul 27, 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

Walkthrough

Adds a cursor-paginated admin endpoint for non-member partition users, a frontend candidate API, and a searchable multi-user picker integrated with batch membership additions and partial-failure feedback.

Changes

Partition member candidate selection

Layer / File(s) Summary
Candidate service and persistence
openrag/core/ports/partition_membership_repo.py, openrag/services/orchestrators/partition_service.py, openrag/services/persistence/..., openrag/services/persistence/migrations/..., tests/unit/services/..., tests/integration/repos/...
Implements searchable cursor pagination, excludes existing members, adds a display-name index, validates inputs, and preserves existing roles on duplicate additions.
Admin candidate endpoint
openrag/api/routers/admin/partitions.py, tests/unit/api/routers/admin/test_phase14_partition_routes.py
Adds the owner-protected candidates route and tests authorization, required search, pagination, and response behavior.
Candidate API and picker
ui/src/lib/api/partitions.ts, ui/src/lib/api/partitions.test.ts, ui/src/pages/admin/partitions/member-candidate.ts, ui/src/pages/admin/partitions/member-picker.*
Adds typed candidate requests and renders searchable checkbox selection with identity labels, pagination, loading, and error states.
Partition users dialog integration
ui/src/pages/admin/partitions/detail.tsx, ui/src/pages/admin/partitions/member-batch.*
Replaces single-ID entry with debounced multi-user selection, sequential batch additions, retryable partial failures, and dialog state management.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Admin
  participant PartitionDetail
  participant MemberPicker
  participant PartitionAPI
  participant PartitionService
  participant MembershipRepository

  Admin->>PartitionDetail: Open add-users dialog
  PartitionDetail->>MemberPicker: Request candidate page
  MemberPicker->>PartitionAPI: GET users/candidates with search and cursor
  PartitionAPI->>PartitionService: list_member_candidates
  PartitionService->>MembershipRepository: Query non-member candidates
  MembershipRepository-->>PartitionService: Candidate rows
  PartitionService-->>PartitionAPI: Paginated candidate page
  PartitionAPI-->>MemberPicker: Candidate identities
  Admin->>MemberPicker: Select multiple users
  MemberPicker-->>PartitionDetail: Selected candidates
  PartitionDetail->>PartitionAPI: Add selected members
Loading

Suggested labels: admin-ui, breaking-change

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR adds searchable multi-select member selection, shows name plus email to distinguish duplicates, and preserves owner checks and roles.
Out of Scope Changes check ✅ Passed The code changes stay focused on name-based member selection and the supporting API, repo, UI, tests, docs, and search index work.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is related to the main change: adding searchable partition-member selection by name, though it omits the exact ID search and UI updates.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/partition-member-picker

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.

@hedhoud
hedhoud marked this pull request as ready for review July 27, 2026 11:50
@coderabbitai coderabbitai Bot added the admin-ui Admin UI label Jul 27, 2026

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 67a43d1016

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread openrag/services/orchestrators/partition_service.py Outdated

@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

🧹 Nitpick comments (1)
tests/unit/api/routers/admin/test_phase14_partition_routes.py (1)

158-175: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add a non-owner authorization regression test.

This test only exercises an allowed owner. Add a case where require_partition_owner rejects the request and assert the endpoint returns the expected 403 response. As per coding guidelines, partition operations must enforce the appropriate owner dependency.

🤖 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 `@tests/unit/api/routers/admin/test_phase14_partition_routes.py` around lines
158 - 175, Add a separate async regression test near
test_list_partition_member_candidates_returns_stable_identities that configures
require_partition_owner to reject the caller, requests the same candidates
endpoint, and asserts the response status is 403. Verify the owner dependency is
enforced without changing the existing successful-owner test.

Source: Coding guidelines

🤖 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 `@openrag/services/orchestrators/partition_service.py`:
- Around line 680-703: The candidate API must become bounded and paginated
instead of loading every user at once. In
openrag/services/orchestrators/partition_service.py lines 680-703, update
list_member_candidates to accept pagination and search inputs, apply search
while preserving non-member filtering, and return page results with continuation
metadata. In openrag/api/routers/admin/partitions.py lines 408-431, accept and
forward those parameters, return the service’s pagination metadata, and retain
require_partition_owner.

In `@ui/src/pages/admin/partitions/detail.tsx`:
- Around line 489-495: Update the Cancel button’s close action to call
handleDialogOpenChange(false) instead of setDialogOpen(false), ensuring selected
user IDs and role are reset through the shared dialog cleanup handler.

---

Nitpick comments:
In `@tests/unit/api/routers/admin/test_phase14_partition_routes.py`:
- Around line 158-175: Add a separate async regression test near
test_list_partition_member_candidates_returns_stable_identities that configures
require_partition_owner to reject the caller, requests the same candidates
endpoint, and asserts the response status is 403. Verify the owner dependency is
enforced without changing the existing successful-owner test.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: bcbc566a-95d7-44f9-9b3a-429dcbd7640e

📥 Commits

Reviewing files that changed from the base of the PR and between b2ab6b1 and 67a43d1.

📒 Files selected for processing (9)
  • openrag/api/routers/admin/partitions.py
  • openrag/services/orchestrators/partition_service.py
  • tests/unit/api/routers/admin/test_phase14_partition_routes.py
  • tests/unit/services/orchestrators/test_partition_service.py
  • ui/src/lib/api/partitions.test.ts
  • ui/src/lib/api/partitions.ts
  • ui/src/pages/admin/partitions/detail.tsx
  • ui/src/pages/admin/partitions/member-picker.test.tsx
  • ui/src/pages/admin/partitions/member-picker.tsx

Comment thread openrag/services/orchestrators/partition_service.py Outdated
Comment thread ui/src/pages/admin/partitions/detail.tsx

@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.

🧹 Nitpick comments (1)
openrag/services/persistence/partition_membership_repo.py (1)

126-165: 🚀 Performance & Scalability | 🔵 Trivial

Correct implementation; one scaling note for later.

The exclusion/search/pagination logic checks out against the integration test. For future scale: STRPOS(LOWER(...)) prevents index usage, and OFFSET-based pagination degrades at very large offsets/user-table sizes. Not an issue at current scale (admin-scoped picker, capped page size), but worth revisiting with a trigram/GIN index or keyset pagination if the users table grows large.

🤖 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 `@openrag/services/persistence/partition_membership_repo.py` around lines 126 -
165, The implementation is correct; no code changes are required. Retain the
exclusion, search, ordering, and bounded OFFSET pagination in
list_partition_member_candidates, while noting trigram/GIN indexing or keyset
pagination as future scaling options only if user-table size warrants them.
🤖 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.

Nitpick comments:
In `@openrag/services/persistence/partition_membership_repo.py`:
- Around line 126-165: The implementation is correct; no code changes are
required. Retain the exclusion, search, ordering, and bounded OFFSET pagination
in list_partition_member_candidates, while noting trigram/GIN indexing or keyset
pagination as future scaling options only if user-table size warrants them.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: da0c569e-6d6c-4206-b874-9c56069bf480

📥 Commits

Reviewing files that changed from the base of the PR and between 67a43d1 and 6954cd7.

📒 Files selected for processing (12)
  • openrag/api/routers/admin/partitions.py
  • openrag/core/ports/partition_membership_repo.py
  • openrag/services/orchestrators/partition_service.py
  • openrag/services/persistence/partition_membership_repo.py
  • tests/integration/repos/test_partition_membership_repo.py
  • tests/unit/api/routers/admin/test_phase14_partition_routes.py
  • tests/unit/services/orchestrators/test_partition_service.py
  • ui/src/lib/api/partitions.test.ts
  • ui/src/lib/api/partitions.ts
  • ui/src/pages/admin/partitions/detail.tsx
  • ui/src/pages/admin/partitions/member-picker.test.tsx
  • ui/src/pages/admin/partitions/member-picker.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • openrag/api/routers/admin/partitions.py
  • ui/src/lib/api/partitions.test.ts
  • ui/src/pages/admin/partitions/detail.tsx

hedhoud added 2 commits July 27, 2026 15:35
…mber-picker

# Conflicts:
#	openrag/services/orchestrators/partition_service.py
#	tests/unit/services/orchestrators/test_partition_service.py
#	ui/src/pages/admin/partitions/detail.tsx

@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)
ui/src/pages/admin/partitions/detail.tsx (1)

440-481: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add an onError handler to addMutation.

Every other mutation in this file (removeMutation, roleMutation, GeneralTab's mutation) surfaces failures via toast.error(...). addMutation only defines mutationFn/onSuccess. addPartitionMembers (member-batch.ts) swallows per-candidate HTTP errors into failures, so this path should rarely reject — but if it does (e.g. an unexpected exception), the dialog is left silently stuck with no feedback and a re-enabled button, since addFailures was already cleared before mutate().

🛠️ Proposed fix
       addPartitionMembers({
         partitionName,
         candidates: selected,
         role: selectedRole,
       }),
+    onError: (error: Error) => {
+      toast.error(`Failed to add users: ${error.message}`);
+    },
     onSuccess: ({ addedCandidates, failures }) => {
🤖 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 `@ui/src/pages/admin/partitions/detail.tsx` around lines 440 - 481, Add an
onError handler to addMutation that surfaces unexpected addPartitionMembers
rejection errors through toast.error, matching the error handling used by
removeMutation, roleMutation, and GeneralTab's mutation. Preserve the existing
onSuccess behavior and use the caught error's message when available.
🧹 Nitpick comments (2)
openrag/services/persistence/migrations/alembic/versions/e5f6a7b8c9d0_add_user_display_name_prefix_index.py (1)

23-29: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider CREATE INDEX CONCURRENTLY to avoid locking users during upgrade.

A plain CREATE INDEX on users takes a lock that blocks writes for the duration of the build; on a populated table this can stall auth/user-creation flows during deploy. CONCURRENTLY avoids this but must run outside a transaction block (verify the Alembic env supports op.get_context().autocommit_block() or non-transactional migrations here before applying).

♻️ Proposed fix (requires non-transactional migration context)
 def upgrade() -> None:
     if table_exists("users") and not index_exists("users", _INDEX_NAME):
-        op.execute(
-            sa.text(
-                f"CREATE INDEX {_INDEX_NAME} ON users (LOWER(display_name) text_pattern_ops)",
-            ),
-        )
+        with op.get_context().autocommit_block():
+            op.execute(
+                sa.text(
+                    f"CREATE INDEX CONCURRENTLY {_INDEX_NAME} ON users (LOWER(display_name) text_pattern_ops)",
+                ),
+            )
🤖 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
`@openrag/services/persistence/migrations/alembic/versions/e5f6a7b8c9d0_add_user_display_name_prefix_index.py`
around lines 23 - 29, Update upgrade() to create the users display-name index
with CREATE INDEX CONCURRENTLY, using the migration context’s supported
autocommit mechanism such as op.get_context().autocommit_block(). Preserve the
existing table_exists and index_exists guards, and verify the Alembic
environment supports this non-transactional execution.
ui/src/pages/admin/partitions/member-picker.tsx (1)

29-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Unify the candidate fallback-label text between the picker and the failure alert. Both sites format "no display name" candidates independently and now disagree on the fallback text (User #<id> vs Unnamed user), which can show two different labels for the same user in one dialog.

  • ui/src/pages/admin/partitions/member-picker.tsx#L29-L31: export candidateLabel so it can be reused elsewhere instead of being re-implemented.
  • ui/src/pages/admin/partitions/detail.tsx#L664-L680: import and use the exported candidateLabel instead of the inline display_name?.trim() || "Unnamed user" fallback.
🤖 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 `@ui/src/pages/admin/partitions/member-picker.tsx` around lines 29 - 31, Unify
candidate fallback labels by exporting candidateLabel from
ui/src/pages/admin/partitions/member-picker.tsx (lines 29-31). In
ui/src/pages/admin/partitions/detail.tsx (lines 664-680), import and use
candidateLabel instead of the inline display_name fallback; no other changes are
needed.
🤖 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 `@ui/src/pages/admin/partitions/detail.tsx`:
- Around line 440-481: Add an onError handler to addMutation that surfaces
unexpected addPartitionMembers rejection errors through toast.error, matching
the error handling used by removeMutation, roleMutation, and GeneralTab's
mutation. Preserve the existing onSuccess behavior and use the caught error's
message when available.

---

Nitpick comments:
In
`@openrag/services/persistence/migrations/alembic/versions/e5f6a7b8c9d0_add_user_display_name_prefix_index.py`:
- Around line 23-29: Update upgrade() to create the users display-name index
with CREATE INDEX CONCURRENTLY, using the migration context’s supported
autocommit mechanism such as op.get_context().autocommit_block(). Preserve the
existing table_exists and index_exists guards, and verify the Alembic
environment supports this non-transactional execution.

In `@ui/src/pages/admin/partitions/member-picker.tsx`:
- Around line 29-31: Unify candidate fallback labels by exporting candidateLabel
from ui/src/pages/admin/partitions/member-picker.tsx (lines 29-31). In
ui/src/pages/admin/partitions/detail.tsx (lines 664-680), import and use
candidateLabel instead of the inline display_name fallback; no other changes are
needed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f9460fdf-6c3e-41d4-8083-ff9c01b15386

📥 Commits

Reviewing files that changed from the base of the PR and between 6954cd7 and a4e93f3.

📒 Files selected for processing (18)
  • openrag/api/routers/admin/partitions.py
  • openrag/core/ports/partition_membership_repo.py
  • openrag/services/orchestrators/partition_service.py
  • openrag/services/persistence/migrations/alembic/versions/e5f6a7b8c9d0_add_user_display_name_prefix_index.py
  • openrag/services/persistence/partition_membership_repo.py
  • openrag/services/persistence/schema.py
  • tests/integration/repos/test_partition_membership_repo.py
  • tests/unit/api/routers/admin/test_phase14_partition_routes.py
  • tests/unit/services/orchestrators/test_partition_service.py
  • tests/unit/services/persistence/test_add_partition_member.py
  • tests/unit/services/persistence/test_partition_member_candidates.py
  • ui/src/lib/api/partitions.test.ts
  • ui/src/lib/api/partitions.ts
  • ui/src/pages/admin/partitions/detail.tsx
  • ui/src/pages/admin/partitions/member-batch.test.ts
  • ui/src/pages/admin/partitions/member-batch.ts
  • ui/src/pages/admin/partitions/member-picker.test.tsx
  • ui/src/pages/admin/partitions/member-picker.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/unit/api/routers/admin/test_phase14_partition_routes.py

@andyne13 andyne13 self-assigned this Jul 28, 2026
@andyne13

andyne13 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review — verified against a5b5c55d

Reviewed the delta against the merge-base db3482c3 (i.e. excluding #809, which is already on develop). I stood up a throwaway PostgreSQL 16 with 200k users to check the query plans rather than reason about them, and compiled the declarative index DDL to compare it against the migration.

Verdict: the backend design is solid and several things I expected to be wrong turned out to be right. The blocking concerns are a breaking API change that isn't documented, a front-end/back-end validation mismatch, and an overclaimed security property.


Verified correct

I want to be explicit about what I checked and found sound, because most of it is the hard part:

The index actually works. I assumed LIKE LOWER($3) || '%' would defeat the prefix optimisation (the pattern isn't a Const, and generic plans can't fold Params). It does not — PG16 produces a genuine prefix range scan in both plan modes:

Bitmap Index Scan on ix_users_lower_display_name_pattern
  Index Cond: ((lower(display_name) ~>=~ 'alice') AND (lower(display_name) ~<~ 'alicf'))

Identical under force_custom_plan and force_generic_plan, so it survives asyncpg's prepared-statement caching. text_pattern_ops is the right call under a non-C collation (en_US.utf8 here). I also probed the pathological case — a 3-char prefix matching ~200k rows, with a deep cursor — and the planner switches to a users_pkey scan with a filter; sub-millisecond execution, no seq scan, no sort blow-up. No performance finding.

Migration hygiene is right. metadata.create_all() runs at startup, so I compiled the new declarative Index:

CREATE INDEX ix_users_lower_display_name_pattern ON users (lower(display_name) text_pattern_ops)

— byte-for-byte the same shape as the migration, and the index_exists() guard makes re-application a no-op, per the idempotency rule. Walking every down_revision in the tree confirms a single head (e5f6a7b8c9d0d4e5f6a7b8c9, no sibling).

The conflict handling is genuinely atomic. ON CONFLICT ... DO NOTHING + inspecting the row count is decided by the database, not by a read-then-write in the service, so two concurrent adds of the same user cannot both win and cannot silently overwrite a role. That is the correct shape and the integration test pins the role-preservation behaviour.

Keyset pagination is correctu.id > $4 / ORDER BY u.id / no OFFSET, so pages stay stable while memberships change underneath. The LIKE escaping (backslash first, then % and _, with ESCAPE '\') is in the right order and the unit test asserts it. No route-ordering conflict either: there is no GET /{partition}/users/{user_id} for candidates to shadow.


🔴 Breaking API change, not reflected in the public docs

add_partition_member changes from DO UPDATE SET role = EXCLUDED.role to DO NOTHING, so POST /{partition}/users goes from upsert to insert-only + 409. Previously, posting for an existing member changed their role and returned 201; now it fails and preserves the old role.

The route docstring is updated, but docs/content/docs/documentation/API.mdx:386 still reads:

/partition/{partition}/users | POST | Add a member — form fields user_id (int), role (default viewer) → 201

Any caller currently using POST as set-or-update — scripts, provisioning, IdP sync — stops working. It does surface as a 409, so a client that checks status codes sees an error; one that doesn't just observes that the role never changed. This needs the API docs updated and a release note. I'd also mention the PATCH endpoint explicitly in the 409 body so callers know the migration path.

I checked: add_partition_member has exactly one caller (PartitionService.add_member), so nothing inside the codebase depends on the old upsert.

🟠 The front-end and back-end disagree on "is this a user ID?"

Front-end (detail.tsx): /^\d+$/ — ASCII digits only.
Back-end (partition_service.py): normalized_search.isdecimal()any Unicode decimal — then int().

Verified:

'١٢٣'.isdecimal()   # True
int('١٢٣')          # 123
# JS /^\d+$/ rejects it

So typing ١٢٣ (Arabic-Indic digits): the UI classifies it as a 3-character name prefix and renders it as a name search, while the back-end performs an exact lookup of user ID 123 and returns an unrelated account. Silent wrong-account risk — which is precisely what this PR sets out to reduce.

Suggested fix, keeping the two sides aligned:

if normalized_search.isascii() and normalized_search.isdecimal():

Related: because any all-digit search routes to the ID branch, a user whose display_name is numeric can never be found by name. Worth a deliberate decision rather than a side effect.

🟠 "No longer exposes an unfiltered user directory" doesn't hold

The PR body claims candidate discovery no longer exposes the directory. The 3-character minimum and the membership exclusion are real, but:

  • the exact-ID branch returns any user by ID, and users.id is a sequential serial;
  • nothing throttles a successfully authenticated caller. The only limiter in the stack is AuthFailureRateLimiter (api/middleware/auth.py:114), and its counter is incremented solely by record_failure, which is called only from _auth_failure (:193) — so an owner enumerating with a valid token never accrues a hit and is never limited;
  • the prefix floor is trivially walked (aaa, aab, …).

So any partition owner can enumerate the full directory — display_name included — in N requests with a five-line script. The control is friction, not a boundary.

That may well be acceptable if partition owners are considered trusted to see who exists. But then the claim in the description is the thing to fix, and the constraint mostly costs UX. Worth stating the intended threat model explicitly, because right now the code implies a boundary it doesn't enforce.

🟡 Candidates omit email, which undercuts the PR's own goal

The stated motivation is that owners "know a name rather than an internal numeric ID" and that raw IDs increase "the risk of granting access to the wrong account". But the candidate payload is only {user_id, display_name}, and member-picker.tsx disambiguates duplicates with #{user_id} — the very identifier the PR is trying to get away from. Two people called "Jean Dupont" are still indistinguishable to an owner who doesn't know their internal IDs.

This is also asymmetric with #809, which added email to the member list: you can see an email after adding someone, but not at the moment you decide. Either include email in candidates or note why it's deliberately withheld.

🟡 CREATE INDEX CONCURRENTLY failure is silently unrecoverable

A failed concurrent build leaves an invalid index behind. index_exists() (schema_helpers.py:22) matches on name only, and SQLAlchemy's PG reflection does not filter pg_index.indisvalid (it's declared in pg_catalog.py:183 but unused in the index query). So on re-run the guard sees the name, skips, and reports success — leaving an unusable index and a search path that silently degrades with no error anywhere.

Worth checking validity rather than existence, and dropping/rebuilding when invalid:

SELECT 1 FROM pg_class c JOIN pg_index i ON i.indexrelid = c.oid
WHERE c.relname = 'ix_users_lower_display_name_pattern' AND i.indisvalid

🟡 Status-string parsing in the repository

return result.endswith(" 1")

This works (INSERT 0 1 vs INSERT 0 0), but it depends on an undocumented asyncpg command-tag format to carry a correctness-critical signal — whether a membership was created. RETURNING says the same thing structurally:

created = await conn.fetchval(
    """INSERT INTO partition_memberships (partition_name, user_id, role, added_at)
       VALUES ($1, $2, $3, NOW())
       ON CONFLICT (partition_name, user_id) DO NOTHING
       RETURNING 1""",
    partition, user_id, role,
)
...
return created is not None

🟡 A batch UX over a single-item API

member-batch.ts awaits one addPartitionMember per candidate in a sequential for loop. Fifty selected users is fifty serialized round trips, and the operation is non-atomic by construction — partial success is the normal case, not an edge case. The failure reporting handles it well and the 401/403 short-circuit is a nice touch, but it's worth acknowledging this is a client-side loop rather than a batch operation, and considering a real batch endpoint (or at least bounded concurrency) before the selection limit grows.


Nits

  • MemberAddFailure.attempted is set and tested but never read by any component — the attempted/not-attempted distinction reaches the user only through the message string. Either render it or drop the field.
  • Validation of the same rules lives in three places: the route (max_length, ge/le), the service (limit/cursor bounds, the 3-char rule), and the UI regex. The route has no min_length, so ?search= reaches the service to be rejected there. Harmless, but the ID/prefix rule in particular now has to be kept in sync across three files — and per the finding above, it already drifted.
  • _MAX_POSTGRES_INTEGER is defined in the service and repeated as the literal 2_147_483_647 in the route's Query(le=...).

Happy to re-review once the API-doc/breaking-change note and the isdecimal mismatch are addressed. The data-access layer here is the strongest part of the change — the index, the keyset pagination, and the atomic conflict handling all hold up under scrutiny.


Edited after posting, for accuracy — no findings added or removed, both conclusions unchanged:

  • the rate-limiting bullet: an auth-failure limiter does exist (AuthFailureRateLimiter); it simply never throttles a successfully authenticated caller. The original wording said there was none.
  • the breaking-change paragraph: the failure does surface as a 409, so "silently stops working" was imprecise.

@coderabbitai coderabbitai Bot added the breaking-change Change of behavior after upgrade label Jul 29, 2026

@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)
tests/unit/services/persistence/test_display_name_index_migration.py (1)

1-83: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the Ruff formatting failure.

ruff format --check currently fails for this file, so the lint pipeline cannot pass. Run ruff format tests/unit/services/persistence/test_display_name_index_migration.py and commit the result.

🤖 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 `@tests/unit/services/persistence/test_display_name_index_migration.py` around
lines 1 - 83, Format the test file with Ruff so it passes `ruff format --check`;
run Ruff formatting on the file containing the `migration` fixture and upgrade
tests, then commit the resulting formatting-only changes.

Source: Pipeline failures

🧹 Nitpick comments (1)
tests/unit/services/persistence/test_display_name_index_migration.py (1)

48-58: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the concurrent-index autocommit boundary.

The fake context records "autocommit", but neither CREATE INDEX test verifies it. A regression that moves CREATE INDEX CONCURRENTLY outside op.get_context().autocommit_block() would still pass these tests despite violating the migration contract.

Suggested assertions
     assert "CREATE INDEX CONCURRENTLY ix_users_lower_display_name_pattern" in statements
+    assert "autocommit" in fake_op.calls

Add the equivalent assertion to the missing-index test.

Also applies to: 72-82

🤖 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 `@tests/unit/services/persistence/test_display_name_index_migration.py` around
lines 48 - 58, Add an assertion in the missing-index test, alongside the
existing CREATE INDEX CONCURRENTLY statement check, verifying that the fake
operation recorded the "autocommit" boundary. Ensure the invalid-index rebuild
test also confirms CREATE INDEX CONCURRENTLY executes within
op.get_context().autocommit_block(), preserving the migration contract.
🤖 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 `@tests/unit/services/persistence/test_display_name_index_migration.py`:
- Around line 1-83: Format the test file with Ruff so it passes `ruff format
--check`; run Ruff formatting on the file containing the `migration` fixture and
upgrade tests, then commit the resulting formatting-only changes.

---

Nitpick comments:
In `@tests/unit/services/persistence/test_display_name_index_migration.py`:
- Around line 48-58: Add an assertion in the missing-index test, alongside the
existing CREATE INDEX CONCURRENTLY statement check, verifying that the fake
operation recorded the "autocommit" boundary. Ensure the invalid-index rebuild
test also confirms CREATE INDEX CONCURRENTLY executes within
op.get_context().autocommit_block(), preserving the migration contract.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c3b74b42-8c03-49dd-8833-e50bdeedc6be

📥 Commits

Reviewing files that changed from the base of the PR and between a5b5c55 and e2a140a.

📒 Files selected for processing (18)
  • docs/content/docs/documentation/API.mdx
  • openrag/api/routers/admin/partitions.py
  • openrag/services/orchestrators/partition_service.py
  • openrag/services/persistence/migrations/alembic/versions/e5f6a7b8c9d0_add_user_display_name_prefix_index.py
  • openrag/services/persistence/partition_membership_repo.py
  • tests/integration/repos/test_partition_membership_repo.py
  • tests/unit/api/routers/admin/test_phase14_partition_routes.py
  • tests/unit/services/orchestrators/test_partition_service.py
  • tests/unit/services/persistence/test_add_partition_member.py
  • tests/unit/services/persistence/test_display_name_index_migration.py
  • ui/src/lib/api/partitions.test.ts
  • ui/src/lib/api/partitions.ts
  • ui/src/pages/admin/partitions/detail.tsx
  • ui/src/pages/admin/partitions/member-batch.test.ts
  • ui/src/pages/admin/partitions/member-batch.ts
  • ui/src/pages/admin/partitions/member-candidate.ts
  • ui/src/pages/admin/partitions/member-picker.test.tsx
  • ui/src/pages/admin/partitions/member-picker.tsx
🚧 Files skipped from review as they are similar to previous changes (15)
  • ui/src/pages/admin/partitions/member-candidate.ts
  • ui/src/pages/admin/partitions/member-batch.test.ts
  • ui/src/pages/admin/partitions/member-batch.ts
  • openrag/api/routers/admin/partitions.py
  • tests/unit/api/routers/admin/test_phase14_partition_routes.py
  • tests/integration/repos/test_partition_membership_repo.py
  • ui/src/pages/admin/partitions/member-picker.tsx
  • tests/unit/services/persistence/test_add_partition_member.py
  • ui/src/pages/admin/partitions/detail.tsx
  • openrag/services/persistence/partition_membership_repo.py
  • openrag/services/persistence/migrations/alembic/versions/e5f6a7b8c9d0_add_user_display_name_prefix_index.py
  • tests/unit/services/orchestrators/test_partition_service.py
  • openrag/services/orchestrators/partition_service.py
  • ui/src/lib/api/partitions.test.ts
  • ui/src/pages/admin/partitions/member-picker.test.tsx

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d7df8923dd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread openrag/services/orchestrators/partition_service.py Outdated
@hedhoud hedhoud added this to the v2.0.2 milestone Jul 29, 2026

@aditykris aditykris left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No issues found,

@andyne13 andyne13 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.

Approving — every review finding verified as addressed on 9774c083

  • Breaking changeAPI.mdx now documents the 409 and the "use PATCH to change a role" note; the error message names the endpoint.
  • ID vs prefix mismatchisascii() and isdecimal(). I re-ran the full matrix: UI and backend now agree on every shape, including ١٢٣ (prefix search, no longer an exact lookup of user 123). Numeric display names became searchable as a bonus.
  • Directory exposure — resolved as an explicit decision rather than code: the new "Trust model" section states the gating is a load/usability control, not a privacy boundary. That's the honest framing.
  • Candidate identityemail is selected and rendered through candidateSecondaryLabel() with an ID fallback, so duplicate names are distinguishable without requiring email to exist.
  • Invalid index_index_validity() drops and rebuilds. Verified against a real PostgreSQL 16, not just the mock test: fresh → valid; re-run → no rebuild; forced indisvalid=false → dropped, rebuilt, valid; and the rebuilt index is used by the candidate query at 200k rows.
  • Status-string parsing — replaced with RETURNING 1 / created is not None.
  • attempted — removed.

Gates run locally: 170 backend unit tests, 179 UI tests, ruff check, ruff format --check, and the layer-import guard — all green.

Two things left that I'm happy to see ship as-is: the batch add is still a sequential per-user loop, and 2_147_483_647 remains a literal in the route alongside _MAX_POSTGRES_INTEGER.

LGTM.

@hedhoud
hedhoud merged commit ddac1fc into develop Jul 29, 2026
6 checks passed
@hedhoud
hedhoud deleted the agent/partition-member-picker branch July 29, 2026 14:24
@andyne13 andyne13 mentioned this pull request Jul 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

admin-ui Admin UI breaking-change Change of behavior after upgrade

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Admin UI: select partition members by user name

3 participants