Skip to content

feat(router): custom authorization rules with the @policy directive - #1369

Open
rafagsiqueira wants to merge 1 commit into
graphql-hive:mainfrom
rafagsiqueira:feat/policy-directive-coprocessor
Open

feat(router): custom authorization rules with the @policy directive#1369
rafagsiqueira wants to merge 1 commit into
graphql-hive:mainfrom
rafagsiqueira:feat/policy-directive-coprocessor

Conversation

@rafagsiqueira

Copy link
Copy Markdown
Contributor

Closes #1134

Adds support for the federation @policy directive, letting a coprocessor decide custom authorization rules the router cannot evaluate on its own.

How it works

The router publishes what needs deciding, the coprocessor decides, the router enforces:

  1. Collect — before the graphql.analysis stage, collect_required_policies walks the operation (reusing OperationFilter, so @skip/@include are honored) and writes every policy it depends on to hive::authorization::required_policies. Router-owned: a coprocessor writing to it fails the request.
  2. Decide — the coprocessor answers with hive::authorization::granted_policies. Anything omitted is denied, so an absent or empty answer grants nothing.
  3. Enforceenforce_operation_authorization now runs after the analysis stage and feeds the granted policies into the existing filter/reject machinery.

@policy(policies: [[...]]) takes an OR of AND groups, the same shape as @requiresScopes. Access is granted when every policy of at least one group is granted.

Unauthorized fields go through the existing authorization.directives.unauthorized.mode setting: filter (default) nulls them and reports UNAUTHORIZED_FIELD_OR_TYPE, reject fails the operation. As with the other authorization directives, subgraph requests that would only resolve unauthorized fields are never sent — confirmed in the e2e logs.

Example coprocessor answer for the graphql.analysis stage:

{
  "version": 1,
  "control": "continue",
  "context": {
    "hive::authorization::granted_policies": ["read_profile"]
  }
}

Notable decisions

Granted-policies set rather than Apollo's policy -> null|true|false map. It mirrors the existing progressive_override domain (unresolved_labels / labels_to_override) and collapses to the same semantics — anything not granted is denied.

AuthorizationRule is now a struct instead of an enum. The previous Authenticated | RequiresScopes enum could not express a field carrying both @requiresScopes and @policy. It now holds authenticated / scopes / policies parts that must all be satisfied. This also fixed the union cross-product to combine scopes and policies independently.

@policy is enforced without JWT. The old !jwt.enabled → return early short-circuit would have disabled policies too. It is narrowed so that when JWT is off, @authenticated/@requiresScopes remain unenforced exactly as before (via the new enforce_jwt_rules argument), while @policy still applies.

Tests

  • 12 unit tests (policy_directive): OR-of-ANDs, type-level vs field-level policies, @policy + @authenticated combined, unknown policies ignored, and required-policy collection including @skip exclusion.
  • 7 e2e tests against a new e2e/supergraph-policy.graphql, driving a real mock coprocessor — including the reserved-key rejection and the no-policies-published case.

cargo test_all 1246 passed · cargo test_e2e 630 passed · cargo test_plugin_examples 28 passed · cargo fmt --check clean · no new clippy findings in changed files.

Not included

Scoped to coprocessors as requested. The plugin-side write API (RequestContextAuthorizationWrite) exists and is gated to the OnGraphqlAnalysis hook for symmetry with the other context domains, but there is no plugin example or plugin-specific test for it.

🤖 Generated with Claude Code

Adds support for the federation `@policy` directive, evaluated by a coprocessor
in the `graphql.analysis` stage.

The router publishes the policies an operation depends on to the request context
under `hive::authorization::required_policies` (read-only), the coprocessor
answers with the subset it grants in `hive::authorization::granted_policies`,
and authorization enforcement then runs against that decision. Policies left out
of the answer are denied, so an absent or empty answer grants nothing.

Unauthorized fields go through the existing
`authorization.directives.unauthorized.mode` handling, and subgraph requests
that would only resolve unauthorized fields are still never sent.

`AuthorizationRule` becomes a struct instead of an enum so a field can carry
`@authenticated`, `@requiresScopes` and `@policy` at once, all of which must be
satisfied. `@policy` is independent of JWT authentication and stays enforced
even when JWT is not configured.

Closes graphql-hive#1134

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

Custom authorization rules using @policy directive

2 participants