Skip to content

Phase B.1: Final acceptance and critical SoD hardening - #4

Merged
henter36 merged 3 commits into
mainfrom
phase-b1-final-acceptance
Jul 19, 2026
Merged

Phase B.1: Final acceptance and critical SoD hardening#4
henter36 merged 3 commits into
mainfrom
phase-b1-final-acceptance

Conversation

@henter36

@henter36 henter36 commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Summary

  • Hardens Critical SoD so all actual processing participants are blocked from final closure, not only the last processor (LastProcessedByUserId).
  • Documents Phase B.1 as Accepted and merged (bda6bdd).
  • Does not start Phase B.2. No migration changes. No API/permission/scope contract changes.

Problem

After User A start-work and User B submit-for-verification, LastProcessedByUserId became B, so User A could still verify-closure despite processing participation.

Solution

EnforceCriticalSoDAsync queries append-only NoteStatusHistory for:

  • Assigned → InProgress / Reopened → InProgress (start-work)
  • InProgress → PendingVerification (submit-for-verification)

Excluded: PendingVerification → InProgress (return-for-rework). Create/submit/assign/cancel/reopen/view are not processing. SoD runs before any mutation.

Tests

  • Unit: multi-user A→B→C, earlier processor, SystemAdministrator, mutation-order, non-critical policy, independent/assigner/creator success.
  • Integration: Critical_note_all_processors_are_blocked_from_final_closure, Independent_verifier_outside_scope_cannot_close_critical_note (with no side-effect assertions).

Docs

README, completion report (Phase B.1 Accepted), test matrix, permissions matrix, state machine.

Verification (tip 2d61411)

Gate Result
Unit 236 passed, 0 skipped
Integration 54 passed, 0 skipped
Frontend 78 passed, 0 skipped
Sonar Quality Gate Passed (Reliability/Maintainability/Security A)
Qlty / Gitleaks / CodeRabbit / Sourcery Passed
Open review threads 0

Explicit non-goals

Phase B.2, notifications, jobs, dashboards, reports, corrective actions.

Do not merge until explicit acceptance.

henter36 and others added 2 commits July 19, 2026 20:16
Derive participation from NoteStatusHistory start-work and
submit-for-verification transitions instead of LastProcessedByUserId
alone, and document Phase B.1 as accepted after merge.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@sourcery-ai

sourcery-ai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR hardens Critical SoD for note closure by enforcing separation-of-duties against all actual processors using NoteStatusHistory, extends unit/integration coverage for multi-user edge cases and non-critical policy, and updates Phase B.1 documentation (completion report, test matrix, permissions matrix, state machine, README) to reflect final acceptance and the new SoD rules.

Sequence diagram for Critical SoD enforcement on verify-closure

sequenceDiagram
    actor User
    participant NoteWorkflowService
    participant db

    User->>NoteWorkflowService: VerifyClosureAsync(id, request, cancellationToken)
    NoteWorkflowService->>NoteWorkflowService: NoteStateMachine.EnsureAllowed(note.Status, NoteStatus.Closed)
    NoteWorkflowService->>NoteWorkflowService: RequireUserId()
    NoteWorkflowService->>NoteWorkflowService: EnforceCriticalSoDAsync(note, actorId, cancellationToken)

    alt note.Severity != NoteSeverity.Critical
        NoteWorkflowService-->>NoteWorkflowService: return (no SoD check)
    else note.Severity == NoteSeverity.Critical
        NoteWorkflowService->>db: NoteStatusHistories.AnyAsync(predicate, cancellationToken)
        db-->>NoteWorkflowService: participated
        alt participated == true
            NoteWorkflowService-->>User: throw InvalidOperationException
        else participated == false
            NoteWorkflowService-->>NoteWorkflowService: proceed with closure
        end
    end

    NoteWorkflowService->>NoteWorkflowService: apply closure mutation and history
    NoteWorkflowService-->>User: NoteDetailDto
Loading

Flow diagram for identifying processing participation from NoteStatusHistory

flowchart LR
    A[Assigned] --> B[InProgress]
    C[Reopened] --> B
    B --> D[PendingVerification]
    D --> B

    subgraph ProcessingTransitions
        A --> B
        C --> B
        B --> D
    end

    subgraph NonProcessingTransitions
        D --> B
    end

    E[User participated in any ProcessingTransitions] --> F[Blocked from verify-closure on Critical note]
    G[No participation in ProcessingTransitions] --> H[Allowed to verify-closure if Notes.VerifyClosure and scope]
Loading

File-Level Changes

