Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GraphOS Enterprise: authorization directives #3397

Merged
merged 97 commits into from
Aug 23, 2023
Merged

Conversation

Geal
Copy link
Contributor

@Geal Geal commented Jul 7, 2023

We introduce two new directives, @authenticated and requiresScopes, that define authorization policies for field and types in the supergraph schema.

They are defined as follows:

directive @authenticated on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM

scalar federation__Scope
directive @requiresScopes(scopes: [[federation__Scope!]!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM

They are implemented by hooking the request lifecycle at multiple steps:

  • in query analysis, we extract from the query the list of scopes that would be relevant to authorize the query
  • in a supergraph plugin, we calculate the authorization status and put it in the context: is_authenticated for @authenticated, and the intersection of the query's required scopes and the scopes provided in the token, for @requiresScopes
  • in the query planning phase, we filter the query to remove the fields that are not authorized, then the filtered query goes through query planning
  • at the subgraph level, if query deduplication is active, the authorization status is used to group queries together
  • at the execution service level, the response is formatted according to the filtered query first, which will remove any unauthorized information, then to the shape of the original query, which will propagate nulls as needed
  • at the execution service level, errors are added to the response indicating which fields were removed because they were not authorized

Checklist

Complete the checklist (and note appropriate exceptions) before a final PR is raised.

  • Changes are compatible[^1]
  • Documentation[^2] completed
  • Performance impact assessed and acceptable
  • Tests added and passing[^3]
    • Unit Tests
    • Integration Tests
    • Manual Tests

Exceptions

Note any exceptions here

Notes

[^1]. It may be appropriate to bring upcoming changes to the attention of other (impacted) groups. Please endeavour to do this before seeking PR approval. The mechanism for doing this will vary considerably, so use your judgement as to how and when to do this.
[^2]. Configuration is an important part of many changes. Where applicable please try to document configuration examples.
[^3]. Tick whichever testing boxes are applicable. If you are adding Manual Tests:
- please document the manual testing (extensively) in the Exceptions.
- please raise a separate issue to automate the test and label it (or ask for it to be labeled) as manual test

We introduce two new directives, `@authenticated` and `requiresScopes`,
that define authorization policies for field and types in the
supergraph schema.

They are defined as follows:

```graphql
directive @authenticated on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM

directive @requiresScopes(scopes: [String!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM
```

They are implemented by hooking the request lifecycle at multiple steps:
- in query analysis, we extract from the query the list of scopes that
  would be relevant to authorize the query
- in a supergraph plugin, we calculate the authorization status and put
  it in the context: `is_authenticated` for `@authenticated`, and the
intersection of the query's required scopes and the scopes provided in
the token, for `@requiresScopes`
- in the query planning phase, we filter the query to remove the fields
that are not authorized, then the filtered query goes through query
planning
- at the subgraph level, if query deduplication is active, the
authorization status is used to group queries together
- at the execution service level, the response is formatted according to
  the filtered query first, which will remove any unauthorized
information, then to the shape of the original query, which will
propagate nulls as needed
- at the execution service level, errors are added to the response
  indicating which fields were removed because they were not authorized
@Geal Geal requested a review from StephenBarlow as a code owner July 7, 2023 15:44
@github-actions

This comment has been minimized.

@router-perf
Copy link

router-perf bot commented Jul 7, 2023

CI performance tests

  • reload - Reload test over a long period of time at a constant rate of users
  • no-graphos - Basic stress test, no GraphOS.
  • step - Basic stress test that steps up the number of users over time
  • events_big_cap_high_rate - Stress test for events with a lot of users, deduplication enabled and high rate event with a big queue capacity
  • const - Basic stress test that runs with a constant number of users
  • large-request - Stress test with a 1 MB request payload
  • xxlarge-request - Stress test with 100 MB request payload
  • events - Stress test for events with a lot of users and deduplication ENABLED
  • events_without_dedup - Stress test for events with a lot of users and deduplication DISABLED
  • xlarge-request - Stress test with 10 MB request payload

@Geal Geal requested review from a team, garypen, SimonSapin and bnjjj July 7, 2023 15:53
Copy link
Contributor

@lennyburdette lennyburdette left a comment

Choose a reason for hiding this comment

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

a couple of nits and suggestions in the docs! this is awesome!

They are defined as follows:

```graphql
directive @authenticated on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM
Copy link
Contributor

Choose a reason for hiding this comment

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

Should these be imported from a @link?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

probably yes. We'll update following @trevor-scheer's recommendations

Copy link
Member

Choose a reason for hiding this comment

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

Yes:

@link(url: "https://specs.apollo.dev/federation/v2.5", import: [..., "@authenticated", "@requiresScopes"])

docs/source/configuration/authorization.mdx Outdated Show resolved Hide resolved
docs/source/configuration/authorization.mdx Outdated Show resolved Hide resolved

type User
implements I
@authenticated {
Copy link
Contributor

Choose a reason for hiding this comment

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

this is the only example that applies the directive to a type. should we have a section on "inheritance rules" and how applying these directives to types affect fields across subgraphs? (this is a common footgun in contracts)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think so yes. I'm unsure about the balance in these docs between simple examples and nearly full spec describing edge cases. I think we'll end up documenting all the edge cases here anyway

docs/source/configuration/authorization.mdx Outdated Show resolved Hide resolved
docs/source/configuration/authorization.mdx Outdated Show resolved Hide resolved
Geal and others added 8 commits July 7, 2023 18:31
Co-authored-by: Lenny Burdette <lenny@apollographql.com>
this uncovers an issue with type condition on fragments, fragment
spreads and inline fragments: we should check if the type is authorized
there
if we want rhai or a (future) coprocessor to modify the authorization status at
the supergraph level, then the cache key metadata for authorization
should be set up after those plugins have run
apollo-router/src/plugins/authorization/scopes.rs Outdated Show resolved Hide resolved
apollo-router/src/plugins/authorization/scopes.rs Outdated Show resolved Hide resolved
apollo-router/src/query_planner/plan.rs Outdated Show resolved Hide resolved
docs/source/configuration/authorization.mdx Outdated Show resolved Hide resolved
})
})
});
let query_scopes = context.get_json_value(REQUIRED_SCOPES_KEY).and_then(|v| {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this (and other new entries) should probably go in private entries instead

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this one cannot go in private entries because it is cached by query analysis, and private entries cannot be cloned

paths = filtered_query.format_response(
&mut response,
operation_name,
variables.clone(),
schema.api_schema(),
variables_set,
);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

if a subgraph sets an error at a path of an unauthorized field (ex: that field is used for joins between subgraphs), that error should be removed from the response

}),
Some((query, mut paths)) => {
if query.is_empty() {
return Err(QueryPlannerError::PlanningErrors(PlanErrors {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

do we want to return a query planning error, or return an empty or null data field with an error?

.map_err(|e| SpecError::ParsingError(e.to_string()))?
.to_string();

if visitor.query_requires_authentication {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

maybe make this logic easier to understand (we return Ok(None) if we don't need to modify the query, which means either the query does not contain fields requiring authentication, or we are authenticated and the query does not need to be modified)

.get(condition)
.is_some_and(|ty| self.is_type_authorized(ty));

// FIXME: if a field was removed inside a fragment definition, then we should add an unauthorized path
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this comment should be under fragment_definition

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the transform visitor handles operations before fragments. It should instead check fragments first, then operations. We would then be able to store the error paths per fragment, and at their application point, generate an error path starting from the operation

@Geal Geal requested a review from a team as a code owner July 12, 2023 14:22
Geal and others added 5 commits August 22, 2023 09:25
If authorization directives are not set consistently on all types
implementing an interface, then a query on that interface should use
fragments.

In the same way, if they are not applied consistently on the fields of
the interface, the query should use fragments.

As an example, with this schema:

```graphql
type Query {
    test: String
    itf: I!
}

interface I {
    id: ID
}

type A implements I {
    id: ID
    a: String
}

type B implements I @authenticated {
    id: ID
    b: String
}
```

The query:

```graphql
query {
    test
    itf {
        id
    }
}
```
should be filtered as:
```graphql
query {
    test
}
```

While this one:

```graphql
query {
    test
    itf {
        ... on A {
            id
        }

        ... on B {
            id
        }
    }
}
```

will be filtered as:

 ```graphql
query {
  test
  itf {
    ... on A {
      id
    }
  }
}
```
docs/source/configuration/authorization.mdx Outdated Show resolved Hide resolved
docs/source/configuration/authorization.mdx Outdated Show resolved Hide resolved
docs/source/configuration/authorization.mdx Outdated Show resolved Hide resolved
docs/source/configuration/authorization.mdx Outdated Show resolved Hide resolved
docs/source/configuration/authorization.mdx Outdated Show resolved Hide resolved
docs/source/configuration/authorization.mdx Outdated Show resolved Hide resolved
docs/source/configuration/authorization.mdx Outdated Show resolved Hide resolved
docs/source/configuration/authorization.mdx Outdated Show resolved Hide resolved
docs/source/configuration/authorization.mdx Outdated Show resolved Hide resolved
docs/source/configuration/authorization.mdx Outdated Show resolved Hide resolved
@BrynCooke BrynCooke self-requested a review August 23, 2023 11:24
apollo-router/src/configuration/metrics.rs Outdated Show resolved Hide resolved
Geal and others added 2 commits August 23, 2023 13:33
Co-authored-by: Bryn Cooke <BrynCooke@gmail.com>
@Geal Geal requested a review from BrynCooke August 23, 2023 12:31
@Geal Geal enabled auto-merge (squash) August 23, 2023 12:32
@Geal Geal merged commit dc75fb9 into dev Aug 23, 2023
2 checks passed
@Geal Geal deleted the geal/authorization-directives branch August 23, 2023 12:48
garypen pushed a commit that referenced this pull request Aug 25, 2023
We introduce two new directives, `@authenticated` and `requiresScopes`,
that define authorization policies for field and types in the supergraph
schema.

They are defined as follows:

```graphql
directive @authenticated on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM

scalar federation__Scope
directive @requiresScopes(scopes: [[federation__Scope!]!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM
```

They are implemented by hooking the request lifecycle at multiple steps:
- in query analysis, we extract from the query the list of scopes that
would be relevant to authorize the query
- in a supergraph plugin, we calculate the authorization status and put
it in the context: `is_authenticated` for `@authenticated`, and the
intersection of the query's required scopes and the scopes provided in
the token, for `@requiresScopes`
- in the query planning phase, we filter the query to remove the fields
that are not authorized, then the filtered query goes through query
planning
- at the subgraph level, if query deduplication is active, the
authorization status is used to group queries together
- at the execution service level, the response is formatted according to
the filtered query first, which will remove any unauthorized
information, then to the shape of the original query, which will
propagate nulls as needed
- at the execution service level, errors are added to the response
indicating which fields were removed because they were not authorized

---------

Co-authored-by: Lenny Burdette <lenny@apollographql.com>
Co-authored-by: Maria Elisabeth Schreiber <maria.schreiber@apollographql.com>
Co-authored-by: Lucas Leadbetter <5595530+lleadbet@users.noreply.github.com>
Co-authored-by: Simon Sapin <simon@apollographql.com>
Co-authored-by: Chandrika Srinivasan <chandrikas@users.noreply.github.com>
Co-authored-by: Bryn Cooke <BrynCooke@gmail.com>
@Geal Geal mentioned this pull request Sep 4, 2023
@abernix abernix mentioned this pull request Sep 4, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants