Skip to content

Stop the development sandbox widening write-class authorization - #2498

Merged
Chris0Jeky merged 4 commits into
mainfrom
issue-1866/sandbox-authz-story
Sep 4, 2026
Merged

Stop the development sandbox widening write-class authorization#2498
Chris0Jeky merged 4 commits into
mainfrom
issue-1866/sandbox-authz-story

Conversation

@Chris0Jeky

Copy link
Copy Markdown
Owner

Summary

DevelopmentSandboxSettings.Enabled had drifted from a local-dev convenience into an
authorization bypass, and it disagreed with AutomationPolicyEngine.HasAccessAsync, which has
never had a sandbox branch. A sandbox caller holding only a Viewer row passed the API bar and
was then refused at the worker. #1861 (write-capable membership in the worker mirror) sharpened
that divergence without creating it.

Adjudicated story S2: converge on the strict end — the development sandbox never widens
write-class authorization. It stays a read and export convenience.

Removed (sandbox branch deleted; membership-backed in every environment):

  • AuthorizationService.CanWriteBoardAsync, GetWritableBoardIdsAsync, CanDeleteBoardAsync,
    CanManageBoardAccessAsync
  • AuthorizationService.GetUserRoleForBoardAsync — now reports the caller's real role instead of
    a synthetic UserRole.Owner
  • BoardAccessService.EnsureCanManageBoardAccessAsync (the service no longer takes a
    DevelopmentSandboxSettings dependency at all)
  • ProposalExecutionAuthorizationSnapshotReader — the execute path is a write lane, so its
    owner-or-member scope filter now always applies (dependency removed here too)

Kept deliberately:

  • Read bypasses: CanReadBoardAsync, GetReadableBoardIdsAsync — seeded local fixtures stay
    browsable without hand-granting access
  • The export/import services' use of the flag as a requirement (403 unless sandbox is on):
    those endpoints are gated on the sandbox, not widened by it

Fixtures that relied on the bypass now get real BoardAccess rows; no test logic was relaxed.

Closes #1866

Test plan

All run in the worktree, Release, -m:1:

  • dotnet test backend/tests/Taskdeck.Application.Tests/Taskdeck.Application.Tests.csproj -c Release -m:1 --filter "FullyQualifiedName~AuthorizationServiceTests|FullyQualifiedName~BoardAccessServiceTests|FullyQualifiedName~AutomationPolicyEngine|FullyQualifiedName~BatchProposalExecutionServiceTests"123 passed, 0 failed
  • dotnet test backend/tests/Taskdeck.Api.Tests/Taskdeck.Api.Tests.csproj -c Release -m:1 --filter "FullyQualifiedName~BatchExecuteProposalsApiTests|FullyQualifiedName~Authorization|FullyQualifiedName~BoardAccess"57 passed, 0 failed
  • dotnet test backend/tests/Taskdeck.Architecture.Tests/Taskdeck.Architecture.Tests.csproj -c Release -m:128 passed, 1 skipped, 0 failed
  • node scripts/check-docs-governance.mjs — passed

BatchProposalExecutionServiceTests was substituted for an Integration filter: no
Taskdeck.Integration.Tests source file references ProposalExecutionAuthorizationSnapshot, and
that Application suite plus BatchExecuteProposalsApiTests are what exercise the reader.

Notable expectation change: ExecuteProposals_DevelopmentSandboxDoesNotWidenTheAuthorizationSnapshot
(renamed) now expects 404 instead of 403 — with the snapshot no longer widened, a caller with
no membership sees nothing, which is exactly the non-sandbox answer.

Docs impact

  • New docs/decisions/ADR-0068-development-sandbox-never-widens-write-authorization.md (Accepted;
    scope = development sandbox authorization posture; records why S1 — teaching the policy engine
    the bypass — was rejected: it would spread a dev-only bypass into the review-gated automation
    lane, the surface ADR-0003 / GP-06 exist to keep honest)
  • docs/decisions/INDEX.md — row 0068
  • docs/platform/CONFIGURATION_REFERENCE.md — the DevelopmentSandbox section now states what the
    flag does and does not relax
  • docs/security/BETA_THREAT_MODEL.md — the "authorization diverges under development sandbox
    settings" row moves from open — #1866 to shipped, with residuals named

Risks

  • A human running DevelopmentSandbox:Enabled=true against a hand-made database with no
    membership rows now gets 403 (or 404 on the execute path) where the bypass previously
    answered success. Boards created through the API set OwnerId to the creator, so the normal demo
    and dogfooding path is unaffected; the fix is one real BoardAccess row.
  • Frontend affordances in sandbox now reflect the caller's real role, because
    GetUserRoleForBoardAsync no longer reports a synthetic Owner. A sandbox Viewer will see
    viewer-level UI — which is the point, but it is a visible behaviour change locally.
  • Production is untouched: SettingsRegistration.cs force-disables the flag outside the
    Development environment and every deployment artefact pins it off.

Residuals (out of scope, tracked on #1866)

  • LlmQueueService (:133) still relaxes its cross-user request-ownership check under sandbox.
  • BoardJsonExportImportService (:239) retains a sandbox branch on the export/import lane.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@Chris0Jeky

Copy link
Copy Markdown
Owner Author

Round-2 fixes pushed as 5f5a7f5bf (docs + one test rename, no logic change):

  • MEDIUM docs/security/BETA_THREAT_MODEL.md residual-risk item 4 still called the sandbox divergence known/open while the control table at §B3 already marked it shipped. It now states the divergence is closed and names the two remaining flag branches — LlmQueueService:133 (cross-user request-ownership check) and BoardJsonExportImportService:239 (a read check, consistent with the read bypasses this ADR keeps) — both tracked on [Authz] Sandbox-mode divergence: API-side CanWriteBoardAsync bypass vs policy-engine repository gate (PR #1861 review) #1866. The #1433 persist-before-validate residual is unchanged.
  • MEDIUM docs/TESTING_GUIDE.md "Development Sandbox Mode" said an authorization bypass could be enabled by the flag. Rewritten: the flag relaxes read authorization only and gates the DB / board-JSON export-import endpoints; write, delete, manage-access and proposal-execute authorization are never bypassed, and fixtures needing those must seed a real BoardAccess row or board ownership.
  • LOW Renamed BoardAccessServiceTests GrantAccessAsync_ShouldStillRefuseNonManager_WhenSandboxModeIsEnabledGrantAccessAsync_ShouldRefuseNonManager_WithNoSandboxBypassAvailable; BoardAccessService no longer takes the settings at all, so no sandbox-enabled instance can exist.
  • LOW ADR-0068 now describes the :239 residual as a read check.

Verified: node scripts/check-docs-governance.mjs passed; dotnet test backend/tests/Taskdeck.Application.Tests/Taskdeck.Application.Tests.csproj -c Release -m:1 --filter "FullyQualifiedName~BoardAccessServiceTests" — 19 passed, 0 failed. Not re-run for this docs commit: the Api and Architecture filters from the original test plan (untouched by this diff).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[Authz] Sandbox-mode divergence: API-side CanWriteBoardAsync bypass vs policy-engine repository gate (PR #1861 review)

1 participant