Skip to content

fix(security): Compile is gated on Permission.Compile (#3128) - #3145

Merged
meshweaver-cloud[bot] merged 1 commit into
mainfrom
fix/3128-compile-is-gated
Sep 2, 2026
Merged

meshweaver-cloud[bot] merged 1 commit into
mainfrom
fix/3128-compile-is-gated

Conversation

@rbuergi

@rbuergi rbuergi commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes #3128.

The hole

MeshOperations.Compile carried no permission check at all. Its two neighbours in the same file do: Recycle requires Update on the target ("it disposes the node's hub"), Export requires Export per node. Compile required nothing — so a caller who may only read a NodeType could trigger a Roslyn build of it, and mint an _Activity node underneath it.

Measured both before and after #3127. That PR fixed the session hub's inert evaluator, which is why create / recycle / export changed answer there and Compile did not: there was no check for the repaired evaluator to answer.

The scope decision the issue asked for: Permission.Compile

The issue deliberately did not fold this into #3127 because it needs a choice — is a compile a read or a write? — and named the two candidates. This picks Permission.Compile, and the reason is that the entitlement was already modelled and simply never consulted:

role has Compile?
Admin, PlatformAdmin ✅ — added explicitly, as Permission.All | Permission.Compile
Editor ✅ — "Compile = may create a NodeType release. Space editors ship releases by default"
Viewer (Read|Execute|Api) ❌
Commenter ❌

Update was the other candidate — it is what the nearest neighbour uses — but it would have invented a second answer to a question the role table had already settled, and it would gate "may ship a release" on a permission that means something else.

🚨 One detail that makes this choice load-bearing rather than cosmetic: Compile is deliberately excluded from Permission.All (so HasFlag(All) / IsGlobalAdmin stays byte-stable), which is exactly why Admin and PlatformAdmin add it by hand. So "does this caller have it?" genuinely cannot be answered by an All check — it has to be asked for by name, which is what this does.

The compile execution is unaffected and still runs as System; what is gated is the request, which is the part that is not free.

The tri-state, not a swallow

Mirrors Recycle's shape verbatim (#2901 / #2742), including why:

  • granted → the compile body runs, unchanged;
  • denied → CompileDeniedMessage, which names the permission by the name it has in the role table so the reader can ask for the right thing;
  • no verdict → CompileUndeterminedMessage — "this is not a statement about your access, try again". A fold that merely hiccuped or completed without emitting must not be reported as "you may not compile this", which would send a caller who already holds Compile off to request something they own. .Timeout alone cannot cover that second case: it faults on silence, and a fold that completes empty sails past it.

Still fail-closed — nothing is triggered without a positive verdict. The refusal is rendered as the operation's own envelope, never raised, per Doc/Architecture/DenialIsAnAnswer.

A bare Take(1), like Recycle's and for its documented reason: leaving the fold's gate also leaves the hub's turn, and the trigger must stay ordered against the caller's turn.

Verification

Added to SessionDenialIsAnAnswerTest — the suite that measured this issue, on a real-RLS monolith mesh (ConfigureMeshBase, no PublicAdminAccess) through a real SessionHubFactory session hub.

control arm (gate asks Read, which a Viewer holds) with the fix
CompilingANodeTypeWithoutCompilePermissionIsRefusedNotPerformed FAILED passed
AnEditorHoldsCompile_AndTheGateLetsItThrough passed passed
suite Failed: 1, Passed: 12 Passed: 13 in 5 s

The control arm asks for a permission the Viewer does hold, which reproduces the ungated behaviour exactly, and the result discriminates precisely: only the Viewer fact moves.

🚨 The second test is the case that could have falsified the first. A gate that refused everybody would satisfy the denial assertion just as well and would be a far worse bug than the hole it closes — Compile is how a Space editor ships a release. It deliberately asserts only not-the-denial, not a successful compile: whether Roslyn then succeeds is not what this gate decides, and pinning it would make an access test fail for a compiler reason.

dotnet build -c Release -warnaserror on MeshWeaver.Mesh.Operations and Memex.Portal.Shared.Test: 0 Warning(s), 0 Error(s).

Note

The two message constants follow their neighbours (RecycleDeniedMessage, RecycleUndeterminedMessage) and are, like them, plain English rather than catalog keys. Localising one of a pair while its twin is not would be worse than either — if these operation envelopes should be localised, that is a change to all of them together and worth its own issue.

Pairs-with: none — no public type is removed; two internal const strings are added.

🤖 Generated with Claude Code

MeshOperations.Compile carried NO permission check at all. Recycle requires
Update on the target and Export requires Export per node; Compile required
nothing, so a caller who may only READ a NodeType could trigger a Roslyn
build of it and mint an _Activity node underneath it.

Measured both BEFORE and AFTER #3127 — that PR fixed the session hub's inert
evaluator, which is why create/recycle/export changed answer there and
Compile did not. There was no check for the repaired evaluator to answer.

The permission is Compile, not Update. Both were defensible and the issue
asked for the choice to be deliberate: Permission.Compile already exists and
is already scoped for exactly this. Role.Editor carries it ("Space editors
ship releases by default"), Role.Viewer and Role.Commenter do not, and
Admin/PlatformAdmin add it EXPLICITLY because it is deliberately excluded
from Permission.All. The entitlement was modelled and never consulted;
Update would have invented a second answer to a settled question.

Tri-state, mirroring Recycle (#2901/#2742): a fold that hiccuped or completed
without emitting is reported as retryable, never as "you may not compile
this" — which would send a caller who already holds Compile to go and request
it. Still fail-closed: nothing is triggered without a positive verdict.

Control arm: with the gate asking for Read instead (which a Viewer holds, so
the ungated behaviour is reproduced), exactly 1 of 13 fails — the Viewer
denial — and the Editor fact still passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 2, 2026 18:39
@meshweaver-cloud
meshweaver-cloud Bot enabled auto-merge September 2, 2026 18:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The permission gate is correctly implemented with existing tri-state patterns, and the added tests verify both the denied (Viewer) and allowed (Editor) cases.

Pull request overview

This PR closes a security gap where MeshOperations.Compile could be invoked without any permission gate, allowing read-only users to trigger a Roslyn compile and create an _Activity under the target NodeType. It adds an explicit Permission.Compile check (with the existing tri-state “granted/denied/undetermined” behavior) and verifies the behavior via the session-hub test suite.

Changes:

  • Add a permission gate to MeshOperations.Compile requiring Permission.Compile, returning operation-shaped refusal/undetermined envelopes instead of throwing.
  • Add/extend session-hub tests to assert a Viewer is refused and an Editor is not refused by the compile gate.
  • Add a “What’s New” entry documenting the behavior change.
File summaries
File Description
test/Memex.Portal.Shared.Test/SessionDenialIsAnAnswerTest.cs Adds tests covering compile refusal for Viewer and pass-through for Editor on a real-RLS session hub.
src/MeshWeaver.Mesh.Operations/MeshOperations.cs Introduces CompileDeniedMessage/CompileUndeterminedMessage and gates Compile(...) on Permission.Compile with tri-state handling.
src/MeshWeaver.Documentation/Data/WhatsNew/2026-09-02-compiling-a-type-now-asks-permission.md Documents the security fix and explains the refusal vs “could not decide” distinction.
Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

437 tests   437 ✅  56s ⏱️
  1 suites    0 💤
  1 files      0 ❌

Results for commit fae634b.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

234 tests   234 ✅  1m 42s ⏱️
  1 suites    0 💤
  1 files      0 ❌

Results for commit fae634b.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

340 tests   338 ✅  47s ⏱️
  3 suites    2 💤
  3 files      0 ❌

Results for commit fae634b.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

576 tests   384 ✅  2m 35s ⏱️
  3 suites  192 💤
  3 files      0 ❌

Results for commit fae634b.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

    4 files      4 suites   1m 42s ⏱️
1 285 tests 1 285 ✅ 0 💤 0 ❌
1 286 runs  1 286 ✅ 0 💤 0 ❌

Results for commit fae634b.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

2 133 tests   2 133 ✅  2m 23s ⏱️
    3 suites      0 💤
    3 files        0 ❌

Results for commit fae634b.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Test Results

   15 files     15 suites   10m 7s ⏱️
5 005 tests 4 811 ✅ 194 💤 0 ❌
5 006 runs  4 812 ✅ 194 💤 0 ❌

Results for commit fae634b.

@meshweaver-cloud
meshweaver-cloud Bot added this pull request to the merge queue Sep 2, 2026
Merged via the queue into main with commit 8ba0e6a Sep 2, 2026
28 checks passed
@rbuergi
rbuergi deleted the fix/3128-compile-is-gated branch October 10, 2026 13:10
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.

Compile has no permission gate: a Viewer can compile a NodeType they may only read

2 participants