Skip to content

Commit b0a050a

Browse files
KSemenenkoclaude
andcommitted
Commit the working tree: graph incoming privacy fence, RapidMembership 10.4.1, integration oracles
Owner request: commit everything in the working tree. This is the in-progress work of the parallel write-path session as it stood: - GraphIncomingEdgesReader checks the target collection DocumentsRead grant before reading the collection definition and reports a denial as the same NotFound as a hidden vertex; GraphIncomingPrivacyTests covers the denial, the unchanged store and the healthy read after the grant is restored. CrossPartitionEdges.md describes the fence. - ManagedCode.Orleans.RapidMembership 10.4.0 -> 10.4.1. - Aspire integration oracles across authorization, client API, database composition, event streams, messaging, query execution and storage replication aligned with the current server contracts. - Unit-test inventory updated for the new case. Verification: not built or tested as a whole by this session, because a dotnet test --no-build integration run was using the binaries; repository governance passes. Build and Tests on this commit is the first full check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent ca6010d commit b0a050a

42 files changed

Lines changed: 224 additions & 107 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎Directory.Packages.props‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
<PackageVersion Include="ModelContextProtocol.AspNetCore" Version="2.3.0" />
3333
<PackageVersion Include="ModelContextProtocol.Core" Version="2.3.0" />
3434
<PackageVersion Include="Microsoft.Orleans.Server" Version="10.4.0" />
35-
<PackageVersion Include="ManagedCode.Orleans.RapidMembership" Version="10.4.0" />
35+
<PackageVersion Include="ManagedCode.Orleans.RapidMembership" Version="10.4.1" />
3636
<PackageVersion Include="Microsoft.Orleans.Streaming" Version="10.4.0" />
3737
<PackageVersion Include="Microsoft.Orleans.Journaling" Version="10.4.0-alpha.1" />
3838
<PackageVersion Include="Microsoft.Orleans.DurableJobs" Version="10.4.0-alpha.1" />

‎docs/Features/GraphTraversal/CrossPartitionEdges.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ Frontend: N/A; no separate graph UI. SQL: N/A for this first edge transport stag
109109

110110
Test-only extension of SAME GraphIncomingClusterTests.PersistedSourceGrantHidesCrossRowsAndTargetGraphDenialIsExplicit; original declaration/UID/Args/deadline/execution50/exclusiveheavy1 preserved. REQ/AC-AUTH-005/007, REQ/AC-GRAPH-XPART-003/005 and REQ-GRAPH-002 under ADR-014/102/125/117. Existing GraphIncomingClusterScenario explicitly verifies three distinct atomic partitions. No public/persisted/product/options/clock seam.
111111

112-
Ordered flow: preserve original target-only source exclusion and target GraphRead denial. Retain full-reader original grants. Healthy admitted official MCP/SDK owner enters all four direct SDK/official MCP/Q1SDK/Q1MCP incoming routes through existing RequestCqrsClusterCallers and SqlClusterProtocol. Independent three-row literals retain all complete edges, full references, delivered revisions, redacted attributes and page version/projection; each call owns a fresh positive current cut. Persist epoch2 source database grant removal (local row only); epoch3 target DocumentsRead removal (PermissionDenied, no page); epoch4 original scoped repair (complete three rows); epoch5 Revoked (Unauthenticated, no protected page); epoch6 exact original scoped repair (complete three rows). All changes through existing authorized ConfigurePrincipal; full returned principal values compared, not caller role. Original MCP owner is created while healthy before revoke, then original requests reauthorize persisted state.
112+
Ordered flow: preserve original target-only source exclusion and target GraphRead denial. Retain full-reader original grants. Healthy admitted official MCP/SDK owner enters all four direct SDK/official MCP/Q1SDK/Q1MCP incoming routes through existing RequestCqrsClusterCallers and SqlClusterProtocol. Independent three-row literals retain all complete edges, full references, delivered revisions, redacted attributes and page version/projection; each call owns a fresh positive current cut. Persist epoch2 source database grant removal (local row only); epoch3 target DocumentsRead removal (NotFound, no page); epoch4 original scoped repair (complete three rows); epoch5 Revoked (Unauthenticated, no protected page); epoch6 exact original scoped repair (complete three rows). All changes through existing authorized ConfigurePrincipal; full returned principal values compared, not caller role. Original MCP owner is created while healthy before revoke, then original requests reauthorize persisted state.
113113

