From 2bc54ed183d7bae842eb74caf2924c37150993a4 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Tue, 28 Nov 2023 02:08:47 -0700 Subject: [PATCH] docs: add back @policy directive (#4249) Adds (back) `@policy` docs as GA. Co-authored-by: Dylan Anthony Co-authored-by: Geoffroy Couprie Co-authored-by: Chandrika Srinivasan Co-authored-by: Edward Huang Co-authored-by: Edward Huang <18322228+shorgi@users.noreply.github.com> --- CHANGELOG.md | 4 +- docs/source/config.json | 3 +- docs/source/configuration/authorization.mdx | 222 ++++++++++++++++++-- docs/source/enterprise-features.mdx | 2 +- 4 files changed, 215 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fde22ecc4a..36ccdafe9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -158,12 +158,14 @@ By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/p > > If your organization doesn't currently have an Enterprise plan, you can test out this functionality by signing up for a free Enterprise trial. -> The `@policy` directive requires using a federation version not yet available at the time of router release `1.34.0`. +> The `@policy` directive requires using a [federation version `2.6`](https://www.apollographql.com/docs/federation/federation-versions). We introduce a new GraphOS authorization directive called `@policy` that is designed to offload authorization policy execution to a coprocessor or Rhai script. When executing an operation, the relevant policy will be determined based on `@policy` directives in the schema. The coprocessor or Rhai script then indicates which of those policies requirements are not met. Finally, the router filters out fields which are unauthorized in the same way it does when using `@authenticated` or `@requiresScopes` before executing the operation. +For more information, see the [documentation](https://www.apollographql.com/docs/router/configuration/authorization#authorization-directives). + By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/3751 ### Authorization directives are enabled by default ([Issue #3842](https://github.com/apollographql/router/issues/3842)) diff --git a/docs/source/config.json b/docs/source/config.json index 11b9dc2fd7..32edc0b399 100644 --- a/docs/source/config.json +++ b/docs/source/config.json @@ -45,8 +45,7 @@ "Authorization": [ "/configuration/authorization", [ - "enterprise", - "preview" + "enterprise" ] ], "Subgraph Authentication": "/configuration/authn-subgraph", diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 92bf836b55..843d380984 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -1,13 +1,10 @@ --- title: Authorization in the Apollo Router description: Strengthen service security with a centralized governance layer -minVersion: 1.29.1 --- - - APIs provide access to business-critical data. Unrestricted access can result in data breaches, monetary losses, or potential denial of service. Even for internal services, checks can be essential to limit data to authorized parties. Services may have their own access controls, but enforcing authorization _in the Apollo Router_ is valuable for a few reasons: @@ -70,6 +67,7 @@ The Apollo Router provides access controls via **authorization directives** that - The [`@requiresScopes`](#requiresscopes) directive allows granular access control through the scopes you define. - The [`@authenticated`](#authenticated) directive allows access to the annotated field or type for _authenticated requests only_. +- The [`@policy`](#policy) directive offloads authorization validation to a [Rhai script](../customizations/rhai/) or a [coprocessor](../customizations/coprocessor) and integrates the result in the router. It's useful when your authorization policies go beyond simple authentication and scopes. For example, imagine you're building a social media platform that includes a `Users` subgraph. You can use the [`@requiresScopes`](#requiresscopes) directive to declare that viewing other users' information requires the `read:user` scope: @@ -101,7 +99,7 @@ Only the Apollo Router supports authorization directives—[`@apollo/gateway Before using the authorization directives in your subgraph schemas, you must: - Validate that your Apollo Router uses version `1.29.1` or later and is [connected to your GraphOS Enterprise organization](../enterprise-features/#enabling-enterprise-features) -- Include **[claims](#configure-request-claims)** in requests made to the router +- Include **[claims](#configure-request-claims)** in requests made to the router (for `@authenticated` and `@requiresScopes`) ### Configure request claims @@ -115,16 +113,20 @@ To provide the router with the claims it needs, you must either configure JSON W ## Authorization directives -While in [preview](/resources/product-launch-stages/#preview), authorization directives are turned off by default. To enable them, include the following in your router's [YAML config file](./overview/): +Authorization directives are turned on by default. To disable them, include the following in your router's [YAML config file](./overview/): ```yaml title="router.yaml" authorization: - preview_directives: - enabled: true + directives: + enabled: false ``` + + ### `@requiresScopes` + + The `@requiresScopes` directive marks fields and types as restricted based on required scopes. The directive includes a `scopes` argument with an array of the required scopes to declare which scopes are required: @@ -279,8 +281,12 @@ query { The response would include an error at the `/users/@/email` path since that field requires the `read:emails` scope. The router can execute the entire query successfully if the request includes the `read:others read:emails` scope set. + + ### `@authenticated` + + The `@authenticated` directive marks specific fields and types as requiring authentication. It works by checking for the `apollo_authentication::JWT::claims` key in a request's context, that is added either by the JWT authentication plugin, when the request contains a valid JWT, or by an authentication coprocessor. If the key exists, it means the request is authenticated, and the router executes the query in its entirety. @@ -412,9 +418,201 @@ The response retains the initial request's shape but returns `null` for unauthor If _every_ requested field requires authentication and a request is unauthenticated, the router generates an error indicating that the query is unauthorized. + + +### `@policy` + + + +The `@policy` directive marks fields and types as restricted based on authorization policies evaluated in a [Rhai script](../customizations/rhai/) or [coprocessor](../customizations/coprocessor). This enables custom authorization validation beyond authentication and scopes. It is useful when we need more complex policy evaluation than verifying the presence of a claim value in a list (example: checking specific values in headers). + +The `@policy` directive includes a `policies` argument that defines an array of the required policies. The following example shows a policy that requires all roles from the claims object to contain the string `"support"`: + +```graphql +@policy(policies: [["claims[`roles`].contains(`support`)"]]) +``` + +At the [`RouterService` level](../customizations/overview#the-request-lifecycle), the Apollo Router extracts the list of policies relevant to a request from the schema and then stores them in the request's context in `apollo_authorization::policies::required` as a map `policy -> null|true|false`. + +At the `SupergraphService` level, you must provide a Rhai script or coprocessor to evaluate the map. If the policy is validated, the script or coprocessor should set its value to `true` or otherwise set it to `false`. If the value is left to `null`, it will be treated as `false` by the router. Afterward, the router filters the requests' types and fields to only those where the policy is `true`. + +If no field of a subgraph query passes its authorization policies, the router stops further processing of the query and precludes unauthorized subgraph requests. This efficiency gain is a key benefit of the `@policy` and other authorization directives. + +#### Usage + +To use the `@policy` directive in a subgraph, you can [import it from the `@link` directive](/federation/federated-types/federated-directives/#importing-directives) like so: + +```graphql +extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.6", + import: [..., "@policy"]) +``` + +The `@policy` directive is defined as follows: + +```graphql +scalar federation__Policy +directive @policy(policies: [[federation__Policy!]!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM +``` + +Using the `@policy` directive requires a [Supergraph plugin](../customizations/overview) to evaluate the authorization policies. You can do this with a [Rhai script](../customizations/rhai/) or [coprocessor](../customizations/coprocessor). Refer to the following [Rhai script](#usage-with-a-rhai-script) and [coprocessor](#usage-with-a-coprocessor) examples for more information. (Although a [native plugin](../customizations/native) can also evaluate authorization policies, we don't recommend using it.) + +#### Combining policies with `AND`/`OR` logic + +Authorization validation uses **AND** logic between the elements in the inner-level `policies` array, where a request must include _all_ elements in the inner-level `policies` array to resolve the associated field or type. For the following example, a request would need `policy1` **AND** `policy2` **AND** `policy3` to be authorized: + +```graphql +@policy(policies: [["policy1", "policy2", "policy3"]]) +``` + +Alternatively, to introduce **OR** logic you can use nested arrays. For the following example, a request would need `policy1` **OR** `policy2` **OR** `policy3` to be authorized: + +```graphql +@policy(policies: [["policy1"], ["policy2"], ["policy3"]]) +``` + +You can nest arrays and elements as needed to achieve your desired logic. For the following example, its syntax requires requests to have either (`policy1` **AND** `policy2`) **OR** just `policy3` to be authorized: + +```graphql +@policy(policies: [["policy1", "policy2"], ["policy3"]]) +``` + +##### Usage with a Rhai script + +The `policies` argument contains a list of strings with no formatting constraints, meaning you can use them to store Rhai code. + + + +As an example, the following schema defines policies as boolean expressions that can be evaluated in Rhai: + +```graphql +type Query { + me: User @policy(policies: [["kind:user"]]) +} + +type User { + id: ID! + username: String + username: String @policy(policies: [["roles:support"]]) +} +``` + +You can then use the following Rhai script to evaluate the policies: + +``` +fn supergraph_service(service) { + let request_callback = |request| { + let claims = request.context["apollo_authentication::JWT::claims"]; + let policies = request.context["apollo_authorization::policies::required"]; + + if policies != () { + for key in policies.keys() { + let array = key.split(":"); + if array.len == 2 { + switch array[0] { + "kind" => { + policies[key] = claims[`kind`] == array[1]; + } + "roles" => { + policies[key] = claims[`roles`].contains(array[1]); + } + _ => {} + } + } + } + } + request.context["apollo_authorization::policies::required"] = policies; + }; + service.map_request(request_callback); +} +``` + + + +##### Usage with a coprocessor + +You can use a [coprocessor](../customizations/coprocessor) called at the Supergraph request stage to receive and execute the list of policies. This is useful to bridge router authorization with an existing authorization stack or link policy execution with lookups in a database. + + + +Suppose you only want a user with a `read_profile` policy to have access to their own information. An additional policy `read_credit_card` is required to access credit card information. Your schema may look something like this: + +```graphql +type Query { + me: User @policy(policies: [["read_profile"]]) +} + +type User { + id: ID! + username: String + credit_card: String @policy(policies: [["read_credit_card"]]) +} +``` + +If you configure your router like this: + +```yaml title="router.yaml" +coprocessor: + url: http://127.0.0.1:8081 + supergraph: + request: + context: true +``` + +A coprocessor can then receive a request with this format: + +```json +{ + "version": 1, + "stage": "SupergraphRequest", + "control": "continue", + "id": "d0a8245df0efe8aa38a80dba1147fb2e", + "context": { + "entries": { + "apollo_authentication::JWT::claims": { + "exp": 10000000000, + "sub": "457f6bb6-789c-4e8b-8560-f3943a09e72a" + }, + "apollo_authorization::policies::required": { + "read_profile": null, + "read_credit_card": null + } + } + }, + "method": "POST" +} +``` + +A user can read their own profile, so `read_profile` will succeed. But only the billing system should be able to see the credit card, so `read_credit_card` will fail. The corpocessor will then return: + + +```json +{ + "version": 1, + "stage": "SupergraphRequest", + "control": "continue", + "id": "d0a8245df0efe8aa38a80dba1147fb2e", + "context": { + "entries": { + "apollo_authentication::JWT::claims": { + "exp": 10000000000, + "sub": "457f6bb6-789c-4e8b-8560-f3943a09e72a" + }, + "apollo_authorization::policies::required": { + "read_profile": true, + "read_credit_card": false + } + } + } +} +``` + + + ## Composition and federation -GraphOS's composition strategy for authorization directives is intentionally accumulative. When you define authorization directives on fields and types in subgraphs, GraphOS composes them into the supergraph schema. In other words, if subgraph fields or types include `@requiresScopes` or `@authenticated` directives, they are set on the supergraph too. +GraphOS's composition strategy for authorization directives is intentionally accumulative. When you define authorization directives on fields and types in subgraphs, GraphOS composes them into the supergraph schema. In other words, if subgraph fields or types include `@requiresScopes`, `@authenticated`, or `@policy` directives, they are set on the supergraph too. #### Composition with `AND`/`OR` logic @@ -638,7 +836,7 @@ The `reject_unauthorized` option configures whether to reject an entire query if ```yaml title="router.yaml" authorization: - preview_directives: + directives: enabled: true reject_unauthorized: true # default: false ``` @@ -653,7 +851,7 @@ By enabling the `log` option, you can choose if query filtering will result in a ```yaml title="router.yaml" authorization: - preview_directives: + directives: errors: log: false # default: true ``` @@ -674,7 +872,7 @@ You can configure `response` to define what part of the GraphQL response should ```yaml title="router.yaml" authorization: - preview_directives: + directives: errors: response: "errors" # possible values: "errors" (default), "extensions", "disabled" ``` @@ -685,7 +883,7 @@ The `dry_run` option allows you to execute authorization directives without modi ```yaml title="router.yaml" authorization: - preview_directives: + directives: enabled: true dry_run: true # default: false ``` diff --git a/docs/source/enterprise-features.mdx b/docs/source/enterprise-features.mdx index 3c771db024..f253aac569 100644 --- a/docs/source/enterprise-features.mdx +++ b/docs/source/enterprise-features.mdx @@ -15,7 +15,7 @@ Try out these Enterprise features for free with an [Enterprise trial](/graphos/o - **Real-time updates** via [GraphQL subscriptions](./executing-operations/subscription-support/) - **Authentication of inbound requests** via [JSON Web Token (JWT)](./configuration/authn-jwt/) -- [**Authorization** of specific fields and types](./configuration/authorization) through the [`@requiresScopes`](./configuration/authorization#requiresscopes) and [`@authenticated`](./configuration/authorization#authenticated) directives +- [**Authorization** of specific fields and types](./configuration/authorization) through the [`@requiresScopes`](./configuration/authorization#requiresscopes), [`@authenticated`](./configuration/authorization#authenticated), and [`@policy`](./configuration/authorization#policy) directives - Redis-backed [**distributed caching** of query plans and persisted queries](./configuration/distributed-caching/) - **Custom request handling** in any language via [external coprocessing](./customizations/coprocessor/) - **Mitigation of potentially malicious requests** via [operation limits](./configuration/operation-limits) and [safelisting with persisted queries](./configuration/persisted-queries)