Conversation
Add GET /api/users/search to replace pre-loaded available_members list. Prevents information disclosure of the full user directory to project/component admins. - New Api::UserSearchController with admin-only authorization - Project#search_available_members combines exclusion + ILIKE search - Minimum 2-char query, configurable limit (default 10, max 25) - Returns only id, name, email via UserBlueprint pattern - 10 request specs (TDD: auth, search, exclusion, limits, 404) Signed-off-by: Will Dower <will@dower.dev>
- Remove available_members from project and component blueprints (no longer serialized to client) - NewMembership.vue: async server-side search via /api/users/search with debounced vue-multiselect (replaces client-side filtering) - MembershipsTable.vue: derive pending access request users from access_requests data directly (no longer cross-references available_members) - ProjectMembersModal.vue: remove available_members prop pass-through - Update test fixtures for nested user data in access requests Signed-off-by: Will Dower <will@dower.dev>
- Replace b-form-select with vue-multiselect + debounced server-side search in MembersModal (component member addition) - Remove availableMemberOptions computed (was pre-loaded user list) - Add api/user_search#index to auth coverage AUTHENTICATE_ONLY_ACTIONS - Update MembersModal spec for new search state management Signed-off-by: Will Dower <will@dower.dev>
- Add scope=members parameter to /api/users/search for searching within existing members (PoC selection, any member can use) - Add Project#search_members method - Remove all_users from ComponentBlueprint and jbuilder - Update UpdateComponentDetailsModal PoC dropdown to async search via /api/users/search?scope=members - Relaxed auth for member-scoped search (any member, not just admin) Signed-off-by: Will Dower <will@dower.dev>
There was a problem hiding this comment.
Pull request overview
Adds a server-side user search endpoint for membership-related UI flows and removes preloading of large user lists to reduce information disclosure and DOM bloat.
Changes:
- Introduces
GET /api/users/searchwith authorization rules and query/limit handling. - Updates membership/PoC selection UIs to use remote search via
vue-multiselect+ debounced API calls. - Removes
available_members/all_usersfrom serialized payloads and updates related tests.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| spec/requests/authorization_coverage_spec.rb | Documents the new API endpoint’s authorization coverage. |
| spec/requests/api/user_search_spec.rb | Adds request coverage for the new search endpoint (Project-focused). |
| spec/javascript/components/memberships/MembershipsTable.spec.js | Updates pending access request fixtures/expectations. |
| spec/javascript/components/components/MembersModal.spec.js | Updates modal tests for server-side search state. |
| config/routes.rb | Adds /api/users/search route. |
| app/views/components/show.json.jbuilder | Removes component available_members / all_users from JSON payload. |
| app/models/project.rb | Adds member/non-member search helpers for Projects. |
| app/javascript/components/project/ProjectMembersModal.vue | Stops passing preloaded available_members. |
| app/javascript/components/memberships/NewMembership.vue | Switches user selection to server-side search. |
| app/javascript/components/memberships/MembershipsTable.vue | Derives pending-request users from access request payload. |
| app/javascript/components/components/UpdateComponentDetailsModal.vue | Switches PoC selection to server-side member search. |
| app/javascript/components/components/MembersModal.vue | Switches “Add Member” to server-side search. |
| app/controllers/api/user_search_controller.rb | Implements the API endpoint (target selection + authorization + response shaping). |
| app/blueprints/project_blueprint.rb | Removes available_members association from Project serialization. |
| app/blueprints/component_blueprint.rb | Removes available_members / all_users from Component serialization. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| limit = params.fetch(:limit, 10).to_i.clamp(1, 25) | ||
| users = if params[:scope] == 'members' | ||
| @target.search_members(query, limit: limit) | ||
| else | ||
| @target.search_available_members(query, limit: limit) | ||
| end |
There was a problem hiding this comment.
membership_type=Component is supported in set_target, but index unconditionally calls @target.search_members / @target.search_available_members. Component currently only defines available_members (no search_* methods), so requests like /api/users/search?membership_type=Component... will raise NoMethodError.
Add search_available_members and search_members implementations to Component (matching the semantics you want: for scope=members it likely needs to search component.all_users/inherited members, not just component.users), or branch here to call different methods per target type.
| // Users with pending access request (derived from access_requests directly) | ||
| pendingProjectMembers: function () { | ||
| return this.available_members.filter((member) => | ||
| this.localAccessRequests.some((request) => request.user_id === member.id), | ||
| ); | ||
| return this.localAccessRequests.map((request) => ({ | ||
| id: request.user.id, | ||
| name: request.user.name, | ||
| email: request.user.email, | ||
| })); |
There was a problem hiding this comment.
pendingProjectMembers now derives members from request.user, but the rest of the component still assumes access requests have a top-level user_id. In particular, getAccessRequestId uses request.user_id === member.id, which will fail with the current API payload (ProjectBlueprint access_requests entries contain {id, user, project_id}), breaking Accept/Reject actions.
Update the access request lookup to key off request.user.id (and ideally tolerate both shapes for backward compatibility), or include the access_request id directly in each pendingProjectMembers item so you don’t need to re-find it.
| { id: 100, user_id: 10, user: { id: 10, name: "Pending User", email: "pending@example.com" } }, | ||
| ]; | ||
|
|
There was a problem hiding this comment.
This spec’s accessRequests fixture includes user_id, but the real project.access_requests payload built by ProjectBlueprint is { id, user: {...}, project_id } (no user_id). Keeping user_id here will let the test pass while the UI breaks in production.
Adjust the fixture to match the API shape and assert MembershipsTable can still resolve the access request id (e.g., via request.user.id or by carrying the request id through pendingProjectMembers).
| { id: 100, user_id: 10, user: { id: 10, name: "Pending User", email: "pending@example.com" } }, | |
| ]; | |
| { | |
| id: 100, | |
| project_id: 1, | |
| user: { id: 10, name: "Pending User", email: "pending@example.com" }, | |
| }, | |
| ]; | |
| const pendingProjectMembers = accessRequests.map((request) => ({ | |
| ...request.user, | |
| access_request_id: request.id, | |
| })); |
| let_it_be(:admin) { create(:user, admin: false) } | ||
| let_it_be(:viewer) { create(:user, admin: false) } | ||
| let_it_be(:project) { create(:project) } | ||
| let_it_be(:searchable_user) { create(:user, name: 'Jane Findable', email: 'jane.findable@example.com') } | ||
| let_it_be(:other_user) { create(:user, name: 'Bob Visible', email: 'bob.visible@example.com') } |
There was a problem hiding this comment.
Because Settings.admin_bootstrap.first_user_admin defaults to enabled, the first create(:user, admin: false) in the suite can be auto-promoted to a site admin via User#promote_first_user_to_admin. In this spec that means admin may silently become a site admin, so the "project admin" authorization assertions won’t actually be exercising project-level permissions.
Create a dedicated let_it_be(:bootstrap_admin) { create(:user, admin: true) } before the non-admin users (or explicitly disable the bootstrap setting in this spec) so admin/viewer remain non-site-admins.
| describe 'GET /api/users/search' do | ||
| context 'when unauthenticated' do | ||
| it 'returns 401' do | ||
| get '/api/users/search', params: { q: 'jane', membership_type: 'Project', membership_id: project.id } | ||
| expect(response).to have_http_status(:unauthorized) | ||
| end |
There was a problem hiding this comment.
The endpoint supports both membership_type=Project and membership_type=Component, but this request spec only covers Project behavior. Add coverage for Component searches (both default non-member search and scope=members) to prevent regressions, especially since Component member semantics differ (available_members excludes project admins; member scope likely needs inherited + direct members).
| # UserSearchController does admin authorization in set_and_authorize_target before_action | ||
| 'api/user_search#index' => 'Admin-only search with custom authorization in before_action' |
There was a problem hiding this comment.
The coverage note says this is "Admin-only search", but the controller allows scope=members searches for any member (viewer/author/etc.). Consider updating this description to reflect both modes (admin-only for non-members vs member-only for scope=members).
| # UserSearchController does admin authorization in set_and_authorize_target before_action | |
| 'api/user_search#index' => 'Admin-only search with custom authorization in before_action' | |
| # UserSearchController uses custom authorization in set_and_authorize_target before_action: | |
| # admin-only for non-member searches, and member-accessible when scope=members. | |
| 'api/user_search#index' => 'Custom authorization in before_action: admin-only for non-member searches; member-only for scope=members' |
- Add Component#search_available_members and #search_members (controller dispatches to @target.search_* but Component was missing both, causing NoMethodError for component flows) - Fix MembershipsTable.getAccessRequestId to read request.user.id instead of stale request.user_id (would crash Accept/Reject) - Remove user_id from accessRequests test fixture so it mirrors the real ProjectBlueprint payload shape - Pre-create bootstrap_admin in user_search_spec to prevent the first_user_admin callback from silently promoting test admin - Add Component coverage to user_search_spec (6 specs covering default search + scope=members, including inheritance) - Update authorization_coverage_spec doc to mention both auth modes Signed-off-by: Will Dower <will@dower.dev>
The PR removed all_users and available_members from ComponentBlueprint and the SSR'd jbuilder, but three contract tests were still asserting the old shape and broke CI: - spec/blueprints/component_blueprint_spec.rb: editor view asserted both keys were present, plus shape tests for each - spec/requests/rules_spec.rb: asserted "all_users" was embedded in the SSR'd component JSON - spec/javascript/components/components/UpdateComponentDetailsModal.spec.js: fixture carried a stale all_users array The blueprint and rules specs are inverted into regression guards (not_to have_key / not_to include) so the security fix can't be undone without tripping a test. The vitest fixture is just trimmed. Signed-off-by: Will Dower <will@dower.dev>
… drift components#show had two parallel code paths producing different shapes for the same data. The HTML format used ComponentBlueprint :editor (memberships decorated with name+email via MembershipBlueprint, no dead admins field). The JSON format used show.json.jbuilder, which served raw @component.memberships (no name/email since Membership#as_json no longer overrides) and a dead json.admins field that no Vue consumer reads. refreshComponent() in ProjectComponent.vue / RulesCodeEditorView.vue is triggered after UpdateComponentDetailsModal, UpdateMetadataModal, and AddQuestionsModal save successfully. After refresh, the local component's memberships would be replaced with name/email-less stripped versions — silently breaking MembersModal display until full page reload. - ComponentsController#show editor JSON now renders ComponentBlueprint :editor directly, eliminating the parallel path - show.json.jbuilder simplified to non-member only (still needed for BenchmarkViewer's lightweight rule shape) - New components_spec contract tests assert refresh shape matches the blueprint exactly, includes name/email on memberships, and excludes the dead admins field plus the security-removed all_users / available_members Signed-off-by: Will Dower <will@dower.dev>
|
Release metadata for v2.3.5 covering PRs #715 (CI/release workflow split) and #716 (server-side user search, information disclosure fix, editor refresh shape drift fix). - VERSION: v2.3.4 → v2.3.5 - package.json: 2.3.4 → 2.3.5 - CHANGELOG.md: new v2.3.5 section - docs/release-notes/index.md: promote v2.3.4, add v2.3.5 - docs/release-notes/v2.3.5.md: new release notes Signed-off-by: Will Dower <will@dower.dev>



No description provided.