114114
Each state operation brackets complete current administrator document images for ALL four vertices, all three partition outbox states and complete actual administrator incoming edge rows (cut observed independently, logical rows exact). All four refusal envelopes preserve errors and omit credentials/private marker; no global physical-position equality across replicated system commits or synthesized cut. Existing root administrator acts only as setup/current-state observer, never as substitute for reader admission.
115115

@@ -123,4 +123,4 @@ Actual existing SqlModelViewClusterAuthorizationAssertions.AssertDeniedAsync est
123123

124124
## target collection privacy fence
125125

126-
GraphIncomingEdgesReader.RequireVisible→TryRequireVisible catches target collection PermissionDenied as hidden visibility and returns exact NotFound. Epoch3 target DocumentsRead reduction requires NotFound/no page, independently from preserved original target GraphRead PermissionDenied. Source GraphRead absence omits remote rows. Original principals/policies and all public codes stay unchanged.
126+
GraphIncomingEdgesReader.RequireVisible→TryRequireVisible catches target collection PermissionDenied as hidden visibility and returns exact NotFound. Epoch3 target DocumentsRead reduction requires NotFound/no page, independently from preserved original target GraphRead PermissionDenied. Source GraphRead absence omits remote rows. Original principals/policies and all public codes stay unchanged. The reader checks the target collection DocumentsRead grant before it reads the collection definition and reports that denial as the same NotFound; `GraphIncomingPrivacyTests.TargetCollectionReadDenialIsNotFoundUntilTheGrantIsRepaired` covers the denial, the unchanged store and the healthy read after the grant is restored.