Change Details Files
Critical SoD enforcement now blocks any user who actually processed a critical note from verifying its final closure, based on status history rather than LastProcessedByUserId alone.
  • VerifyClosureAsync calls a new asynchronous EnforceCriticalSoDAsync instead of a synchronous check
  • EnforceCriticalSoDAsync queries NoteStatusHistory for processing transitions Assigned→InProgress, Reopened→InProgress, and InProgress→PendingVerification
  • Return-for-rework PendingVerification→InProgress is explicitly excluded from processing participation
  • SoD is executed before any note/assignment/history/audit mutation and applies to SystemAdministrator as well
  • Error message updated to reference any processing participant rather than just the last processor
src/backend/Baseera.Application/Notes/NoteWorkflowService.cs
Unit test suite expanded to cover all Critical SoD scenarios (multi-user, system admin, mutation order, non-critical policy, processors vs independent verifiers) and helper methods refactored to make workflows and histories easier to construct.
  • BuildWorkflow is refactored to return workflow plus IDs, and BuildWorkflowForUser is introduced to build workflows for arbitrary users
  • New helper methods SeedPendingCritical and AppendProcessingHistory seed critical notes and append NoteStatusHistory entries for specific transitions
  • New tests assert that any processor (start-work, submit-for-verification, earlier/reopened processor, system admin) is rejected from verify-closure without mutations
  • New tests verify that independent verifiers, assigners, and creators who never processed can close critical notes when authorized
  • New tests cover non-critical notes where processors may verify closure when policy allows, including multi-user A→B→C and return-for-rework actor behavior
  • Shared assertion helper AssertVerifyRejectedWithoutMutation ensures the note, assignments, histories, and audits remain unchanged on SoD rejection
src/backend/tests/Baseera.UnitTests/NoteWorkflowServiceTests.cs
src/backend/tests/Baseera.UnitTests/NoteWorkflowTests.cs
Integration tests are added to validate Critical SoD behavior end-to-end for multi-processor scenarios and scope-based access control for verifiers.
  • New integration test Critical_note_all_processors_are_blocked_from_final_closure seeds multiple processor/verifier users, drives the workflow via HTTP, and asserts conflict for processors and success for an independent verifier
  • New integration test Independent_verifier_outside_scope_cannot_close_critical_note ensures an out-of-scope independent verifier receives 404 when attempting closure
  • Integration tests verify correct status, ClosedByUserId, single Closed status history entry, and single NoteClosed audit log
src/backend/tests/Baseera.IntegrationTests/NotesCoreIntegrationTests.cs
Phase B.1 documentation updated to mark the phase as accepted/merged and to document the post-merge Critical SoD hardening, test counts, and state-machine/permissions behavior.
  • Phase B.1 completion report now marks the PR as merged, records the merge commit SHA, and declares Phase B.1 Accepted
  • Completion report adds a Post-Merge Critical SoD Hardening section describing the gap, fix, tests, and updated suite counts
  • Phase B.1 test matrix updated with post-merge test counts, adds rows for Critical SoD all-processors behavior, mutation ordering, and non-critical policy
  • Permissions matrix expands the Critical SoD section to describe participation via NoteStatusHistory transitions, excluded transitions, independence from permission checks, and SystemAdministrator non-bypass
  • Phase B.1 state machine doc updated to describe SoD based on NoteStatusHistory, excluded transitions, explicit SystemAdministrator behavior, out-of-scope verifier behavior, and that SoD runs before mutations
  • README now marks Phase B.1 as complete/accepted/merged and points to the completion report; Phase A.1 completion report updated to reference the B.1 merge and decision
docs/phase-b1-completion-report.md
docs/phase-b1-test-matrix.md
docs/permissions-matrix.md
docs/phase-b1-state-machine.md
README.md
docs/phase-a1-completion-report.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@henter36, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 55 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7ebb4af9-b071-4641-a4f8-47e07a3ee356

📥 Commits

Reviewing files that changed from the base of the PR and between 9dfe12e and 2d61411.

📒 Files selected for processing (2)
  • docs/phase-b1-completion-report.md
  • src/backend/tests/Baseera.IntegrationTests/NotesCoreIntegrationTests.cs
📝 Walkthrough

Walkthrough

Critical note closure now identifies processing participants from NoteStatusHistory transitions instead of LastProcessedByUserId. Unit and integration tests cover rejection, independent verification, scope enforcement, and mutation safety, while Phase B.1 documentation records the accepted implementation and hardening.

Changes

Critical SoD hardening

