Added policy interfaces - #10008
Open
michaelstaib wants to merge 87 commits into
Open
Added policy interfaces#10008michaelstaib wants to merge 87 commits into
michaelstaib wants to merge 87 commits into
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR introduces first-class support for authorization policy directives in Fusion composition and execution by (1) merging user @policy directives (including interface-to-implementor propagation), (2) stamping them into @fusion__policy in the composed schema, and (3) exposing parsed policy applications on Fusion object/field definitions for execution-time use.
Changes:
- Add composition support for
@policy(merge + dedupe + propagate from interfaces) and stamp results as@fusion__policy. - Add execution schema support for parsing/storing
@fusion__policyapplications on object types and fields, plus a schema-levelHasPoliciesflag. - Introduce policy runtime interfaces (
IAuthorizationPolicy/IAuthorizationContext) and add targeted tests in both composition and execution layers.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Execution/Types/PolicyApplicationsTests.cs | Adds execution-layer tests validating parsing/storage of @fusion__policy applications and HasPolicies. |
| src/HotChocolate/Fusion/test/Fusion.Composition.Tests/SourceSchemaMerger.PolicyDirective.Tests.cs | Adds composition-layer tests for stamping, interface propagation, and deduping behavior. |
| src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Policies/IAuthorizationPolicy.cs | Introduces policy evaluation interfaces and an entity container for policy evaluation. |
| src/HotChocolate/Fusion/src/Fusion.Execution.Types/PolicyDenialBehavior.cs | Adds runtime enum describing denial behavior semantics (Null/Error/Abort). |
| src/HotChocolate/Fusion/src/Fusion.Execution.Types/PolicyApplication.cs | Adds runtime model representing a single policy application on a coordinate. |
| src/HotChocolate/Fusion/src/Fusion.Execution.Types/FusionSchemaDefinition.cs | Adds HasPolicies and computes it during schema sealing based on stored applications. |
| src/HotChocolate/Fusion/src/Fusion.Execution.Types/FusionOutputFieldDefinition.cs | Adds PolicyApplications to output field definitions and wires it from completion context. |
| src/HotChocolate/Fusion/src/Fusion.Execution.Types/FusionObjectTypeDefinition.cs | Adds PolicyApplications to object type definitions and wires it from completion context. |
| src/HotChocolate/Fusion/src/Fusion.Execution.Types/FusionBuiltIns.cs | Adds fusion__policy built-in directive name constant. |
| src/HotChocolate/Fusion/src/Fusion.Execution.Types/Completion/CompositeSchemaBuilder.cs | Parses @fusion__policy directives into PolicyApplication collections during completion. |
| src/HotChocolate/Fusion/src/Fusion.Execution.Types/Completion/CompositeOutputFieldCompletionContext.cs | Extends field completion context to carry policy applications. |
| src/HotChocolate/Fusion/src/Fusion.Execution.Types/Completion/CompositeObjectTypeCompletionContext.cs | Extends object completion context to carry policy applications. |
| src/HotChocolate/Fusion/src/Fusion.Composition/WellKnownTypeNames.cs | Adds well-known type names for policy denial behavior (user + fusion enum). |
| src/HotChocolate/Fusion/src/Fusion.Composition/WellKnownDirectiveNames.cs | Adds well-known directive names for policy and fusion__policy. |
| src/HotChocolate/Fusion/src/Fusion.Composition/WellKnownArgumentNames.cs | Adds onDenied argument constant for policy directives. |
| src/HotChocolate/Fusion/src/Fusion.Composition/SourceSchemaMerger.cs | Implements policy directive merging, interface-policy propagation, and stamping to @fusion__policy; adds fusion enum/ ನಿರ್ದೇಶive definitions. |
| src/HotChocolate/Fusion/src/Fusion.Composition/Directives/PolicyDirective.cs | Adds parsed representation of user @policy directives. |
| src/HotChocolate/Fusion/src/Fusion.Composition/DirectiveMergers/PolicyDirectiveMerger.cs | Adds merger logic: collect, dedupe by name, choose “max” denial behavior. |
| src/HotChocolate/Fusion/src/Fusion.Composition/Definitions/PolicyMutableDirectiveDefinition.cs | Defines canonical user @policy directive (repeatable, object/interface/field). |
| src/HotChocolate/Fusion/src/Fusion.Composition/Definitions/PolicyDenialBehaviorMutableEnumTypeDefinition.cs | Defines user-facing PolicyDenialBehavior enum for @policy. |
| src/HotChocolate/Fusion/src/Fusion.Composition/Definitions/FusionPolicyMutableDirectiveDefinition.cs | Defines @fusion__policy directive for composed schema stamping. |
| src/HotChocolate/Fusion/src/Fusion.Composition/Definitions/FusionPolicyDenialBehaviorMutableEnumTypeDefinition.cs | Defines fusion__PolicyDenialBehavior enum for @fusion__policy. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+1517
to
+1539
| private static void AddFusionPolicyDirectives( | ||
| IDirectivesProvider member, | ||
| IReadOnlyList<PolicyDirective> policies, | ||
| MutableDirectiveDefinition directiveDefinition) | ||
| { | ||
| foreach (var policy in policies) | ||
| { | ||
| var arguments = new List<ArgumentAssignment> | ||
| { | ||
| new(ArgumentNames.Name, policy.Name) | ||
| }; | ||
|
|
||
| if (policy.OnDenied != "NULL") | ||
| { | ||
| arguments.Add( | ||
| new ArgumentAssignment( | ||
| ArgumentNames.OnDenied, | ||
| new EnumValueNode(policy.OnDenied))); | ||
| } | ||
|
|
||
| member.AddDirective(new Directive(directiveDefinition, arguments)); | ||
| } | ||
| } |
# Conflicts: # src/HotChocolate/Fusion/src/Fusion.Composition/SourceSchemaMerger.cs # src/HotChocolate/Fusion/src/Fusion.Composition/WellKnownTypeNames.cs
…ed orchestrator output)
# Conflicts: # .gitignore # AGENTS.md # CLAUDE.md # src/Directory.Packages.props # src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/ApolloParityBenchmark.cs # src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/CorpusParsingBenchmark.cs # src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/CorpusPlanningBenchmark.cs # src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/CorpusPlanningProbe.cs # src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/DbRowWriteBenchmark.cs # src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/DocumentRewriterBenchmark.cs # src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/FusionBenchmarkBase.cs # src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/GraphQLQueryBenchmark.cs # src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/GraphQLServerHelper.cs # src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/HashCodeBenchmark.cs # src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/InlineFragmentOperationRewriterBenchmark.cs # src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/LockStoreBenchmark.cs # src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/OperationCompilerBenchmark.cs # src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/OperationPlannerBenchmark.cs # src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/Program.cs # src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/VariableMergingBenchmark.cs # src/HotChocolate/Fusion/src/Fusion.Execution/Diagnostics/IFusionExecutionDiagnosticEvents.cs # src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Nodes/OperationCompiler.cs # src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Nodes/OperationPlan.cs # src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Nodes/Selection.cs # src/HotChocolate/Fusion/src/Fusion.Execution/Execution/OperationPlanContext.Pooling.cs # src/HotChocolate/Fusion/src/Fusion.Execution/HotChocolate.Fusion.Execution.csproj # src/HotChocolate/Fusion/src/Fusion.Execution/Planning/OperationPlanner.BuildExecutionTree.cs # src/HotChocolate/Fusion/src/Fusion.Execution/Properties/FusionExecutionResources.Designer.cs # src/HotChocolate/Fusion/src/Fusion.Execution/Properties/FusionExecutionResources.resx # src/HotChocolate/Fusion/src/Fusion.Execution/Types/Completion/SemanticIntrospectionSchema.cs # src/HotChocolate/Fusion/src/Fusion.Execution/Types/FusionObjectTypeDefinition.cs # src/HotChocolate/Fusion/src/Fusion.Execution/Types/FusionOutputFieldDefinition.cs # src/HotChocolate/Fusion/src/Fusion.Execution/Types/ScalarResultType.cs # src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Execution/OperationCompilerTests.cs
VerifySignatureAsync accepted any certificate whose issuer and serial number matched a trusted certificate, because X509Certificate2Collection.Contains compares only those two fields. A forged certificate carrying its own key pair but the same subject and serial number as a trusted signer passed verification. Membership is now checked by comparing the raw certificate bytes of every signer against the trusted collection. Also close the empty-collection gap in FileSystemFusionConfigurationProvider: a configured but empty TrustedSigningCertificates collection was treated as no trust root, silently accepting unsigned archives instead of rejecting everything.
The GetPolicyParentDependencies borrowing predicate now accepts a lift whose downstream operation merely survived in the same rewritten incremental-plan part, rather than requiring it to be paired with the policy node, provided the lift was recorded against the immediate parent scope, its requirement exactly provides the full response leaf, and its own parent step is itself an immediate-parent provider of that leaf. This let the planner produce a non-empty parent-dependency closure for a query shape where an outer @defer has no fields of its own (its lifted fetch is hoisted straight into the root part and it never materializes its own IncrementalPlan), which in turn exposed a pre-existing gap in PolicyArtifactBinder.TryResolveParentPlanScope: it only fell back to the root scope when the child's own DeliveryGroup had no parent, not when the referenced parent group itself had none and materialized no plan. TryResolveParentPlanScope now also resolves to the root scope in that case; a parent group nested one level deeper that still materialized no plan remains a known limitation and keeps failing closed.
Introduces a v2 Rego policy format (policies/rego/2.0.0/) alongside the existing flat policy-pair format: a manifest.json indexes every policy package, shared library module, and the root data mount by canonical relative path and sha256 digest, cross-checked against a scan of each module's declared package and its # METADATA / entrypoint: true rules. Unlisted, missing, duplicate, or tampered payloads are rejected at read. FusionArchive gains SetRegoPolicyBundleAsync/GetRegoPolicyBundleAsync as the single scanner/validator boundary shared by the reader and the new `nitro fusion policy pack <root> --out <far|dir>` CLI command. Version dispatch is based on the presence of a nested manifest.json rather than the literal version number, so the existing flat-pair format keeps using arbitrary version numbers of its own. PackagePolicyContentReader gains a v2 branch and now rejects an archive whose only Rego formats exceed what the runtime supports (fail closed) instead of silently serving the schema with no policies; a runtime built before this change still returns null for such an archive. PolicyContentSnapshot gains a separate library/module catalog, and RegoPolicyProvider compiles it into every policy set without ever treating a library as a decision or falling back to a synthetic '<name>.allow' policy for it. RegoEntrypointScanner moves from Fusion.Policies.Rego to Fusion.Packaging so the bundle manifest validator and the Rego policy provider share one decision-discovery implementation instead of two.
F3: a malformed manifest (missing required property, duplicate sha256 key) now surfaces InvalidDataException via ThrowHelper instead of an uncaught KeyNotFoundException or ArgumentException from the raw JsonElement/ImmutableSortedDictionary calls. F4: read-time path collisions (a module and a library, or two packages, claiming the same path) now throw InvalidDataException instead of the write-time ArgumentException they were reusing. Added tests for manifest path traversal, both path-collision shapes, and a tampered lib/*.rego or data/data.json whose manifest hash was never updated. F5: the CLI's --out <dir> extraction now goes entirely through IFileSystem instead of raw File/Directory calls, and is covered by Pack_Should_WriteUnpackedBundle_When_OutIsDirectory. Fixed a latent bug this surfaced: a package with no <package>.graphql file was packed with a non-null, zero-length Requirements value (the byte[]-to- ReadOnlyMemory<byte> implicit conversion turns a null array into an empty struct before it reaches the nullable wrapper) instead of null, which failed the bundle's own requirements-prefix check on write. F6: dropped the unused WellKnownVersions.RegoPolicyPairFormatVersion. RegoEntrypointScanner goes back to internal, visible to Fusion.Policies.Rego and its tests via InternalsVisibleTo, since ruling 699 keeps the scanner in Fusion.Packaging without asking for a public surface. F7: SetRegoPolicyAsync and SetRegoDataAsync now reject a format version that already carries a manifest-indexed bundle, so a flat write can never land inside a v2 bundle directory. F8: split the two over-5-assert tests (the bundle round-trip and the bundle read) into one focused test per concern instead of asserting package, library, and data all in one test body.
Closes the six residual minors from zh8's final review (repo-ctf.25): - provider collisions with no last-good snapshot are now reported as RegoDataProviderException(providerName) instead of a bare RegoDataMergeException, for both the single- and multi-provider case - the FAR-publish compile precheck disposes its (never served) CompiledPolicySet instead of leaking it - a provider refresh that leaves the served (FAR, providers) combination unchanged no longer triggers a redundant recompile and republish - ProviderState's members are internal rather than public, matching the narrowest visibility this design supports
…e event message Lifts the 6fl blanket rejection for a policy directly on a subscription root's own (concrete) EventStream payload type when its requirement is derivable from the composed @eventstream message projection. Such an application is folded into the existing per-event slot path instead of a PolicyExecutionNode (EventStream can never be a producer, provider or dependency of one), and evaluated against the event's own materialized root element once merged into the shared context. Non-derivable requirements keep hitting a narrowed, specific rejection; nested/non-root subscription-field policies and abstract payload types are unchanged.
…e coverage A list-typed subscription root can carry more than one resource per event, so GetConcreteEventStreamMessage and PolicyArtifactBinder's event-resource coordinate check now exclude it, keeping the old unnarrowed rejection instead of silently deriving from the message projection; PolicyRequestState also denies outright (instead of leaving the decision undecided) if the per-event slot path ever sees more than one root entity. Adds the missing negative tests: JSON mutations of the round-trip plan (dropped requirements, changed selectionSet, unrelated policy name) and a message mutation that only the binder's independent derivability check catches, plus the list-typed root rejection test. Amends IsRequirementCoveredByEventMessage's doc to note the inline-fragment branch is currently unreachable.
…vent resource policy
A subscription event's per-event reset rebuilt the result document but left FetchResultStore's cached policy-denial state (deny flags, plan, plan part) from the previous event in place. AddPartialResults merges the next event's data before that event's own policy re-evaluation refreshes this state, so ValueCompletion.ApplyPolicyDenials replayed the prior event's denial against the new, not-yet-decided root: for ABORT this nulled the fresh root outright, crashing the next event's own per-event resource read with "Expected StartObject but found Null"; for ERROR/NULL it silently re-denied an event that should have been allowed. Reset now clears this state so each event starts undecided until it is actually evaluated. Adds 3-event regressions for ABORT and ERROR (allowed, denied, allowed) proving the third event is no longer corrupted by the second, and trims two existing test comments and one over-length test name flagged in review.
An action policy's decision depends on its own occurrence's arguments, so two occurrences that share a field's shape (type, field, root-ness) but differ by parent must never be aggregated by response name alone - two distinct occurrences (e.g. the same field under different parents) can share a response name. PolicyArtifactBinder.MatchesCoordinate and MatchesCoordinatePosition now match an action coordinate by its first occurrence's compiled position instead. OperationPlan.ValidatePolicyArtifacts follows suit: the gate-identity fold and the per-slot field-coordinate uniqueness check key on occurrence position rather than response names, so the fix above does not immediately fail the duplicate-gate-identity check it was previously masking. OperationPlanner's coordinate loop now rejects an action slot that overflows the 64-gate table with the documented "cannot contain more than 64 policy gates" error instead of silently falling back to a residual (post-fetch) PolicyExecutionNode target, which cannot carry action input and would let a denied mutation's fetch run. PolicyRequestState now evaluates a subscription-root action expression once, at the initial (pre-subscribe) request-level pass, and replays that decision for every subsequent per-event re-evaluation instead of re-running it once per event; non-event passes (including every variable-batch item) never read the replay cache, so variable-batch isolation is unaffected. Adds regression coverage: planning and gateway tests for the shared response-name aliasing fix, a 64/65-alias planning pair for the gate-cap message, gateway defer and subscription tests for action policies (including the once-per-stream evaluation count), and an HTTP-transport gateway test in Fusion.AspNetCore.Tests confirming a denied mutation's source is never invoked while an allowed one is.
…on decisions per name The planner's action-slot gate disambiguator was keyed on TypeName/FieldName/ResponseNames plus the registry's own allocation length, a term that can never match an existing entry, so the "same occurrence seen again" merge branch was unreachable. Key it on the occurrence's compiled position (selection set + coordinate path + occurrence ordinal) instead, supplied by the BuildPolicyTargets occurrence loop, mirroring the binder's OccurrenceOrdinal. PolicyRequestState cached a mixed action/non-action expression's whole decision keyed by (slot, coordinate, expression) and replayed it for every subscription per-event pass, so a non-action name sharing the expression with an action name was silently stuck on its first decision instead of being re-evaluated per event. Cache per action name instead, and always route non-action names through EvaluatePolicyOnceAsync so they see every pass.
# Conflicts: # src/HotChocolate/Fusion/src/Fusion.Execution/Execution/FusionRequestExecutorManager.cs # src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Execution/FusionOptionsTests.cs
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.