‎scripts/Features/CodeQuality/functional-coverage.unit-test-inventory.json‎

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19997,9 +19997,9 @@
1999719997
"methodName": "HiddenSourceEndpointIsOmittedFromIncomingProjection",
1999819998
"instanceName": "HiddenSourceEndpointIsOmittedFromIncomingProjection",
1999919999
"sourcePath": "tests/KeyLoad.UnitTests/GraphTraversal/Cases/Graph/GraphIncoming/GraphIncomingPrivacyTests.cs",
20000-
"sourceSha256": "3b8d0538a2a3c21fa96178c0cbcba2eccf60cdbcc9e4c8a50d775009edb9fafe",
20001-
"lineNumber": 14,
20002-
"endLineNumber": 37,
20000+
"sourceSha256": "efcc8b9585aee902241a9fe762d1622e0f7497030659f5112e097c48c0ea0f98",
20001+
"lineNumber": 17,
20002+
"endLineNumber": 40,
2000320003
"classification": "functional",
2000420004
"exclusionReason": null,
2000520005
"requirements": [
@@ -20015,9 +20015,27 @@
2001520015
"methodName": "HiddenTargetEndpointIsNotFoundWithoutReturningProjectionRows",
2001620016
"instanceName": "HiddenTargetEndpointIsNotFoundWithoutReturningProjectionRows",
2001720017
"sourcePath": "tests/KeyLoad.UnitTests/GraphTraversal/Cases/Graph/GraphIncoming/GraphIncomingPrivacyTests.cs",
20018-
"sourceSha256": "3b8d0538a2a3c21fa96178c0cbcba2eccf60cdbcc9e4c8a50d775009edb9fafe",
20019-
"lineNumber": 39,
20020-
"endLineNumber": 61,
20018+
"sourceSha256": "efcc8b9585aee902241a9fe762d1622e0f7497030659f5112e097c48c0ea0f98",
20019+
"lineNumber": 42,
20020+
"endLineNumber": 64,
20021+
"classification": "functional",
20022+
"exclusionReason": null,
20023+
"requirements": [
20024+
"REQ-GRAPH-XPART-003"
20025+
],
20026+
"acceptance": [
20027+
"AC-GRAPH-XPART-003"
20028+
]
20029+
},
20030+
{
20031+
"className": "KeyLoad.UnitTests.GraphTraversal.GraphIncomingPrivacyTests",
20032+
"reportClassName": "KeyLoad.UnitTests.GraphTraversal.GraphIncomingPrivacyTests",
20033+
"methodName": "TargetCollectionReadDenialIsNotFoundUntilTheGrantIsRepaired",
20034+
"instanceName": "TargetCollectionReadDenialIsNotFoundUntilTheGrantIsRepaired",
20035+
"sourcePath": "tests/KeyLoad.UnitTests/GraphTraversal/Cases/Graph/GraphIncoming/GraphIncomingPrivacyTests.cs",
20036+
"sourceSha256": "efcc8b9585aee902241a9fe762d1622e0f7497030659f5112e097c48c0ea0f98",
20037+
"lineNumber": 66,
20038+
"endLineNumber": 96,
2002120039
"classification": "functional",
2002220040
"exclusionReason": null,
2002320041
"requirements": [
@@ -70133,6 +70151,7 @@
7013370151
"KeyLoad.UnitTests.GraphTraversal.GraphIncomingMcpContractTests|AndPublicJsonRoundTripsPreserveFullIncomingEdgeAndProjection|AndPublicJsonRoundTripsPreserveFullIncomingEdgeAndProjection",
7013470152
"KeyLoad.UnitTests.GraphTraversal.GraphIncomingPrivacyTests|HiddenSourceEndpointIsOmittedFromIncomingProjection|HiddenSourceEndpointIsOmittedFromIncomingProjection",
7013570153
"KeyLoad.UnitTests.GraphTraversal.GraphIncomingPrivacyTests|HiddenTargetEndpointIsNotFoundWithoutReturningProjectionRows|HiddenTargetEndpointIsNotFoundWithoutReturningProjectionRows",
70154+
"KeyLoad.UnitTests.GraphTraversal.GraphIncomingPrivacyTests|TargetCollectionReadDenialIsNotFoundUntilTheGrantIsRepaired|TargetCollectionReadDenialIsNotFoundUntilTheGrantIsRepaired",
7013670155
"KeyLoad.UnitTests.GraphTraversal.GraphIncomingProjectionTests|LocalCanonicalIncomingEdgeUsesZeroDeliveryRevision|LocalCanonicalIncomingEdgeUsesZeroDeliveryRevision",
7013770156
"KeyLoad.UnitTests.GraphTraversal.GraphIncomingProjectionTests|MixedLocalAndCrossRowsProjectProtectedFieldsBeforeRetention|MixedLocalAndCrossRowsProjectProtectedFieldsBeforeRetention",
7013870157
"KeyLoad.UnitTests.GraphTraversal.GraphIncomingSortCancellationTests|FullPageSortPreservesCancellationAndDeadlineAndHealthyReadFollows|FullPageSortPreservesCancellationAndDeadlineAndHealthyReadFollows",

‎src/KeyLoad.Core/GraphTraversal/Queries/GraphIncoming/GraphIncomingEdgesReader.cs‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@ internal sealed class GraphIncomingEdgesReader(DatabaseEngine database, IKeyValu
1212
{
1313
private const string RangeExceeded = "The graph incoming-edge scan exceeds its bound.";
1414
private const string InvalidProjection = "A graph reverse projection is inconsistent.";
15+
private const string VertexUnavailable = "The graph vertex is unavailable.";
1516
private ResourceDefinition targetGraph = null!;
1617
private ResourceDefinition targetCollection = null!;
1718

1819
internal GraphIncomingEdgesPage Read()
1920
{
2021
targetGraph = RequireGraphRead(request.Target.Partition);
21-
AuthorizationRequire(request.Target.Partition, Capability.DocumentsRead, request.Target.Collection);
22+
RequireTargetCollectionRead();
2223
targetCollection = GraphIncomingReadScope.ReadCollection(view, grant,
2324
request.Target.Partition, request.Target.Collection, budget);
2425
request = request with
@@ -138,10 +139,20 @@ private ResourceDefinition RequireGraphRead(PartitionRef partition)
138139
}
139140
}
140141

141-
private void RequireVisible(EntityRef entity)
142+
private void RequireTargetCollectionRead()
142143
{
143-
const string VertexUnavailable = "The graph vertex is unavailable.";
144+
try
145+
{
146+
AuthorizationRequire(request.Target.Partition, Capability.DocumentsRead, request.Target.Collection);
147+
}
148+
catch (KeyLoadException error) when (error.Code is ErrorCode.PermissionDenied)
149+
{
150+
throw Errors.Fail(ErrorCode.NotFound, VertexUnavailable);
151+
}
152+
}
144153

154+
private void RequireVisible(EntityRef entity)
155+
{
145156
if (!TryRequireVisible(entity))
146157
{
147158
throw Errors.Fail(ErrorCode.NotFound, VertexUnavailable);

‎tests/KeyLoad.IntegrationTests/Authorization/Models/Eventing/EventingRequiredClaimClusterState.cs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ internal sealed class EventingRequiredClaimClusterState(RequiredProcessingPolicy
55
internal RequiredProcessingPolicyClusterState Input { get; } = input;
66
internal ReceiveRequest Denied { get; } = new(Guid.NewGuid(), input.Lane);
77
internal ReceiveRequest EpochProbe { get; } = new(Guid.NewGuid(), input.Lane, MaxBytes: 1);
8+
internal ReceiveRequest Claim { get; } = new(Guid.NewGuid(), input.Lane);
89
internal ReceiveResult EpochReceipt { get; set; } = null!;
910
internal ReceiveResult Claimed { get; set; } = null!;
1011
internal bool Completed

‎tests/KeyLoad.IntegrationTests/Authorization/Observations/PolicyEpochMembership.cs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ internal static class PolicyEpochMembership
1010
private const int MaximumResponseBytes = 16_384;
1111
private static readonly TimeSpan PollInterval = TimeSpan.FromMilliseconds(50);
1212

13-
internal static async Task WaitForSurvivorAsync(ClusterFixture fixture, CancellationToken token)
13+
internal static async Task WaitForSurvivorsAsync(ClusterFixture fixture, string[] survivors, CancellationToken token)
1414
{
1515
using var http = McpCallerHttp.Create(fixture, McpCallerProtocol.Node2);
1616
http.MaxResponseContentBufferSize = MaximumResponseBytes;
@@ -24,7 +24,7 @@ internal static async Task WaitForSurvivorAsync(ClusterFixture fixture, Cancella
2424
?? throw new InvalidDataException("The Orleans membership health response is missing.");
2525
await File.WriteAllTextAsync(Path.Combine(TestContext.ResultsDirectory, "policy-authority-membership.json"),
2626
JsonSerializer.Serialize(snapshot, JsonDefaults.Options), token);
27-
if (snapshot.ActiveSilos.SequenceEqual(new[] { McpCallerProtocol.Node2 }, StringComparer.Ordinal))
27+
if (snapshot.ActiveSilos.Order(StringComparer.Ordinal).SequenceEqual(survivors, StringComparer.Ordinal))
2828
{
2929
return;
3030
}

‎tests/KeyLoad.IntegrationTests/Authorization/Scenarios/Eventing/EventingRequiredClaimClusterFinal.cs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ await EventingRequiredClaimClusterRefusals.ReceiveAsync(root, reader, state, sta
1616
await EventingRequiredClaimClusterCurrent.ObserveAsync(root, state, async () => await SqlClusterProtocol.EqualAsync(
1717
state.Receipt, await RequiredProcessingPolicyClusterRoutes.ProcessAsync(reader, state.Input, state.Healthy, route, token)), token);
1818
}
19-
await EventingRequiredClaimClusterRefusals.ReceiveAsync(root, reader, state, state.Denied,
19+
await EventingRequiredClaimClusterRefusals.ReceiveAsync(root, reader, state, state.Denied, ErrorCode.PermissionDenied,
20+
RequiredProcessingPolicyClusterState.EpochChanged, token);
21+
await EventingRequiredClaimClusterRefusals.ReceiveAsync(root, reader, state, state.Claim,
2022
ErrorCode.StaleLease, "The delivery lease is stale.", token);
2123
await EventingRequiredClaimClusterTenant.RefuseAsync(root, reader, state, token);
2224
await EventingRequiredClaimClusterExport.RefuseAsync(root, reader, state, token);

‎tests/KeyLoad.IntegrationTests/Authorization/Scenarios/Eventing/EventingRequiredClaimClusterWarm.cs‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ internal static async Task RunAsync(RequestCqrsClusterCallers root, RequestCqrsC
1515
await EventingRequiredClaimClusterRefusals.ReceiveAsync(root, reader, state, state.EpochProbe, ErrorCode.PermissionDenied, RequiredProcessingPolicyClusterState.Denied, token);
1616
await RequiredProcessingPolicyClusterSetup.PolicyAsync(root, input, RequiredProcessingPolicyClusterState.Before, token);
1717
await EventingRequiredClaimClusterReceive.ReplayAsync(root, reader, state, state.EpochProbe, state.EpochReceipt, token);
18+
await EventingRequiredClaimClusterRefusals.ReceiveAsync(root, reader, state, state.Denied, ErrorCode.PermissionDenied, RequiredProcessingPolicyClusterState.Denied, token);
1819
await RequiredProcessingPolicyClusterSetup.PrincipalAsync(root, input, 2L, true, token);
1920
await EventingRequiredClaimClusterRefusals.ReceiveAsync(root, reader, state, state.Denied, ErrorCode.Unauthenticated, "The credential is unavailable or expired.", token);
2021
await RequiredProcessingPolicyClusterSetup.PrincipalAsync(root, input, 3L, false, token);
2122
await EventingRequiredClaimClusterRefusals.ReceiveAsync(root, reader, state, state.EpochProbe, ErrorCode.PermissionDenied, RequiredProcessingPolicyClusterState.EpochChanged, token);
22-
state.Claimed = await RequiredProcessingPolicyClusterRoutes.ReceiveAsync(reader, input, "first", state.Denied, token);
23+
await EventingRequiredClaimClusterRefusals.ReceiveAsync(root, reader, state, state.Denied, ErrorCode.PermissionDenied, RequiredProcessingPolicyClusterState.EpochChanged, token);
24+
state.Claimed = await RequiredProcessingPolicyClusterRoutes.ReceiveAsync(reader, input, "first", state.Claim, token);
2325
var delivery = state.Claimed.Deliveries.Single();
24-
await EventingRequiredClaimClusterReceive.ReplayAsync(root, reader, state, state.Denied, state.Claimed, token);
26+
await EventingRequiredClaimClusterReceive.ReplayAsync(root, reader, state, state.Claim, state.Claimed, token);
2527
var forged = input.Processing(delivery, "first") with
2628
{
2729
Token = delivery.Token + ".forged"

‎tests/KeyLoad.IntegrationTests/Authorization/Scenarios/PolicyEpochs/PolicyEpochAuthorityOutage.cs‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ namespace KeyLoad.IntegrationTests.Authorization;
1010
internal static class PolicyEpochAuthorityOutage
1111
{
1212
private const string Scenario = "policy-cursor-authority-unavailable";
13-
private static readonly string[] StoppedNodes = [McpCallerProtocol.Node1, McpCallerProtocol.Node3];
13+
// One copy is lost while the membership majority survives: losing a majority stops membership agreement until the
14+
// whole cluster restarts, so it cannot model a recoverable authority outage.
15+
private static readonly string[] StoppedNodes = [McpCallerProtocol.Node3];
16+
private static readonly string[] Survivors = [McpCallerProtocol.Node1, McpCallerProtocol.Node2];
1417

1518
internal static async Task RunAsync(ClusterFixture fixture, RequestCqrsClusterCallers reader,
1619
KeyLoadClient root, PolicyEpochPublicState state, CancellationToken token)
@@ -27,7 +30,7 @@ await ServerFailureObserver.ObserveAsync(async () =>
2730
stopped.Add(node);
2831
await fixture.KillServerAsync(node, Scenario, token);
2932
}
30-
await PolicyEpochMembership.WaitForSurvivorAsync(fixture, token);
33+
await PolicyEpochMembership.WaitForSurvivorsAsync(fixture, Survivors, token);
3134
await ReadAvailableAsync(reader, state, cursors, token);
3235
await ClusterReplicationTestSupport.MissingCopiesAsync(await root.CommitAsync(state.Seed, token));
3336
var available = await PolicyEpochPublicAssertions.ImageAsync(root, state.Scenario, token);

‎tests/KeyLoad.IntegrationTests/ClientApi/Assertions/Mcp/McpCatalogExpectations.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ internal static ImmutableArray<McpToolExpectation> Entries
118118
new(QueueDispatchName, false, true, true, McpExpectedBody.Object, true,
119119
["tenantId", "databaseId", "queue", "expectedSchemaVersion", "paused"],
120120
["name", "kind", "transactionDomainId", "indexes", "fieldPolicies", "headerPolicies",
121-
"queuePolicy", "eventRetention", "authority", "schemaVersion", "paused", "blobPolicy",
121+
"queuePolicy", "eventRetention", "authority", "schemaVersion", "paused", "filePolicy",
122122
"relationalSchema", "vectorProfiles", "inboxPolicy", "analyticsSchema"],
123123
BodyPropertyFields: ["tenantId", "databaseId", "queue", "expectedSchemaVersion", "paused"]),
124124
new(McpCallerTools.ResourcesConfigure, false, true, false, McpExpectedBody.Object, true,

0 commit comments

Comments
 (0)