Skip to content

Conversation

@dariuszkuc
Copy link
Member

Current merge policies for @authenticated, @requiresScopes and @policy were inconsistent.

If single subgraph declared a field with one of the directives then it would restrict access to this supergraph field regardless which subgraph would resolve this field (results in AND rule for any applied auth directive, i.e. @authenticated AND @policy is required to access this field). If the same auth directive (@requiresScopes/@policy) were applied across the subgraphs then the resulting supergraph field could be resolved by fullfilling either one of the subgraph requirements (resulting in OR rule, i.e. either @policy 1 or @policy 2 has to be true to access the field). While arguably this allowed for easier schema evolution, it did result in weakening the security requirements.

Since @policy and @requiresScopes values are represent boolean conditions in Disjunctive Normal Form, we can merge them conjunctively to get the final auth requirements, i.e.

type T @authenticated {
  # requires scopes (A1 AND A2) OR A3
  secret: String @requiresScopes(scopes: [["A1", "A2"], ["A3"]])
}

type T {
  # requires scopes B1 OR B2
  secret: String @requiresScopes(scopes: [["B1"], ["B2"]]
}

type T @authenticated {
  secret: String @requiresScopes(
    scopes: [
      ["A1", "A2", "B1"],
      ["A1", "A2", "B2"],
      ["A3", "B1"],
      ["A3", "B2"]
    ])
}

This algorithm also deduplicates redundant requirements, e.g.

type T {
  # requires A1 AND A2 scopes to access
  secret: String @requiresScopes(scopes: [["A1", "A2"]])
}

type T {
  # requires only A1 scope to access
  secret: String @requiresScopes(scopes: [["A1"]])
}

type T {
  # requires only A1 scope to access as A2 is redundant
  secret: String @requiresScopes(scopes: [["A1"]])
}

Partial backport of apollographql/federation#3321 and apollographql/federation#3343

Current merge policies for `@authenticated`, `@requiresScopes` and `@policy` were inconsistent.

If single subgraph declared a field with one of the directives then it would restrict access to this supergraph field regardless which subgraph would resolve this field (results in AND rule for any applied auth directive, i.e. `@authenticated` AND `@policy` is required to access this field). If the same auth directive (`@requiresScopes`/`@policy`) were applied across the subgraphs then the resulting supergraph field could be resolved by fullfilling either one of the subgraph requirements (resulting in OR rule, i.e. either `@policy` 1 or `@policy` 2 has to be true to access the field). While arguably this allowed for easier schema evolution, it did result in weakening the security requirements.

Since `@policy` and `@requiresScopes` values are represent boolean conditions in Disjunctive Normal Form, we can merge them conjunctively to get the final auth requirements, i.e.

```graphql
type T @authenticated {
  # requires scopes (A1 AND A2) OR A3
  secret: String @requiresScopes(scopes: [["A1", "A2"], ["A3"]])
}

type T {
  # requires scopes B1 OR B2
  secret: String @requiresScopes(scopes: [["B1"], ["B2"]]
}

type T @authenticated {
  secret: String @requiresScopes(
    scopes: [
      ["A1", "A2", "B1"],
      ["A1", "A2", "B2"],
      ["A3", "B1"],
      ["A3", "B2"]
    ])
}
```

This algorithm also deduplicates redundant requirements, e.g.

```graphql
type T {
  # requires A1 AND A2 scopes to access
  secret: String @requiresScopes(scopes: [["A1", "A2"]])
}

type T {
  # requires only A1 scope to access
  secret: String @requiresScopes(scopes: [["A1"]])
}

type T {
  # requires only A1 scope to access as A2 is redundant
  secret: String @requiresScopes(scopes: [["A1"]])
}
```

<!-- FED-853 -->

Partial backport of apollographql/federation#3321 and apollographql/federation#3343
@dariuszkuc dariuszkuc requested review from a team as code owners January 21, 2026 00:05
@apollo-librarian
Copy link

apollo-librarian bot commented Jan 21, 2026

✅ Docs preview has no changes

The preview was not built because there were no changes.

Build ID: d6233b59d5942d803a402cee
Build Logs: View logs

@github-actions
Copy link
Contributor

@dariuszkuc, please consider creating a changeset entry in /.changesets/. These instructions describe the process and tooling.

if let Value::List(disjunctions) = value {
disjunctions.iter_mut().for_each(|disjunction| {
if let Value::List(conjunctions) = disjunction.make_mut() {
// TODO should this also filter duplicates? ["A", "A"]?
Copy link
Member Author

Choose a reason for hiding this comment

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

arguably this should be extremely rare so probably doesn't matter

@dariuszkuc
Copy link
Member Author

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.

2 participants