Vulcan v2.3.5
Released: 2026-04-11
Highlights
- Information disclosure fix — full user directory was previously pre-loaded into the project/component page payloads, allowing any project admin to enumerate every registered Vulcan user. Replaced with a server-side
/api/users/searchendpoint with admin-only authorization and ascope=membersmode for PoC selection. - Editor refresh shape drift fix —
refreshComponent()was silently degrading the in-memory component shape after edits (memberships lost their name/email decoration, breakingMembersModaluntil full page reload). The editor JSON path now usesComponentBlueprint :editordirectly, eliminating the parallel jbuilder code path. - CI/release workflow split — Docker release moved to its own
release.ymlso the test suite no longer reruns on release publish. All GitHub Actions pinned to commit SHAs.
Added
GET /api/users/search— server-side user search endpointq(min 2 chars),membership_type(ProjectorComponent),membership_id,limit(default 10, max 25)scope=members(optional) — search within existing members instead of non-members; accessible to any member, not just admins- Returns only
id,name,email
Project#search_available_members/Project#search_membersComponent#search_available_members/Component#search_members(matchesall_userssemantics for direct + inherited members)- Async server-side search via
vue-multiselectinNewMembership.vue,MembersModal.vue, andUpdateComponentDetailsModal.vue(debounced 300ms) - Contract tests asserting
/components/:id.jsoneditor refresh response shape exactly matchesComponentBlueprint :editor - Regression guards in
ComponentBlueprintandrules_specassertingavailable_membersandall_usersare absent from serialized payloads - 6
Component-target test cases inuser_search_speccovering default search,scope=members, project-admin exclusion, and inherited member resolution - Dedicated
release.ymlworkflow triggered only on release published events
Changed
available_membersandall_usersremoved fromComponentBlueprintandProjectBlueprinteditor payloadsMembershipsTablederives pending access request user info fromaccess_requestsdirectlyComponentsController#showeditor JSON rendersComponentBlueprint :editordirectly (no more jbuilder editor branch)show.json.jbuildersimplified to non-member only (BenchmarkViewer's lightweight rule shape)- All GitHub Actions pinned to full commit SHAs
Fixed
- Information disclosure: project admins could enumerate the full user directory via the page payload
- Editor refresh shape drift:
refreshComponent()was replacingcomponent.membershipswith raw ActiveRecord JSON (noname/emailfrom the user join), silently breakingMembersModal MembershipsTable.getAccessRequestIdwas reading stalerequest.user_idafter the payload moved to nestedrequest.user.{id,name,email}— Accept/Reject would crashComponent#search_available_members/#search_memberswere missing — controller dispatched to@target.search_*but onlyProjecthad them, causingNoMethodErrorfor anymembership_type=Componentrequestfirst_user_adminafter_create callback was silently promoting test users to site admin in new request specs- SBOM tag mismatch (
v2.3.4vs2.3.4) in release workflow
Security
The /api/users/search endpoint enforces:
- Default search (non-members): admin-only — only project/component admins can search for candidates to add
scope=memberssearch: any member of the project/component (used for PoC selection where the user list is already known to the searcher)- Site admins (
current_user.admin) can search any project or component - Min 2-character query, max 25 results — limits enumeration even with admin access
- ILIKE input sanitized via
ActiveRecord::Base.sanitize_sql_like
Upgrade Notes
No database migrations required. This is a drop-in upgrade from v2.3.4.
Frontend behavior change:
- The user dropdown in "Add Member" modals (project, component, and PoC selection) now shows an empty list until the user types at least 2 characters. This is intentional — the full user directory is no longer sent to the client.
No new environment variables.
No gem changes.
Version: v2.3.5
