feat: triage workspace + PAT auth + upgrade infrastructure + seed dataset + OpenAPI hardening#731
feat: triage workspace + PAT auth + upgrade infrastructure + seed dataset + OpenAPI hardening#731aaronlippold wants to merge 589 commits into
Conversation
|
| GitGuardian id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| 33788689 | Triggered | Generic Password | 4271c98 | spec/requests/api/auth_spec.rb | View secret |
| 33788688 | Triggered | Generic Password | 4271c98 | doc/openapi/paths/api_auth_login.yaml | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secrets safely. Learn here the best practices.
- Revoke and rotate these secrets.
- If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.
To avoid such incidents in the future consider
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.
There was a problem hiding this comment.
Pull request overview
This PR modernizes the comment/triage workflow and development seeding by introducing a split-pane triage UI (replacing the modal flow), modular/idempotent seed helpers + dev rake tasks, and a DRY’d comment composer state model (ReplyComposerMixin) with supporting factory/test improvements.
Changes:
- Add split-pane triage experience (queue navigation + rule context panel + extracted triage form) and related API support (
include_rule_content, HTML redirect to triage). - Modularize and validate seed pipeline (SeedHelpers, numbered seed files,
dev:*rake tasks, seed verification specs). - Consolidate composer/triage vocabulary utilities and standardize factories/spec fixtures around new traits.
Reviewed changes
Copilot reviewed 83 out of 83 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| spec/services/import/json_archive_importer_spec.rb | Switch review setup to factories/traits for import lifecycle fixtures. |
| spec/services/import/integration/backup_round_trip_spec.rb | Use factory-created reviews in backup round-trip integration spec. |
| spec/services/export/serializers/backup_serializer_spec.rb | Use factory traits for review lifecycle/duplicate-of serialization coverage. |
| spec/services/export/base_spec.rb | Use factory traits for disposition-related export setup. |
| spec/seeds/seed_pipeline_spec.rb | Add end-to-end seed pipeline spec (tagged, truncation strategy). |
| spec/requests/users_spec.rb | Use review factory traits for “My Comments” request fixtures. |
| spec/requests/rules_spec.rb | Use review factory traits for component JSON sidebar/history tests. |
| spec/requests/reactions_spec.rb | Use factory-created reviews/replies for reaction request specs. |
| spec/requests/rack_attack_spec.rb | Use factory-created review for rack-attack reaction target fixture. |
| spec/requests/projects_import_backup_spec.rb | Use factory-created review for project import backup tests. |
| spec/requests/projects_disposition_matrix_export_spec.rb | Use factory traits for triage-status filtered disposition export tests. |
| spec/requests/projects_create_from_backup_spec.rb | Use factory-created review for include_reviews param coverage. |
| spec/requests/project_comments_aggregate_spec.rb | Use factory traits to build aggregate comment fixtures. |
| spec/requests/components_spec.rb | Add HTML redirect expectation for /components/:id/comments; use review factory. |
| spec/requests/components_disposition_matrix_export_spec.rb | Use review factory traits for component disposition export fixtures. |
| spec/requests/component_reviews_spec.rb | Use :component_comment factory trait for component-scoped review tests. |
| spec/rails_helper.rb | Exclude :seed_pipeline specs by default to avoid parallel DB corruption. |
| spec/models/user_spec.rb | Use review factory traits for imported attribution behavior tests. |
| spec/models/reviews_spec.rb | Standardize review creation on factories/traits across invariants + scopes. |
| spec/models/review_polymorphic_commentable_spec.rb | Use :comment/:component_comment traits for polymorphic coverage. |
| spec/models/review_cross_scope_validations_spec.rb | Use review factory traits for cross-scope validation fixtures. |
| spec/models/reaction_spec.rb | Use review factory traits for reaction invariants and summary tests. |
| spec/models/query_performance_spec.rb | Use factory-created reviews to drive component review query tests. |
| spec/models/project_pending_comment_counts_spec.rb | Use factory traits for pending/total count fixtures. |
| spec/models/paginated_comments_pii_spec.rb | Use review factory traits for paginated comment response/reaction counts. |
| spec/models/components_spec.rb | Add assertions for include_rule_content payload shape + updated_at presence. |
| spec/models/component_pending_comment_counts_spec.rb | Use review factory traits for per-component pending count fixtures. |
| spec/lib/seed_helpers_spec.rb | Add unit specs for SeedHelpers API (seed_xccdf/component/review/reply/status/verify). |
| spec/lib/disposition_matrix_export_spec.rb | Switch to review factory traits for export and CSV defang coverage. |
| spec/javascript/mixins/ReplyComposerMixin.spec.js | Add vitest coverage for unified composer state mixin behaviors. |
| spec/javascript/constants/triageVocabulary.spec.js | Add tests for buildStatusFilterOptions(). |
| spec/javascript/components/triage/TriageQueueNav.spec.js | Add tests for 2D triage queue navigation component. |
| spec/javascript/components/triage/CommentTriageForm.spec.js | Add tests for extracted triage form component validation/events. |
| spec/javascript/components/components/ComponentComments.spec.js | Update ComponentComments tests for split-mode behavior and sorting/filter visibility. |
| spec/javascript/components/components/CommentTriageModal.spec.js | Update modal tests to validate delegation to CommentTriageForm and new doTriage API. |
| spec/factory_specs/stig_deadlock_spec.rb | Add regression spec preventing STIG factory deadlock/import side effects. |
| spec/factory_specs/factory_traits_spec.rb | Add spec verifying new FactoryBot traits across multiple models. |
| spec/factories/stigs.rb | Add :skip_rules trait to disable rule import on STIG factory. |
| spec/factories/stig_rules.rb | Ensure stig_rule factory uses a skip-import STIG by default. |
| spec/factories/rules.rb | Add rule status/locked traits used by tests/seeds. |
| spec/factories/reviews.rb | Add review comment/triage/lifecycle traits + auto-membership wiring. |
| spec/factories/projects.rb | Add project membership traits (:with_admin, :with_members). |
| spec/factories/memberships.rb | Add explicit :viewer trait. |
| spec/factories/components.rb | Add component traits for comment period, PoC, released/locked rules. |
| spec/config/seed_idempotency_spec.rb | Update idempotency checks to match modular seed loader + SeedHelpers. |
| spec/blueprints/rule_blueprint_spec.rb | Use review factory traits for comment summary blueprint tests. |
| spec/blueprints/review_membership_blueprints_spec.rb | Use review factory traits for blueprint output coverage. |
| lib/tasks/dev.rake | Add dev:prime/status/verify/reset seed tasks. |
| lib/seed_helpers.rb | Introduce SeedHelpers module (XCCDF import, component seeding, comment seeding, verify/status). |
| docs/superpowers/plans/2026-05-19-comment-interaction-dry.md | Document DRY plan for composer state/event standardization. |
| docs/development/testing.md | Document available FactoryBot traits and usage patterns. |
| docs/development/seed-system.md | Document seed architecture, commands, and SeedHelpers API. |
| db/seeds/data/00_users.rb | Modular seed: demo admin + role-tier + filler users. |
| db/seeds/data/01_projects.rb | Modular seed: demo projects. |
| db/seeds/data/02_srgs.rb | Modular seed: SRG imports via SeedHelpers. |
| db/seeds/data/03_stigs.rb | Modular seed: STIG imports via SeedHelpers. |
| db/seeds/data/04_components.rb | Modular seed: demo components, overlays, dummy stress components, PoC backfill. |
| db/seeds/data/05_memberships.rb | Modular seed: demo RBAC wiring and counter cache reset. |
| db/seeds/data/06_rule_statuses.rb | Modular seed: vary rule statuses for demo coverage. |
| db/seeds/data/10_comments.rb | Modular seed: demo comment threads + triage states. |
| db/seeds/data/11_cross_project.rb | Modular seed: cross-project comment fixtures + triage examples. |
| app/models/review.rb | Improve sync_commentable_from_rule to backfill rule_id from commentable when needed. |
| app/models/component.rb | Add include_rule_content option to paginated_comments, include updated_at, and serialize rule content. |
| app/controllers/components_controller.rb | Redirect HTML /components/:id/comments to triage; add include_rule_content param handling for JSON. |
| app/javascript/mixins/ReplyComposerMixin.vue | Add unified comment composer state + open/close/post hooks. |
| app/javascript/constants/triageVocabulary.js | Add terminal/single-button status sets + buildStatusFilterOptions(). |
| app/javascript/composables/ruleFieldConfig.js | Add canonical FIELD_LABELS mapping for rule context display. |
| app/javascript/components/users/UserComments.vue | Adopt ReplyComposerMixin; switch truncation to CSS; use shared status options builder. |
| app/javascript/components/shared/CommentThread.vue | Switch date formatting to DateFormatMixin helper and remove ad-hoc formatter. |
| app/javascript/components/rules/RulesCodeEditorView.vue | Adopt ReplyComposerMixin and unify composer open/post handling. |
| app/javascript/components/components/ProjectComponent.vue | Adopt ReplyComposerMixin and unify composer open/post handling. |
| app/javascript/components/components/ComponentTriagePage.vue | Add command bar controls for split-mode context/admin panel; wire split-mode events. |
| app/javascript/components/components/ComponentComments.vue | Add split-mode triage integration; default sort by ID; hide filters in split mode; use status options builder; adopt ReplyComposerMixin. |
| app/javascript/components/components/CommentTriageModal.vue | Embed CommentTriageForm; refactor to doTriage API; use DateFormatMixin. |
| app/javascript/components/components/CommentDedupBanner.vue | Switch date formatting to DateFormatMixin helper and remove ad-hoc formatter. |
| app/javascript/components/triage/TriageQueueNav.vue | Add queue navigation UI (prev/next comment + prev/next rule + jump-to dropdown). |
| app/javascript/components/triage/RuleContextPanel.vue | Add rule context side panel with collapsible sections and related comments list. |
| app/javascript/components/triage/CommentTriageForm.vue | Extract triage form component used by modal + split view. |
| app/javascript/components/triage/TriageSplitView.vue | Add split-pane triage view with optimistic-lock support and admin sidebar actions. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| elsif is_stig | ||
| record = Stig.from_mapping(parsed) | ||
| existing = Stig.find_by(stig_id: record.stig_id) | ||
| if existing | ||
| puts " Already exists: #{existing.name} (Stig)" | ||
| return existing | ||
| end | ||
| record.xml = Nokogiri::XML(xml) | ||
| else |
| trait :reply do | ||
| action { 'comment' } | ||
| comment { 'Reply to parent comment' } | ||
| triage_status { nil } | ||
|
|
||
| after(:build) do |review, _evaluator| | ||
| unless review.responding_to_review_id | ||
| parent = create(:review, :comment, user: review.user, rule: review.rule) | ||
| review.responding_to_review_id = parent.id | ||
| review.section = parent.section | ||
| end | ||
| end |
| trait :component_comment do | ||
| action { 'comment' } | ||
| rule { nil } | ||
| section { nil } | ||
| comment { 'Component-level comment' } | ||
|
|
||
| after(:build) do |review| | ||
| unless review.commentable_type == 'Component' | ||
| component = create(:component, :skip_rules) | ||
| review.commentable = component | ||
| review.commentable_type = 'Component' | ||
|
|
||
| if review.user | ||
| project = component.project | ||
| create(:membership, user: review.user, membership: project, role: 'viewer') unless Membership.exists?(user: review.user, membership: project) | ||
| end | ||
| end | ||
| end |
| trait :duplicate do | ||
| triage_status { 'duplicate' } | ||
|
|
||
| after(:build) do |review| | ||
| review.triage_set_by ||= create(:user) | ||
| review.triage_set_at ||= Time.current | ||
| unless review.duplicate_of_review_id | ||
| target = create(:review, :comment, rule: review.rule, user: review.user) | ||
| review.duplicate_of_review_id = target.id | ||
| end | ||
| end |
| comments: { type: Array, required: true }, | ||
| currentId: { type: [Number, String], default: null }, | ||
| }, | ||
| computed: { | ||
| ruleGroups() { | ||
| const groups = []; | ||
| const seen = new Map(); | ||
| for (const c of this.comments) { | ||
| const key = c.rule_id || `component-${c.id}`; | ||
| if (!seen.has(key)) { | ||
| const group = { | ||
| ruleId: key, | ||
| ruleName: c.rule_displayed_name || "(component)", | ||
| comments: [], | ||
| }; | ||
| seen.set(key, group); | ||
| groups.push(group); | ||
| } | ||
| seen.get(key).comments.push(c); | ||
| } | ||
| return groups; | ||
| }, | ||
| currentPosition() { | ||
| for (let gi = 0; gi < this.ruleGroups.length; gi++) { | ||
| const group = this.ruleGroups[gi]; | ||
| for (let ci = 0; ci < group.comments.length; ci++) { | ||
| if (group.comments[ci].id === this.currentId) { | ||
| return { ruleIndex: gi, commentIndex: ci }; | ||
| } | ||
| } | ||
| } | ||
| return { ruleIndex: -1, commentIndex: -1 }; | ||
| }, |
| props: { | ||
| rows: { type: Array, required: true }, | ||
| initialCommentId: { type: [Number, String], required: true }, | ||
| componentId: { type: [Number, String], required: true }, | ||
| effectivePermissions: { type: String, default: null }, | ||
| adminPanelOpen: { type: Boolean, default: false }, | ||
| contextMode: { type: String, default: "commented" }, | ||
| }, | ||
| data() { | ||
| return { | ||
| activeCommentId: this.initialCommentId, | ||
| isDirty: false, | ||
| saving: false, | ||
| conflictAlert: null, | ||
| adminAction: null, | ||
| adminAuditComment: "", | ||
| adminConfirmationId: "", | ||
| adminTargetRuleId: null, | ||
| }; | ||
| }, | ||
| computed: { | ||
| sortedRows() { | ||
| return [...this.rows].sort((a, b) => a.id - b.id); | ||
| }, | ||
| activeComment() { | ||
| return this.sortedRows.find((r) => r.id === this.activeCommentId) || null; | ||
| }, | ||
| canTriage() { |
| it 'creates a review comment using FactoryBot' do | ||
| review = described_class.find_or_seed_review( | ||
| rule: rule, user: user, section: 'check_content', | ||
| comment: 'Test comment for seed helper' | ||
| ) | ||
| expect(review).to be_persisted | ||
| expect(review.action).to eq('comment') | ||
| expect(review.triage_status).to eq('pending') | ||
| end |
| async onTriageSave(decision) { | ||
| await this.doSave(decision, false); | ||
| }, | ||
| async onTriageSaveAndNext(decision) { | ||
| await this.doSave(decision, true); | ||
| }, | ||
| async doSave(decision, advance) { | ||
| if (!this.activeComment) return; | ||
| this.saving = true; | ||
| this.conflictAlert = null; | ||
| try { | ||
| const payload = { | ||
| triage_status: decision.triage_status, | ||
| expected_updated_at: this.activeComment.updated_at, | ||
| }; | ||
| if (decision.response_comment) { | ||
| payload.response_comment = decision.response_comment; | ||
| } | ||
| if (decision.triage_status === "duplicate") { | ||
| payload.duplicate_of_review_id = decision.duplicate_of_review_id; | ||
| } | ||
|
|
||
| const triageRes = await axios.patch(`/reviews/${this.activeComment.id}/triage`, payload); | ||
| this.$emit("triaged", triageRes.data.review); | ||
|
|
||
| if (triageRes.data.response_review) { | ||
| this.$emit("response-posted", { | ||
| parentId: this.activeComment.id, | ||
| responseReview: triageRes.data.response_review, | ||
| }); | ||
| } | ||
|
|
||
| if (!SINGLE_BUTTON_STATUSES.has(decision.triage_status)) { | ||
| const adjRes = await axios.patch(`/reviews/${this.activeComment.id}/adjudicate`, {}); | ||
| this.$emit("adjudicated", adjRes.data.review); | ||
| } | ||
|
|
|
Some notes after review:
Some side notes about locked fields:
|
|
That 3NF doc design is a big enough change to warrant its own PR. Going to cherry pick it and make a new PR off main. |
|
@wdower — heads up, this PR is still in progress. All your review feedback items from the first round are addressed (admin inline, toggle moved, rename, locked comments, staleness badge, etc.) but I still have a few cards to finish:
Will push these in the next session. Don't need a full re-review yet — just wanted you to know the status. — Aaron |
Added/Changed/Fixed sections covering three-column layout, progress bar, DRY color palette, ARIA accessibility, InfoTooltip/InfoNotice adoption, and bug fixes Authored by: Aaron Lippold<lippold@gmail.com>
Vertically center the login content within the viewport. Two-column layout: welcome text (col-lg-5) aligned with the login card (col-lg-6). Removes orphaned heading, br tags, and #login wrapper redundancy. Card gets shadow-sm for light-mode visibility. Stacks cleanly on mobile. Verified light + dark at 1440x900. Authored by: Aaron Lippold<lippold@gmail.com>
Authored by: Aaron Lippold<lippold@gmail.com>
Authored by: Aaron Lippold<lippold@gmail.com>
Add password_automatically_set flag to users. SSO-created users get the flag set true; the Change Password page hides the current-password field and the controller bypasses update_with_password. After setting a password the flag clears and future changes require current password. Backfills existing SSO users. Session specs updated to use the registered provider name from Devise.omniauth_providers. Authored by: Aaron Lippold<lippold@gmail.com>
Logo centered above action text with flex-column layout. 32px height for visual weight. One partial, all button instances updated. Authored by: Aaron Lippold<lippold@gmail.com>
Authored by: Aaron Lippold<lippold@gmail.com>
When a new provider sign-in matches an existing email, the conflict handler stores pending auth data in the session and redirects with link_pending=true. POST /users/complete_link verifies the existing account password and calls link_identity! to attach the new provider. Replaces the dead-end ProviderConflictError flash with an actionable one-step linking flow. Authored by: Aaron Lippold<lippold@gmail.com>
When a cold-match conflict redirects with link_pending=true and the session has pending auth data, the login page shows a warning card with a password field to verify existing-account ownership. Submits to /users/complete_link which attaches the new provider identity. Authored by: Aaron Lippold<lippold@gmail.com>
New docs/deployment/auth/login-gov.md covering keypair generation, portal registration, env vars (local + Heroku inline PEM), issuer trailing-slash gotcha, acr_values table, sandbox vs prod, PIV/CAC. VitePress sidebar updated with Login.gov entry. oidc-okta.md cross-references. VitePress env vars page gets multi-provider registry section. yarn docs:build green with zero dead links. Authored by: Aaron Lippold<lippold@gmail.com>
Replace hardcoded :oidc with Devise.omniauth_providers.first in initiate_link, session_auth_method, and oidc_login_buttons specs. Update oidc_discovery_spec for renamed fetch_oidc_logout_endpoint_for method + providers mock. Replace connect_identity_spec with model-layer tests (OmniAuth session persistence is a test-infra limitation). Authored by: Aaron Lippold<lippold@gmail.com>
- UnifiedRuleForm: status hint → b-alert above form - RuleBlueprint :editor: added missing histories field - OpenAPI RuleEditorResponse updated (39 fields) - Contract tests updated Authored by: Aaron Lippold<lippold@gmail.com>
- "Activity" → "Changelog" (project + component levels) - "History" → "Rule Changelog" (via RULE_TERM) - "Comment History" → "Rule Discussion" (via RULE_TERM) - "Revisions" → "Version Comparison" (project level) - All labels use RULE_TERM/COMPONENT_TERM for customization - Updated tooltips, sidebar titles, button labels - 11 test files updated to match Authored by: Aaron Lippold<lippold@gmail.com>
- CommentRowBlueprint: srg_info object (title, version, is_latest) - parent_rule_displayed_name added to :project + :user views - SecurityRequirementsGuide.srg_info_for_components helper - All 3 consumers wired (CommentQueryService, Project, Users) - Triage sidebar + comment table show SRG name + version - OpenAPI CommentRow + UserCommentRow schemas updated Authored by: Aaron Lippold<lippold@gmail.com>
- VuePropsHelper centralizes 3 shared HAML props (DRY) - 3 HAML files refactored with **splat pattern - docs/development/haml-serialization.md documents standard - VitePress sidebar entry added - Zero hand-built AR model hashes remain in HAML Authored by: Aaron Lippold<lippold@gmail.com>
- VersionSortable: latest? + latest_for_family methods - SRG/STIG/Component Blueprints: is_latest + latest fields - VersionCurrencyDot.vue shared component (green/yellow) - BenchmarkTable: dots in version + based_on columns - OpenAPI schemas updated (SRG, STIG, Component) - 7 model tests + 10 blueprint tests + 6 Vue tests Authored by: Aaron Lippold<lippold@gmail.com>
- ADR for DISA catalog service (SolidQueue, one-click import) - Full v2.x roadmap: 814sp/133 cards/16 epics - Critical path: 5 cards to SPA spike Authored by: Aaron Lippold<lippold@gmail.com>
- Tests expected old single-button unlink (data-test=unlink-identity-button) - Component now uses per-identity table rows with can_unlink flag - Updated tests to pass identities array + verify identity_id in payload - 29/29 UserProfile tests green Authored by: Aaron Lippold<lippold@gmail.com>
|
- SPA spike: 36 endpoints ready, 19 undocumented, 16 missing - Roadmap updated: v2-xcy, v2-54tg, spike all DONE - v2-btu scoped to 14 critical cards (was 39) Authored by: Aaron Lippold<lippold@gmail.com>
The test-env override forced unlock_strategy to :time, claiming SMTP dependency — but ActionMailer uses :test delivery in specs, so no SMTP is needed. The override meant Devise never generated the unlock email routes in test, making POST /users/unlock untestable. Authored by: Aaron Lippold<lippold@gmail.com>
Standard Devise controllers returned 500 for JSON requests — the SPA
needs proper JSON on all auth flows. Overrides follow the existing
RegistrationsController respond_to pattern:
- Users::PasswordsController — request reset, execute reset, and
validate-token (GET edit returns {valid, minimum_password_length})
- Users::ConfirmationsController — resend confirmation instructions
- Users::UnlocksController — resend unlock instructions
- Users::RegistrationsController#edit — profile as CurrentUserBlueprint
Paranoid mode preserved: identical success toast for known and unknown
emails (no enumeration); blank email returns 422. HTML flows unchanged.
14 request specs cover success and failure paths. Authorization
coverage spec skips the new controllers (Devise owns their auth).
Authored by: Aaron Lippold<lippold@gmail.com>
Five new path files documenting the auth flow endpoints the SPA needs: POST/PUT /users/password, GET /users/password/edit, POST /users/confirmation, POST /users/unlock, GET /users/edit. Toast endpoints reference ToastResponse; profile references CurrentUserResponse; token validation documents its inline shape. 12 contract tests validate real responses against the bundled schema via openapi_first, including paranoid-mode success responses and 422 error shapes. Live-tested against the running dev server with curl — all request/response pairs verified with real data. Authored by: Aaron Lippold<lippold@gmail.com>
bundler-audit flagged patch-level CVEs in transitive dependencies: concurrent-ruby, crass, faraday, json, msgpack, net-imap, nokogiri, oauth2, oj, websocket-driver. Conservative update — no version pin changes in Gemfile. bundler-audit now clean; full suite green (3851 RSpec / 3357 Vitest). Authored by: Aaron Lippold<lippold@gmail.com>
helpful_errors rescued StandardError and rendered a generic 500 toast with no logging — the root cause of any production 500 was invisible everywhere. Log the exception class, message, and top of the backtrace before rendering. Specs cover the log line and the admin/non-admin message split. Authored by: Aaron Lippold<lippold@gmail.com>
devise-security's before_logout hook calls update_unique_session_id! during the sign_out that follows account self-deletion; on the destroyed record it raises NotPersistedError. The account deleted but the user saw a 500 toast (session limits are enabled by default). The hook respects skip_session_limitable? — return true for destroyed records. Covered by the self-delete request specs and live-verified. Authored by: Aaron Lippold<lippold@gmail.com>
Self-delete previously took one click on a live session — no password, no continuity check. Per OWASP ASVS 3.7.1 and the GitLab/GitHub account deletion pattern: - Local-credential users must re-enter current_password; wrong attempts count toward lockout (423 once locked). Provider-managed and SSO-created (auto-password) accounts are exempt — their IdP owns re-authentication. - The last system admin cannot self-delete (mirrors the existing UsersController guard): 422 with promote-another-admin guidance. - ConfirmDeleteModal gains an optional requirePassword prop (default false, other consumers unaffected); the profile delete modal collects the password for local users only. - DELETE /users documented in OpenAPI (200/422/423) with contract tests validating each response shape against the schema. Live-verified: curl matrix with DB checks + Playwright golden path in light and dark modes. Authored by: Aaron Lippold<lippold@gmail.com>
A project could drop to zero admins through the membership API, orphaning it for its team. Per the GitLab/GitHub last-owner pattern, the last project-admin membership can no longer be removed or downgraded — the 422 names the project and says to transfer the admin role first. The guard lives in the model as the single source of truth (update validation + before_destroy abort), so console, importers, and any future callers hit it too; the existing controller error paths surface it with no controller changes. Cascades are exempt via destroyed_by_association (deleting a project legitimately removes its memberships; the user-deletion path gets its own guard next). Component memberships are exempt — project admins govern components through effective_permissions. The OpenAPI spec had promised 'cannot demote the last admin' since it was authored; the 422 responses are now documented and the code honors the design. 13 new specs + schema-validated contract tests; live verified via curl with the allow-path confirmed after adding a second admin. Authored by: Aaron Lippold<lippold@gmail.com>
Deleting a user who held a project's only admin membership silently
orphaned the project for its team — the membership cascade bypasses the
membership-layer guard by design (destroyed_by_association). Apply the
GitLab/GitHub transfer-ownership rule at both deletion endpoints: the
admin user-management path and Devise self-delete return 422 naming the
affected projects until the admin role is transferred.
User#solely_administered_projects is the shared source of truth — one
SQL query (admin-membership subselect anti-joined against other-admin
memberships), no Ruby enumeration; component memberships never count
since project admins govern components through effective_permissions.
The block message comes from a single model method with a contextual
subject ('This user is' / 'You are'), capped at five project names with
an overflow count. System admins get no bypass — continuity, not
privilege.
OpenAPI 422s documented on both delete operations with contract tests
validating each against the schema. Live-verified: both paths blocked,
then allowed after transferring the role to a second admin.
Authored by: Aaron Lippold<lippold@gmail.com>
|
| target: production | ||
| environment: | ||
| RAILS_ENV: production | ||
| DATABASE_URL: postgres://postgres:schemathesis@db/vulcan_schemathesis |
| @@ -39,6 +39,11 @@ | |||
| default: false, | |||
| }, | |||
| }, | |||
| data() { | |||
| return { | |||
| instanceId: Math.random().toString(36).slice(2, 9), | |||
| // picks its own seed, so popoverIds don't collide when (e.g.) the navbar | ||
| // pack and the project_component pack both render UserBadges on the same | ||
| // page. Combined with this._uid for per-instance uniqueness within a pack. | ||
| const moduleSeed = Math.random().toString(36).slice(2, 8); |
| name: Jane Doe | ||
| email: jane.doe@example.org | ||
| admin: false | ||
| password: SecureP@ssw0rd2026! |
| summary: Set a new password | ||
| value: | ||
| user: | ||
| password: SecureP@ssw0rd2026! |
|
|
||
| it 'returns true when IP is within an allowed CIDR' do | ||
| token = create(:personal_access_token, user: user, | ||
| allowed_ips: ['10.0.0.0/8']) |
|
|
||
| it 'returns true when allowed_ips is empty array (allow all)' do | ||
| token = create(:personal_access_token, user: user, allowed_ips: []) | ||
| expect(token.ip_allowed?('1.2.3.4')).to be true |
| describe '#ip_allowed?' do | ||
| it 'returns true when allowed_ips is nil (allow all)' do | ||
| token = create(:personal_access_token, user: user, allowed_ips: nil) | ||
| expect(token.ip_allowed?('1.2.3.4')).to be true |
|
|
||
| it 'accepts valid CIDR entries' do | ||
| token = build(:personal_access_token, user: user, | ||
| allowed_ips: ['10.0.0.0/8', '192.168.1.0/24', '2001:db8::/32']) |
|
|
||
| it 'accepts valid CIDR entries' do | ||
| token = build(:personal_access_token, user: user, | ||
| allowed_ips: ['10.0.0.0/8', '192.168.1.0/24', '2001:db8::/32']) |




Summary
Major feature branch: 247 commits, 668 files changed, 53K insertions. Builds on the triage split-pane workspace (PR #717 follow-up) and adds infrastructure standardization, API authentication, upgrade system, seed data, and documentation.
Core Features (earlier commits on this branch)
New in this push
{title, message: Array, variant}across all mutation endpointsvulcan_development/test/productionnaming, test DB name hardcoded to prevent collision, port registry (5435)drop_invalid_reviewsnow deletes children before parents (RESTRICT safety)Bug fixes
Test plan
bin/parallel_rspec spec/— full backend suiteyarn test:unit— Vue component testsyarn lint:ci— 0 warningsbundle exec rubocop— 0 offensesyarn openapi:bundle && yarn openapi:lint— 0 errorsbundle exec rspec spec/contracts/— 107 contract testsyarn docs:build— VitePress builds cleanbundle exec brakeman— 0 warningsAuthored by: Aaron Lippoldlippold@gmail.com