Layer / File(s) Summary
History-based closure enforcement
src/backend/Baseera.Application/Notes/NoteWorkflowService.cs
VerifyClosureAsync now performs an asynchronous, cancellation-aware history query and rejects users associated with qualifying processing transitions.
Critical SoD behavioral validation
src/backend/tests/Baseera.UnitTests/*, src/backend/tests/Baseera.IntegrationTests/NotesCoreIntegrationTests.cs
Tests cover processing participants, independent verifiers, return-for-rework behavior, unchanged state after rejection, final persistence effects, and out-of-scope 404 responses.
Rules and acceptance documentation
README.md, docs/permissions-matrix.md, docs/phase-a1-completion-report.md, docs/phase-b1-*
Documentation records Phase B.1 acceptance and the history-based Critical SoD transition rules, mutation ordering, scope behavior, and test coverage.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Verifier
  participant VerifyClosureAsync
  participant NoteStatusHistories
  Verifier->>VerifyClosureAsync: request verify-closure
  VerifyClosureAsync->>NoteStatusHistories: query processing transitions
  NoteStatusHistories-->>VerifyClosureAsync: participation result
  VerifyClosureAsync-->>Verifier: reject participant or continue closure
Loading

Possibly related PRs

  • henter36/Baseera#3: Establishes the Phase B.1 Critical SoD baseline extended by this history-based enforcement update.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.67% 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 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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly reflects the two main changes: Phase B.1 final acceptance and Critical SoD hardening.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch phase-b1-final-acceptance

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.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request implements hardening for the Critical Separation of Duties (SoD) policy on operational notes. The validation logic in NoteWorkflowService has been updated to asynchronously query the append-only NoteStatusHistory to ensure that any user who participated in the actual processing of a critical note (such as starting work or submitting for verification) is blocked from verifying its final closure. This restriction also applies to system administrators. Additionally, the PR updates relevant documentation and introduces comprehensive unit and integration tests to verify these SoD rules under various multi-user scenarios. There are no review comments to address, and the changes look solid.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

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

Hey - I've found 2 issues, and left some high level feedback:

  • In AppendProcessingHistory and some of the new unit test helpers you call _db.SaveChanges() repeatedly for single-row inserts; consider batching these changes and saving once per test setup to keep the tests faster and reduce noise in future additions.
  • The Critical SoD logic encodes specific (FromStatus, ToStatus) pairs directly in EnforceCriticalSoDAsync; it may be worth centralizing these transitions (e.g., a shared helper or configuration) so that any future state-machine changes don’t silently diverge between SoD enforcement and the documentation/matrix.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `AppendProcessingHistory` and some of the new unit test helpers you call `_db.SaveChanges()` repeatedly for single-row inserts; consider batching these changes and saving once per test setup to keep the tests faster and reduce noise in future additions.
- The Critical SoD logic encodes specific `(FromStatus, ToStatus)` pairs directly in `EnforceCriticalSoDAsync`; it may be worth centralizing these transitions (e.g., a shared helper or configuration) so that any future state-machine changes don’t silently diverge between SoD enforcement and the documentation/matrix.

## Individual Comments

### Comment 1
<location path="src/backend/tests/Baseera.IntegrationTests/NotesCoreIntegrationTests.cs" line_range="191-200" />
<code_context>
+    public async Task Independent_verifier_outside_scope_cannot_close_critical_note()
</code_context>
<issue_to_address>
**suggestion (testing):** Add DB assertions to confirm no closure or side-effects when an out-of-scope verifier is rejected

This integration test only checks the 404 response for the out-of-scope verifier. Because Critical SoD and scoping are security-sensitive, please also assert that the note remains in `PendingVerification` and that no `NoteStatusHistory` or `AuditLogs` entries for `NoteClosed` are created, similar to `Critical_note_all_processors_are_blocked_from_final_closure`. This will confirm the API does not mutate state when access is denied.

Suggested implementation:

```csharp
    [IntegrationConnectionFact]
    public async Task Independent_verifier_outside_scope_cannot_close_critical_note()
    {
        // Arrange users: admin, in-scope worker, and verifier that is outside the note's facility scope
        await _factory.SeedUserAsync(
            "sod-out-admin",
            "مسؤول",
            [RoleCodes.SystemAdministrator],
            (ScopeType.Global, null, null));

        await _factory.SeedUserWithPermissionsAsync(
            "sod-out-worker",
            "معالج",
            [RoleCodes.FacilityCoordinator],
            [],
            (ScopeType.Facility, SeedIds.RegionA, SeedIds.FacilityA1));

        await _factory.SeedUserAsync(
            "sod-out-verifier",
            "معتمد خارج النطاق",
            [RoleCodes.FacilityDirector],
            (ScopeType.Facility, SeedIds.RegionB, SeedIds.FacilityB1));

        // Arrange a critical SoD note that belongs to RegionA / FacilityA1 and is pending verification
        using var arrangeScope = _factory.Services.CreateScope();
        var arrangeDb = arrangeScope.ServiceProvider.GetRequiredService<BaseeraDbContext>();

        var worker = await arrangeDb.Users.SingleAsync(u => u.UserName == "sod-out-worker");
        var admin = await arrangeDb.Users.SingleAsync(u => u.UserName == "sod-out-admin");

        var note = new OperationalNote
        {
            Title = "Critical SoD note - out of scope verifier",
            Description = "Integration test note for out-of-scope verifier closure attempt",
            RegionId = SeedIds.RegionA,
            FacilityId = SeedIds.FacilityA1,
            Status = NoteStatus.PendingVerification,
            CreatedByUserId = admin.Id,
            AssignedProcessorUserId = worker.Id,
            IsCritical = true,
        };

        arrangeDb.OperationalNotes.Add(note);
        await arrangeDb.SaveChangesAsync();

        // Act: attempt to close the note with the out-of-scope verifier
        var client = await _factory.CreateClientForUserAsync("sod-out-verifier");
        var closeRequest = new
        {
            noteId = note.Id,
            status = "Closed",
            reason = "Attempting closure from out-of-scope verifier",
        };

        var response = await client.PostAsJsonAsync($"/api/notes/{note.Id}/close", closeRequest);

        // Assert: API denies access (404 or equivalent not-found for scoping)
        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);

        // Assert: database state has NOT been mutated
        // The note must remain pending verification, with no closure status history or audit log entries.
        using var verifyScope = _factory.Services.CreateScope();
        var verifyDb = verifyScope.ServiceProvider.GetRequiredService<BaseeraDbContext>();

        var entity = await verifyDb.OperationalNotes.SingleAsync(n => n.Id == note.Id);
        Assert.Equal(NoteStatus.PendingVerification, entity.Status);

        Assert.Equal(
            0,
            await verifyDb.NoteStatusHistories.CountAsync(
                h => h.OperationalNoteId == note.Id && h.ToStatus == NoteStatus.Closed));

        Assert.Equal(
            0,
            await verifyDb.AuditLogs.CountAsync(
                a => a.EntityId == note.Id.ToString() && a.Action == "NoteClosed"));

```

1. If the existing implementation of `Independent_verifier_outside_scope_cannot_close_critical_note` already creates the note and calls the API using helper methods (similar to `Critical_note_all_processors_are_blocked_from_final_closure`), you should merge the DB assertion block at the end of the test instead of replacing the whole body. The key part to keep is:
   - Creating a `verifyScope`, `verifyDb`
   - Loading the same `note` used in the test
   - Asserting `NoteStatus.PendingVerification`
   - Asserting `NoteStatusHistories` and `AuditLogs` have **zero** `NoteClosed` entries.

2. Replace `CreateClientForUserAsync`, the route `"/api/notes/{note.Id}/close"`, and the request payload with whatever helpers and endpoints are already used in other closure tests (e.g. `Critical_note_all_processors_are_blocked_from_final_closure`) to keep consistency with the rest of the test suite.

3. Ensure `NoteStatus.PendingVerification`, `NoteStatusHistory`, and `AuditLogs` are the same entities and enums used in the other tests; adjust property names (`OperationalNoteId`, `Action`, etc.) if they differ in your actual model.
</issue_to_address>

### Comment 2
<location path="docs/phase-b1-completion-report.md" line_range="157" />
<code_context>
+
+### Tests
+
+- Unit: all-processors rejection, SystemAdmin, mutation-order, multi-user ABC, non-critical policy, assigner/creator/independent verifier success.
+- Integration: `Critical_note_all_processors_are_blocked_from_final_closure`, `Independent_verifier_outside_scope_cannot_close_critical_note`.
+
</code_context>
<issue_to_address>
**suggestion (typo):** توحيد تسمية الدور بين SystemAdmin و SystemAdministrator في المستندات.

في قسم الاختبارات هنا ذُكر الدور باسم "SystemAdmin"، بينما في أجزاء أخرى من التوثيق (مثل الجملة "SystemAdministrator does not bypass SoD") يُستخدم "SystemAdministrator". يُرجى توحيد الاسم المستخدم لهذا الدور في جميع المستندات.

Suggested implementation:

```
### Tests

- Unit: all-processors rejection, SystemAdministrator, mutation-order, multi-user ABC, non-critical policy, assigner/creator/independent verifier success.
- Integration: `Critical_note_all_processors_are_blocked_from_final_closure`, `Independent_verifier_outside_scope_cannot_close_critical_note`.

```

1. Search the entire `docs/phase-b1-completion-report.md` for any other occurrences of `SystemAdmin` and replace them with `SystemAdministrator` to keep the terminology consistent.
2. If there are cross-references from other documentation files (e.g., design docs or SoD policy docs) that still use `SystemAdmin`, consider updating them as well so all docs use `SystemAdministrator` uniformly.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread docs/phase-b1-completion-report.md Outdated
…ope.

Co-authored-by: Cursor <cursoragent@cursor.com>
@sonarqubecloud

Copy link
Copy Markdown

@henter36
henter36 merged commit 12ab345 into main Jul 19, 2026
7 checks passed
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.

1 participant