From 522da6890cef09d1e48b83457a4a96551c6ebdf2 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Fri, 7 Jul 2023 17:31:50 +0200 Subject: [PATCH 01/82] GraphOS Enterprise: authorization directives 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 --- apollo-router/feature_discussions.json | 3 +- ...nfiguration__tests__schema_generation.snap | 9 +- .../plugins/authorization/authenticated.rs | 709 ++++++++++++++++++ .../src/plugins/authorization/mod.rs | 516 ++++++++----- .../src/plugins/authorization/scopes.rs | 573 ++++++++++++++ ...zation__authenticated__tests__array-2.snap | 20 + ...rization__authenticated__tests__array.snap | 13 + ...ticated__tests__authenticated_request.snap | 16 + ...zation__authenticated__tests__defer-2.snap | 19 + ...rization__authenticated__tests__defer.snap | 10 + ...nticated__tests__interface_fragment-2.snap | 16 + ...henticated__tests__interface_fragment.snap | 13 + ...d__tests__interface_inline_fragment-2.snap | 16 + ...ted__tests__interface_inline_fragment.snap | 13 + ...ion__authenticated__tests__mutation-2.snap | 13 + ...ation__authenticated__tests__mutation.snap | 5 + ...__authenticated__tests__query_field-2.snap | 13 + ...on__authenticated__tests__query_field.snap | 10 + ...ation__authenticated__tests__scalar-2.snap | 16 + ...ization__authenticated__tests__scalar.snap | 10 + ...ization__authenticated__tests__test-2.snap | 24 + ...orization__authenticated__tests__test.snap | 10 + ...cated__tests__unauthenticated_request.snap | 29 + ..._tests__unauthenticated_request_defer.snap | 14 + ...authorization__scopes__tests__array-2.snap | 17 + ...__authorization__scopes__tests__array.snap | 10 + ...zation__scopes__tests__extract_scopes.snap | 11 + ...__scopes__tests__filter_basic_query-2.snap | 23 + ...__scopes__tests__filter_basic_query-3.snap | 10 + ...__scopes__tests__filter_basic_query-4.snap | 23 + ...__scopes__tests__filter_basic_query-5.snap | 14 + ...__scopes__tests__filter_basic_query-6.snap | 16 + ...__scopes__tests__filter_basic_query-7.snap | 14 + ...__scopes__tests__filter_basic_query-8.snap | 16 + ...on__scopes__tests__filter_basic_query.snap | 10 + ...__scopes__tests__interface_fragment-2.snap | 16 + ...__scopes__tests__interface_fragment-3.snap | 17 + ...__scopes__tests__interface_fragment-4.snap | 5 + ...on__scopes__tests__interface_fragment.snap | 13 + ...s__tests__interface_inline_fragment-2.snap | 16 + ...pes__tests__interface_inline_fragment.snap | 13 + ...horization__scopes__tests__mutation-2.snap | 13 + ...uthorization__scopes__tests__mutation.snap | 6 + ...ization__scopes__tests__query_field-2.snap | 13 + ...orization__scopes__tests__query_field.snap | 10 + ...uthorization__scopes__tests__scalar-2.snap | 16 + ..._authorization__scopes__tests__scalar.snap | 10 + ...ion__tests__authenticated_directive-2.snap | 16 + ...ation__tests__authenticated_directive.snap | 39 + ...horization__tests__scopes_directive-2.snap | 29 + ...horization__tests__scopes_directive-3.snap | 16 + ...uthorization__tests__scopes_directive.snap | 24 + .../src/plugins/authorization/tests.rs | 527 +++++++++++++ apollo-router/src/plugins/mod.rs | 2 +- .../plugins/traffic_shaping/deduplication.rs | 31 +- .../src/query_planner/bridge_query_planner.rs | 106 ++- .../query_planner/caching_query_planner.rs | 42 +- apollo-router/src/query_planner/plan.rs | 8 +- .../src/services/execution_service.rs | 15 + .../src/services/layers/query_analysis.rs | 15 + apollo-router/src/services/router_service.rs | 3 +- .../src/services/supergraph_service.rs | 5 +- apollo-router/src/spec/mod.rs | 4 +- apollo-router/src/spec/query.rs | 4 + apollo-router/src/spec/query/tests.rs | 2 + .../src/uplink/license_enforcement.rs | 4 + apollo-router/tests/redis_test.rs | 2 +- ...ecycle_tests__cli_config_experimental.snap | 1 + docs/source/config.json | 7 + docs/source/configuration/authorization.mdx | 302 ++++++++ 70 files changed, 3357 insertions(+), 249 deletions(-) create mode 100644 apollo-router/src/plugins/authorization/authenticated.rs create mode 100644 apollo-router/src/plugins/authorization/scopes.rs create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__array-2.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__array.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__authenticated_request.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__defer-2.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__defer.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_fragment-2.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_fragment.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_inline_fragment-2.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_inline_fragment.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__mutation-2.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__mutation.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__query_field-2.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__query_field.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__scalar-2.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__scalar.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__test-2.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__test.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__unauthenticated_request.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__unauthenticated_request_defer.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__array-2.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__array.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__extract_scopes.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-2.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-3.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-4.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-5.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-6.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-7.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-8.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-2.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-3.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-4.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-2.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__mutation-2.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__mutation.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field-2.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__scalar-2.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__scalar.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__tests__authenticated_directive-2.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__tests__authenticated_directive.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__tests__scopes_directive-2.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__tests__scopes_directive-3.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__tests__scopes_directive.snap create mode 100644 apollo-router/src/plugins/authorization/tests.rs create mode 100644 docs/source/configuration/authorization.mdx diff --git a/apollo-router/feature_discussions.json b/apollo-router/feature_discussions.json index 2f68ec98c7..2456d361bb 100644 --- a/apollo-router/feature_discussions.json +++ b/apollo-router/feature_discussions.json @@ -3,7 +3,8 @@ "experimental_retry": "https://github.com/apollographql/router/discussions/2241", "experimental_response_trace_id": "https://github.com/apollographql/router/discussions/2147", "experimental_logging": "https://github.com/apollographql/router/discussions/1961", - "experimental_http_max_request_bytes": "https://github.com/apollographql/router/discussions/3220" + "experimental_http_max_request_bytes": "https://github.com/apollographql/router/discussions/3220", + "experimental_enable_authorization_directives": "https://github.com/apollographql/router/discussions/???" }, "preview": {} } \ No newline at end of file diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap index 27eeae4cdc..3fca615a0e 100644 --- a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap @@ -209,12 +209,15 @@ expression: "&schema" "authorization": { "description": "Authorization plugin", "type": "object", - "required": [ - "require_authentication" - ], "properties": { + "experimental_enable_authorization_directives": { + "description": "enables the `@authenticated` and `@hasScopes` directives", + "default": false, + "type": "boolean" + }, "require_authentication": { "description": "Reject unauthenticated requests", + "default": false, "type": "boolean" } } diff --git a/apollo-router/src/plugins/authorization/authenticated.rs b/apollo-router/src/plugins/authorization/authenticated.rs new file mode 100644 index 0000000000..e82dc0f227 --- /dev/null +++ b/apollo-router/src/plugins/authorization/authenticated.rs @@ -0,0 +1,709 @@ +//! Authorization plugin + +use apollo_compiler::hir; +use apollo_compiler::hir::FieldDefinition; +use apollo_compiler::hir::TypeDefinition; +use apollo_compiler::ApolloCompiler; +use apollo_compiler::FileId; +use apollo_compiler::HirDatabase; +use tower::BoxError; + +use crate::json_ext::Path; +use crate::json_ext::PathElement; +use crate::spec::query::transform; + +pub(crate) const AUTHENTICATED_DIRECTIVE_NAME: &str = "authenticated"; + +pub(crate) struct AuthenticatedVisitor<'a> { + compiler: &'a ApolloCompiler, + file_id: FileId, + pub(crate) query_requires_authentication: bool, + pub(crate) unauthorized_paths: Vec, + current_path: Path, +} + +impl<'a> AuthenticatedVisitor<'a> { + pub(crate) fn new(compiler: &'a ApolloCompiler, file_id: FileId) -> Self { + Self { + compiler, + file_id, + query_requires_authentication: false, + unauthorized_paths: Vec::new(), + current_path: Path::default(), + } + } + + fn is_field_authenticated(&self, field: &FieldDefinition) -> bool { + field + .directive_by_name(AUTHENTICATED_DIRECTIVE_NAME) + .is_some() + || field + .ty() + .type_def(&self.compiler.db) + .map(|t| self.is_type_authenticated(&t)) + .unwrap_or(false) + } + + fn is_type_authenticated(&self, t: &TypeDefinition) -> bool { + t.directive_by_name(AUTHENTICATED_DIRECTIVE_NAME).is_some() + } +} + +impl<'a> transform::Visitor for AuthenticatedVisitor<'a> { + fn compiler(&self) -> &ApolloCompiler { + self.compiler + } + + fn field( + &mut self, + parent_type: &str, + node: &hir::Field, + ) -> Result, BoxError> { + let field_name = node.name(); + + let mut is_field_list = false; + + let field_requires_authentication = self + .compiler + .db + .types_definitions_by_name() + .get(parent_type) + .and_then(|def| def.field(&self.compiler.db, field_name)) + .is_some_and(|field| { + if field.ty().is_list() { + is_field_list = true; + } + self.is_field_authenticated(field) + }); + + self.current_path.push(PathElement::Key(field_name.into())); + if is_field_list { + self.current_path.push(PathElement::Flatten); + } + + let res = if field_requires_authentication { + self.unauthorized_paths.push(self.current_path.clone()); + self.query_requires_authentication = true; + Ok(None) + } else { + transform::field(self, parent_type, node) + }; + + if is_field_list { + self.current_path.pop(); + } + self.current_path.pop(); + + res + } + + fn fragment_definition( + &mut self, + node: &hir::FragmentDefinition, + ) -> Result, BoxError> { + let fragment_requires_authentication = self + .compiler + .db + .types_definitions_by_name() + .get(node.type_condition()) + .is_some_and(|type_definition| self.is_type_authenticated(type_definition)); + + if fragment_requires_authentication { + Ok(None) + } else { + transform::fragment_definition(self, node) + } + } + + fn fragment_spread( + &mut self, + node: &hir::FragmentSpread, + ) -> Result, BoxError> { + let fragments = self.compiler.db.fragments(self.file_id); + let condition = fragments + .get(node.name()) + .ok_or("MissingFragmentDefinition")? + .type_condition(); + self.current_path + .push(PathElement::Fragment(condition.into())); + + let fragment_requires_authentication = self + .compiler + .db + .types_definitions_by_name() + .get(condition) + .is_some_and(|type_definition| self.is_type_authenticated(type_definition)); + + let res = if fragment_requires_authentication { + self.query_requires_authentication = true; + self.unauthorized_paths.push(self.current_path.clone()); + + Ok(None) + } else { + transform::fragment_spread(self, node) + }; + + self.current_path.pop(); + res + } + + fn inline_fragment( + &mut self, + parent_type: &str, + + node: &hir::InlineFragment, + ) -> Result, BoxError> { + match node.type_condition() { + None => { + self.current_path.push(PathElement::Fragment(String::new())); + let res = transform::inline_fragment(self, parent_type, node); + self.current_path.pop(); + res + } + Some(name) => { + self.current_path.push(PathElement::Fragment(name.into())); + + let fragment_requires_authentication = self + .compiler + .db + .types_definitions_by_name() + .get(name) + .is_some_and(|type_definition| self.is_type_authenticated(type_definition)); + + let res = if fragment_requires_authentication { + self.query_requires_authentication = true; + self.unauthorized_paths.push(self.current_path.clone()); + Ok(None) + } else { + transform::inline_fragment(self, parent_type, node) + }; + + self.current_path.pop(); + + res + } + } + } +} + +#[cfg(test)] +mod tests { + use apollo_compiler::ApolloCompiler; + use multimap::MultiMap; + use serde_json_bytes::json; + use tower::ServiceExt; + + use crate::http_ext::TryIntoHeaderName; + use crate::http_ext::TryIntoHeaderValue; + use crate::json_ext::Path; + use crate::plugin::test::MockSubgraph; + use crate::plugins::authorization::authenticated::AuthenticatedVisitor; + use crate::services::router::ClientRequestAccepts; + use crate::services::supergraph; + use crate::spec::query::transform; + use crate::Context; + use crate::MockedSubgraphs; + use crate::TestHarness; + + static BASIC_SCHEMA: &str = r#" + directive @authenticated on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM + directive @defer on INLINE_FRAGMENT | FRAGMENT_SPREAD + + type Query { + topProducts: Product + customer: User + me: User @authenticated + itf: I! + } + + type Mutation @authenticated { + ping: User + } + + interface I { + id: ID + } + + type Product { + type: String + price(setPrice: Int): Int + reviews: [Review] @authenticated + internal: Internal + publicReviews: [Review] + nonNullId: ID! @authenticated + } + + scalar Internal @authenticated @specifiedBy(url: "http///example.com/test") + + type Review { + body: String + author: User + } + + type User + implements I + @authenticated { + id: ID + name: String + } + "#; + + fn filter(query: &str) -> (apollo_encoder::Document, Vec) { + let mut compiler = ApolloCompiler::new(); + + let _schema_id = compiler.add_type_system(BASIC_SCHEMA, "schema.graphql"); + let file_id = compiler.add_executable(query, "query.graphql"); + + let diagnostics = compiler.validate(); + for diagnostic in &diagnostics { + println!("{diagnostic}"); + } + assert!(diagnostics.is_empty()); + + let mut visitor = AuthenticatedVisitor::new(&compiler, file_id); + + ( + transform::document(&mut visitor, file_id).unwrap(), + visitor.unauthorized_paths, + ) + } + + #[test] + fn mutation() { + static QUERY: &str = r#" + mutation { + ping { + name + } + } + "#; + + let (doc, paths) = filter(QUERY); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + } + + #[test] + fn query_field() { + static QUERY: &str = r#" + query { + topProducts { + type + } + + me { + name + } + } + "#; + + let (doc, paths) = filter(QUERY); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + } + + #[test] + fn scalar() { + static QUERY: &str = r#" + query { + topProducts { + type + internal + } + } + "#; + + let (doc, paths) = filter(QUERY); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + } + + #[test] + fn array() { + static QUERY: &str = r#" + query { + topProducts { + type + publicReviews { + body + author { + name + } + } + } + } + "#; + + let (doc, paths) = filter(QUERY); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + } + + #[test] + fn interface_inline_fragment() { + static QUERY: &str = r#" + query { + topProducts { + type + } + itf { + id + ... on User { + name + } + } + } + "#; + + let (doc, paths) = filter(QUERY); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + } + + #[test] + fn interface_fragment() { + static QUERY: &str = r#" + query { + topProducts { + type + } + itf { + id + ...F + } + } + + fragment F on User { + name + } + "#; + + let (doc, paths) = filter(QUERY); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + } + + #[test] + fn defer() { + static QUERY: &str = r#" + query { + topProducts { + type + + ...@defer { + nonNullId + } + } + } + "#; + + let (doc, paths) = filter(QUERY); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + } + + #[test] + fn test() { + static QUERY: &str = r#" + query { + topProducts { + type + reviews { + body + } + } + + customer { + name + } + } + "#; + + let (doc, paths) = filter(QUERY); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + } + + const SCHEMA: &str = r#"schema + @core(feature: "https://specs.apollo.dev/core/v0.1") + @core(feature: "https://specs.apollo.dev/join/v0.1") + @core(feature: "https://specs.apollo.dev/inaccessible/v0.1") + { + query: Query + } + directive @core(feature: String!) repeatable on SCHEMA + directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION + directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE + directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE + directive @join__graph(name: String!, url: String!) on ENUM_VALUE + directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION + scalar join__FieldSet + enum join__Graph { + USER @join__graph(name: "user", url: "http://localhost:4001/graphql") + ORGA @join__graph(name: "orga", url: "http://localhost:4002/graphql") + } + + directive @authenticated on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM + + type Query { + currentUser: User @join__field(graph: USER) @authenticated + orga(id: ID): Organization @join__field(graph: ORGA) + } + type User + @join__owner(graph: USER) + @join__type(graph: ORGA, key: "id") + @join__type(graph: USER, key: "id"){ + id: ID! + name: String + phone: String @authenticated + activeOrganization: Organization + } + type Organization + @join__owner(graph: ORGA) + @join__type(graph: ORGA, key: "id") + @join__type(graph: USER, key: "id") { + id: ID + creatorUser: User + name: String + nonNullId: ID! @authenticated + suborga: [Organization] + }"#; + + #[tokio::test] + async fn authenticated_request() { + let subgraphs = MockedSubgraphs([ + ("user", MockSubgraph::builder().with_json( + serde_json::json!{{ + "query": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{name phone}}}", + "variables": { + "representations": [ + { "__typename": "User", "id":0 } + ], + } + }}, + serde_json::json! {{ + "data": { + "_entities":[ + { + "name":"Ada", + "phone": "1234" + } + ] + } + }}, + ).build()), + ("orga", MockSubgraph::builder().with_json( + serde_json::json!{{"query":"{orga(id:1){id creatorUser{__typename id}}}"}}, + serde_json::json!{{"data": {"orga": { "id": 1, "creatorUser": { "__typename": "User", "id": 0 } }}}} + ).build()) + ].into_iter().collect()); + + let service = TestHarness::builder() + .configuration_json(serde_json::json!({ + "include_subgraph_errors": { + "all": true + }, + "authorization": { + "experimental_enable_authorization_directives": true + }})) + .unwrap() + .schema(SCHEMA) + .extra_plugin(subgraphs) + .build_supergraph() + .await + .unwrap(); + + let context = Context::new(); + context + .insert( + "apollo_authentication::JWT::claims", + "placeholder".to_string(), + ) + .unwrap(); + let request = supergraph::Request::fake_builder() + .query("query { orga(id: 1) { id creatorUser { id name phone } } }") + .variables( + json! {{ "isAuthenticated": true }} + .as_object() + .unwrap() + .clone(), + ) + .context(context) + // Request building here + .build() + .unwrap(); + let response = service + .oneshot(request) + .await + .unwrap() + .next_response() + .await + .unwrap(); + + insta::assert_json_snapshot!(response); + } + + #[tokio::test] + async fn unauthenticated_request() { + let subgraphs = MockedSubgraphs([ + ("user", MockSubgraph::builder().with_json( + serde_json::json!{{ + "query": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{name}}}", + "variables": { + "representations": [ + { "__typename": "User", "id":0 } + ], + } + }}, + serde_json::json! {{ + "data": { + "_entities":[ + { + "name":"Ada" + } + ] + } + }}, + ).build()), + ("orga", MockSubgraph::builder().with_json( + serde_json::json!{{"query":"{orga(id:1){id creatorUser{__typename id}}}"}}, + serde_json::json!{{"data": {"orga": { "id": 1, "creatorUser": { "__typename": "User", "id": 0 } }}}} + ).build()) + ].into_iter().collect()); + + let service = TestHarness::builder() + .configuration_json(serde_json::json!({ + "include_subgraph_errors": { + "all": true + }, + "authorization": { + "experimental_enable_authorization_directives": true + }})) + .unwrap() + .schema(SCHEMA) + .extra_plugin(subgraphs) + .build_supergraph() + .await + .unwrap(); + + let context = Context::new(); + /*context + .insert( + "apollo_authentication::JWT::claims", + "placeholder".to_string(), + ) + .unwrap();*/ + let request = supergraph::Request::fake_builder() + .query("query { orga(id: 1) { id creatorUser { id name phone } } }") + .variables( + json! {{ "isAuthenticated": false }} + .as_object() + .unwrap() + .clone(), + ) + .context(context) + // Request building here + .build() + .unwrap(); + let response = service + .oneshot(request) + .await + .unwrap() + .next_response() + .await + .unwrap(); + + insta::assert_json_snapshot!(response); + } + + #[tokio::test] + async fn unauthenticated_request_defer() { + let subgraphs = MockedSubgraphs([ + ("orga", MockSubgraph::builder().with_json( + serde_json::json!{{"query":"{orga(id:1){id creatorUser{id}}}"}}, + serde_json::json!{{"data": {"orga": { "id": 1, "creatorUser": { "id": 0 } }}}} + ) + .with_json( + serde_json::json!{{ + "query": "query($representations:[_Any!]!){_entities(representations:$representations){...on Orga{name}}}", + "variables": { + "representations": [ + { "__typename": "Organization", "id":1 } + ], + } + }}, + serde_json::json! {{ + "data": { + "_entities":[ + { + "name":"Orga 1" + } + ] + } + }}, + ).build()) + ].into_iter().collect()); + + let service = TestHarness::builder() + .configuration_json(serde_json::json!({ + "include_subgraph_errors": { + "all": true + }, + "authorization": { + "experimental_enable_authorization_directives": true + }})) + .unwrap() + .schema(SCHEMA) + .extra_plugin(subgraphs) + .build_supergraph() + .await + .unwrap(); + + let context = Context::new(); + /*context + .insert( + "apollo_authentication::JWT::claims", + "placeholder".to_string(), + ) + .unwrap();*/ + let mut headers: MultiMap = MultiMap::new(); + headers.insert( + "Accept".into(), + "multipart/mixed; deferSpec=20220824".into(), + ); + context.private_entries.lock().insert(ClientRequestAccepts { + multipart_defer: true, + multipart_subscription: true, + json: true, + wildcard: true, + }); + let request = supergraph::Request::fake_builder() + .query("query { orga(id: 1) { id creatorUser { id } ... @defer { nonNullId } } }") + .variables( + json! {{ "isAuthenticated": false }} + .as_object() + .unwrap() + .clone(), + ) + //.headers(headers) + .context(context) + .build() + .unwrap(); + + let mut response = service.oneshot(request).await.unwrap(); + + let first_response = response.next_response().await.unwrap(); + + insta::assert_json_snapshot!(first_response); + + assert!(response.next_response().await.is_none()); + } +} diff --git a/apollo-router/src/plugins/authorization/mod.rs b/apollo-router/src/plugins/authorization/mod.rs index f8d534f848..e17d1d2480 100644 --- a/apollo-router/src/plugins/authorization/mod.rs +++ b/apollo-router/src/plugins/authorization/mod.rs @@ -1,31 +1,287 @@ //! Authorization plugin +use std::collections::HashSet; use std::ops::ControlFlow; +use std::sync::Arc; +use apollo_compiler::ApolloCompiler; +use apollo_compiler::InputDatabase; use http::StatusCode; +use router_bridge::planner::UsageReporting; use schemars::JsonSchema; use serde::Deserialize; +use tokio::sync::Mutex; use tower::BoxError; use tower::ServiceBuilder; use tower::ServiceExt; +use self::authenticated::AuthenticatedVisitor; +use self::authenticated::AUTHENTICATED_DIRECTIVE_NAME; +use self::scopes::ScopeExtractionVisitor; +use self::scopes::ScopeFilteringVisitor; +use self::scopes::REQUIRES_SCOPES_DIRECTIVE_NAME; +use crate::error::PlanErrors; +use crate::error::QueryPlannerError; +use crate::error::SchemaError; +use crate::error::ServiceBuildError; use crate::graphql; +use crate::json_ext::Object; +use crate::json_ext::Path; use crate::layers::ServiceBuilderExt; use crate::plugin::Plugin; use crate::plugin::PluginInit; use crate::plugins::authentication::APOLLO_AUTHENTICATION_JWT_CLAIMS; +use crate::query_planner::FilteredQuery; +use crate::query_planner::QueryKey; +use crate::query_planner::QUERY_PLANNER_CACHE_KEY_METADATA; use crate::register_plugin; use crate::services::supergraph; +use crate::spec::query::transform; +use crate::spec::query::traverse; +use crate::spec::Query; +use crate::spec::Schema; +use crate::spec::SpecError; +use crate::spec::GRAPHQL_VALIDATION_FAILURE_ERROR_KEY; +use crate::Configuration; +use crate::Context; + +pub(crate) mod authenticated; +pub(crate) mod scopes; + +const REQUIRED_SCOPES_KEY: &str = "apollo_authorization::scopes::required"; /// Authorization plugin #[derive(Clone, Debug, Default, Deserialize, JsonSchema)] -struct Conf { +#[allow(dead_code)] +pub(crate) struct Conf { /// Reject unauthenticated requests + #[serde(default)] + require_authentication: bool, + /// enables the `@authenticated` and `@hasScopes` directives + #[serde(default)] + experimental_enable_authorization_directives: bool, +} + +pub(crate) struct AuthorizationPlugin { require_authentication: bool, + enable_authorization_directives: bool, } -struct AuthorizationPlugin { - enabled: bool, +impl AuthorizationPlugin { + pub(crate) fn enable_directives( + configuration: &Configuration, + schema: &Schema, + ) -> Result { + let has_config = configuration + .apollo_plugins + .plugins + .iter() + .find(|(s, _)| s.as_str() == "authorization") + .and_then(|(_, v)| { + v.get("experimental_enable_authorization_directives") + .and_then(|v| v.as_bool()) + }); + let has_authorization_directives = schema + .type_system + .definitions + .directives + .contains_key(AUTHENTICATED_DIRECTIVE_NAME) + || schema + .type_system + .definitions + .directives + .contains_key(REQUIRES_SCOPES_DIRECTIVE_NAME); + + match has_config { + Some(b) => Ok(b), + None => { + if has_authorization_directives { + Err(ServiceBuildError::Schema(SchemaError::Api("cannot start the router on a schema with authorization directives without configuring the authorization plugin".to_string()))) + } else { + Ok(false) + } + } + } + } + + pub(crate) async fn query_analysis( + query: &str, + schema: &Schema, + configuration: &Configuration, + context: &Context, + ) { + let (compiler, file_id) = Query::make_compiler(query, schema, configuration); + + let mut visitor = ScopeExtractionVisitor::new(&compiler); + + // if this fails, the query is invalid and will fail at the query planning phase. + // We do not return validation errors here for now because that would imply a huge + // refactoring of telemetry and tests + if traverse::document(&mut visitor, file_id).is_ok() { + let scopes: Vec = visitor.extracted_scopes.into_iter().collect(); + + context.insert(REQUIRED_SCOPES_KEY, scopes).unwrap(); + } + } + + pub(crate) fn filter_query( + key: &QueryKey, + schema: &Schema, + ) -> Result, QueryPlannerError> { + // we create a compiler to filter the query. The filtered query will then be used + // to generate selections for response formatting, to execute introspection and + // generating a query plan + let mut compiler = ApolloCompiler::new(); + compiler.set_type_system_hir(schema.type_system.clone()); + let _id = compiler.add_executable(&key.filtered_query, "query"); + + let is_authenticated = key + .metadata + .get("is_authenticated") + .and_then(|v| v.as_bool()) + .unwrap_or_default(); + let scopes = key + .metadata + .get("scopes") + .and_then(|v| v.as_array()) + .map(|v| { + v.iter() + .filter_map(|el| el.as_str().map(|s| s.to_string())) + .collect::>() + }) + .unwrap_or_default(); + + let filter_res = Self::authenticated_filter_query(&compiler, is_authenticated)?; + + let filter_res = match filter_res { + None => Self::scopes_filter_query(&compiler, &scopes).map(|opt| { + opt.map(|(query, paths)| (query, paths, Arc::new(Mutex::new(compiler)))) + }), + Some((query, mut paths)) => { + if query.is_empty() { + return Err(QueryPlannerError::PlanningErrors(PlanErrors { + errors: Arc::new(vec![router_bridge::planner::PlanError { + message: Some("Unauthorized query".to_string()), + extensions: None, + validation_error: false, + }]), + usage_reporting: UsageReporting { + stats_report_key: GRAPHQL_VALIDATION_FAILURE_ERROR_KEY.to_string(), + referenced_fields_by_type: Default::default(), + }, + })); + } + let mut compiler = ApolloCompiler::new(); + compiler.set_type_system_hir(schema.type_system.clone()); + let _id = compiler.add_executable(&query, "query"); + + match Self::scopes_filter_query(&compiler, &scopes)? { + None => Ok(Some((query, paths, Arc::new(Mutex::new(compiler))))), + Some((new_query, new_paths)) => { + let mut compiler = ApolloCompiler::new(); + compiler.set_type_system_hir(schema.type_system.clone()); + let _id = compiler.add_executable(&new_query, "query"); + paths.extend(new_paths.into_iter()); + Ok(Some((new_query, paths, Arc::new(Mutex::new(compiler))))) + } + } + } + }?; + + match filter_res { + None => Ok(None), + Some((filtered_query, paths, _)) => { + if filtered_query.is_empty() { + Err(QueryPlannerError::PlanningErrors(PlanErrors { + errors: Arc::new(vec![router_bridge::planner::PlanError { + message: Some("Unauthorized query".to_string()), + extensions: None, + validation_error: false, + }]), + usage_reporting: UsageReporting { + stats_report_key: GRAPHQL_VALIDATION_FAILURE_ERROR_KEY.to_string(), + referenced_fields_by_type: Default::default(), + }, + })) + } else { + let mut compiler = ApolloCompiler::new(); + compiler.set_type_system_hir(schema.type_system.clone()); + let _id = compiler.add_executable(&filtered_query, "query"); + Ok(Some(( + filtered_query, + paths, + Arc::new(Mutex::new(compiler)), + ))) + } + } + } + } + + fn authenticated_filter_query( + compiler: &ApolloCompiler, + is_authenticated: bool, + ) -> Result)>, QueryPlannerError> { + let id = compiler + .db + .executable_definition_files() + .pop() + .expect("the query was added to the compiler earlier"); + + let mut visitor = AuthenticatedVisitor::new(compiler, id); + let modified_query = transform::document(&mut visitor, id) + .map_err(|e| SpecError::ParsingError(e.to_string()))? + .to_string(); + + if visitor.query_requires_authentication { + if is_authenticated { + tracing::debug!("the query contains @authenticated, the request is authenticated, keeping the query"); + Ok(None) + } else { + tracing::debug!("the query contains @authenticated, modified query:\n{modified_query}\nunauthorized paths: {:?}", visitor + .unauthorized_paths + .iter() + .map(|path| path.to_string()) + .collect::>()); + + Ok(Some((modified_query, visitor.unauthorized_paths))) + } + } else { + tracing::debug!("the query does not contain @authenticated"); + Ok(None) + } + } + + fn scopes_filter_query( + compiler: &ApolloCompiler, + scopes: &[String], + ) -> Result)>, QueryPlannerError> { + let id = compiler + .db + .executable_definition_files() + .pop() + .expect("the query was added to the compiler earlier"); + + let mut visitor = + ScopeFilteringVisitor::new(compiler, id, scopes.iter().cloned().collect()); + + let modified_query = transform::document(&mut visitor, id) + .map_err(|e| SpecError::ParsingError(e.to_string()))? + .to_string(); + + if visitor.query_requires_scopes { + tracing::debug!("the query required scopes, the requests present scopes: {scopes:?}, modified query:\n{modified_query}\nunauthorized paths: {:?}", + visitor + .unauthorized_paths + .iter() + .map(|path| path.to_string()) + .collect::>() + ); + Ok(Some((modified_query, visitor.unauthorized_paths))) + } else { + tracing::debug!("the query does not require scopes"); + Ok(None) + } + } } #[async_trait::async_trait] @@ -34,12 +290,15 @@ impl Plugin for AuthorizationPlugin { async fn new(init: PluginInit) -> Result { Ok(AuthorizationPlugin { - enabled: init.config.require_authentication, + require_authentication: init.config.require_authentication, + enable_authorization_directives: init + .config + .experimental_enable_authorization_directives, }) } fn supergraph_service(&self, service: supergraph::BoxService) -> supergraph::BoxService { - if self.enabled { + let service = if self.require_authentication { ServiceBuilder::new() .checkpoint(move |request: supergraph::Request| { if request @@ -70,6 +329,60 @@ impl Plugin for AuthorizationPlugin { .boxed() } else { service + }; + + if self.enable_authorization_directives { + ServiceBuilder::new() + .map_request(move |request: supergraph::Request| { + let is_authenticated = request + .context + .contains_key(APOLLO_AUTHENTICATION_JWT_CLAIMS); + + let request_scopes = request + .context + .get_json_value(APOLLO_AUTHENTICATION_JWT_CLAIMS) + .and_then(|value| { + value.as_object().and_then(|object| { + object.get("scope").and_then(|v| { + v.as_str().map(|s| { + s.split(' ').map(|s| s.to_string()).collect::>() + }) + }) + }) + }); + let query_scopes = request + .context + .get_json_value(REQUIRED_SCOPES_KEY) + .and_then(|v| { + v.as_array().map(|v| { + v.iter() + .filter_map(|s| s.as_str().map(|s| s.to_string())) + .collect::>() + }) + }); + + let mut scopes = match (request_scopes, query_scopes) { + (None, _) => vec![], + (_, None) => vec![], + (Some(req), Some(query)) => req.intersection(&query).cloned().collect(), + }; + scopes.sort(); + + request + .context + .upsert(QUERY_PLANNER_CACHE_KEY_METADATA, |mut o: Object| { + o.insert("is_authenticated", is_authenticated.into()); + o.insert("scopes", scopes.into()); + o + }) + .unwrap(); + + request + }) + .service(service) + .boxed() + } else { + service } } } @@ -82,195 +395,4 @@ impl Plugin for AuthorizationPlugin { register_plugin!("apollo", "authorization", AuthorizationPlugin); #[cfg(test)] -mod tests { - use serde_json_bytes::json; - use tower::ServiceExt; - - use crate::plugin::test::MockSubgraph; - use crate::services::supergraph; - use crate::Context; - use crate::MockedSubgraphs; - use crate::TestHarness; - - const SCHEMA: &str = r#"schema - @core(feature: "https://specs.apollo.dev/core/v0.1") - @core(feature: "https://specs.apollo.dev/join/v0.1") - @core(feature: "https://specs.apollo.dev/inaccessible/v0.1") - { - query: Query - } - directive @core(feature: String!) repeatable on SCHEMA - directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION - directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE - directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE - directive @join__graph(name: String!, url: String!) on ENUM_VALUE - directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION - scalar join__FieldSet - enum join__Graph { - USER @join__graph(name: "user", url: "http://localhost:4001/graphql") - ORGA @join__graph(name: "orga", url: "http://localhost:4002/graphql") - } - type Query { - currentUser: User @join__field(graph: USER) - orga(id: ID): Organization @join__field(graph: ORGA) - } - type User - @join__owner(graph: USER) - @join__type(graph: ORGA, key: "id") - @join__type(graph: USER, key: "id"){ - id: ID! - name: String - phone: String - activeOrganization: Organization - } - type Organization - @join__owner(graph: ORGA) - @join__type(graph: ORGA, key: "id") - @join__type(graph: USER, key: "id") { - id: ID - creatorUser: User - name: String - nonNullId: ID! - suborga: [Organization] - }"#; - - #[tokio::test] - async fn authenticated_request() { - let subgraphs = MockedSubgraphs([ - ("user", MockSubgraph::builder().with_json( - serde_json::json!{{ - "query": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{name phone}}}", - "variables": { - "representations": [ - { "__typename": "User", "id":0 } - ], - } - }}, - serde_json::json! {{ - "data": { - "_entities":[ - { - "name":"Ada", - "phone": "1234" - } - ] - } - }}, - ).build()), - ("orga", MockSubgraph::builder().with_json( - serde_json::json!{{"query":"{orga(id:1){id creatorUser{__typename id}}}"}}, - serde_json::json!{{"data": {"orga": { "id": 1, "creatorUser": { "__typename": "User", "id": 0 } }}}} - ).build()) - ].into_iter().collect()); - - let service = TestHarness::builder() - .configuration_json(serde_json::json!({ - "include_subgraph_errors": { - "all": true - }, - "authorization": { - "require_authentication": true - }})) - .unwrap() - .schema(SCHEMA) - .extra_plugin(subgraphs) - .build_supergraph() - .await - .unwrap(); - - let context = Context::new(); - context - .insert( - "apollo_authentication::JWT::claims", - "placeholder".to_string(), - ) - .unwrap(); - let request = supergraph::Request::fake_builder() - .query("query { orga(id: 1) { id creatorUser { id name phone } } }") - .variables( - json! {{ "isAuthenticated": true }} - .as_object() - .unwrap() - .clone(), - ) - .context(context) - .build() - .unwrap(); - let response = service - .oneshot(request) - .await - .unwrap() - .next_response() - .await - .unwrap(); - - insta::assert_json_snapshot!(response); - } - - #[tokio::test] - async fn unauthenticated_request() { - let subgraphs = MockedSubgraphs([ - ("user", MockSubgraph::builder().with_json( - serde_json::json!{{ - "query": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{name}}}", - "variables": { - "representations": [ - { "__typename": "User", "id":0 } - ], - } - }}, - serde_json::json! {{ - "data": { - "_entities":[ - { - "name":"Ada" - } - ] - } - }}, - ).build()), - ("orga", MockSubgraph::builder().with_json( - serde_json::json!{{"query":"{orga(id:1){id creatorUser{__typename id}}}"}}, - serde_json::json!{{"data": {"orga": { "id": 1, "creatorUser": { "__typename": "User", "id": 0 } }}}} - ).build()) - ].into_iter().collect()); - - let service = TestHarness::builder() - .configuration_json(serde_json::json!({ - "include_subgraph_errors": { - "all": true - }, - "authorization": { - "require_authentication": true - }})) - .unwrap() - .schema(SCHEMA) - .extra_plugin(subgraphs) - .build_supergraph() - .await - .unwrap(); - - let context = Context::new(); - let request = supergraph::Request::fake_builder() - .query("query { orga(id: 1) { id creatorUser { id name phone } } }") - .variables( - json! {{ "isAuthenticated": false }} - .as_object() - .unwrap() - .clone(), - ) - .context(context) - // Request building here - .build() - .unwrap(); - let response = service - .oneshot(request) - .await - .unwrap() - .next_response() - .await - .unwrap(); - - insta::assert_json_snapshot!(response); - } -} +mod tests; diff --git a/apollo-router/src/plugins/authorization/scopes.rs b/apollo-router/src/plugins/authorization/scopes.rs new file mode 100644 index 0000000000..da67a136ea --- /dev/null +++ b/apollo-router/src/plugins/authorization/scopes.rs @@ -0,0 +1,573 @@ +//! Authorization plugin + +use std::collections::HashSet; + +use apollo_compiler::hir; +use apollo_compiler::hir::FieldDefinition; +use apollo_compiler::hir::TypeDefinition; +use apollo_compiler::hir::Value; +use apollo_compiler::ApolloCompiler; +use apollo_compiler::FileId; +use apollo_compiler::HirDatabase; +use tower::BoxError; + +use crate::json_ext::Path; +use crate::json_ext::PathElement; +use crate::spec::query::transform; +use crate::spec::query::traverse; + +pub(crate) struct ScopeExtractionVisitor<'a> { + compiler: &'a ApolloCompiler, + pub(crate) extracted_scopes: HashSet, +} + +pub(crate) const REQUIRES_SCOPES_DIRECTIVE_NAME: &str = "requiresScopes"; + +impl<'a> ScopeExtractionVisitor<'a> { + #[allow(dead_code)] + pub(crate) fn new(compiler: &'a ApolloCompiler) -> Self { + Self { + compiler, + extracted_scopes: HashSet::new(), + } + } + + fn get_scopes_from_field(&mut self, field: &FieldDefinition) { + self.extracted_scopes.extend( + scopes_argument(field.directive_by_name(REQUIRES_SCOPES_DIRECTIVE_NAME)).cloned(), + ); + + if let Some(ty) = field.ty().type_def(&self.compiler.db) { + self.extracted_scopes.extend( + scopes_argument(ty.directive_by_name(REQUIRES_SCOPES_DIRECTIVE_NAME)).cloned(), + ); + } + } +} + +fn scopes_argument(opt_directive: Option<&hir::Directive>) -> impl Iterator { + opt_directive + .and_then(|directive| directive.argument_by_name("scopes")) + .and_then(|value| match value { + Value::List { value, .. } => Some(value), + _ => None, + }) + .into_iter() + .flatten() + .filter_map(|v| match v { + Value::String { value, .. } => Some(value), + _ => None, + }) +} + +impl<'a> traverse::Visitor for ScopeExtractionVisitor<'a> { + fn compiler(&self) -> &ApolloCompiler { + self.compiler + } + + fn field(&mut self, parent_type: &str, node: &hir::Field) -> Result<(), BoxError> { + if let Some(ty) = self + .compiler + .db + .types_definitions_by_name() + .get(parent_type) + { + if let Some(field) = ty.field(&self.compiler.db, node.name()) { + self.get_scopes_from_field(field); + } + } + + traverse::field(self, parent_type, node) + } +} + +pub(crate) struct ScopeFilteringVisitor<'a> { + compiler: &'a ApolloCompiler, + file_id: FileId, + request_scopes: HashSet, + pub(crate) query_requires_scopes: bool, + pub(crate) unauthorized_paths: Vec, + current_path: Path, +} + +impl<'a> ScopeFilteringVisitor<'a> { + pub(crate) fn new( + compiler: &'a ApolloCompiler, + file_id: FileId, + scopes: HashSet, + ) -> Self { + Self { + compiler, + file_id, + request_scopes: scopes, + query_requires_scopes: false, + unauthorized_paths: vec![], + current_path: Path::default(), + } + } + + fn is_field_authorized(&mut self, field: &FieldDefinition) -> bool { + let field_scopes = scopes_argument(field.directive_by_name(REQUIRES_SCOPES_DIRECTIVE_NAME)) + .cloned() + .collect::>(); + + if !self.request_scopes.is_superset(&field_scopes) { + return false; + } + + if let Some(ty) = field.ty().type_def(&self.compiler.db) { + self.is_type_authorized(&ty) + } else { + false + } + } + + fn is_type_authorized(&self, ty: &TypeDefinition) -> bool { + let type_scopes = scopes_argument(ty.directive_by_name(REQUIRES_SCOPES_DIRECTIVE_NAME)) + .cloned() + .collect::>(); + + self.request_scopes.is_superset(&type_scopes) + } +} + +impl<'a> transform::Visitor for ScopeFilteringVisitor<'a> { + fn compiler(&self) -> &ApolloCompiler { + self.compiler + } + + fn field( + &mut self, + parent_type: &str, + node: &hir::Field, + ) -> Result, BoxError> { + let field_name = node.name(); + let mut is_field_list = false; + + let is_authorized = self + .compiler + .db + .types_definitions_by_name() + .get(parent_type) + .is_some_and(|def| { + if let Some(field) = def.field(&self.compiler.db, field_name) { + if field.ty().is_list() { + is_field_list = true; + } + self.is_field_authorized(field) + } else { + false + } + }); + + self.current_path.push(PathElement::Key(field_name.into())); + if is_field_list { + self.current_path.push(PathElement::Flatten); + } + + if !is_authorized { + self.unauthorized_paths.push(self.current_path.clone()); + } + + let res = if is_authorized { + transform::field(self, parent_type, node) + } else { + self.query_requires_scopes = true; + Ok(None) + }; + + if is_field_list { + self.current_path.pop(); + } + self.current_path.pop(); + + res + } + + fn fragment_definition( + &mut self, + node: &hir::FragmentDefinition, + ) -> Result, BoxError> { + let fragment_is_authorized = self + .compiler + .db + .types_definitions_by_name() + .get(node.type_condition()) + .is_some_and(|ty| self.is_type_authorized(ty)); + + if !fragment_is_authorized { + Ok(None) + } else { + transform::fragment_definition(self, node) + } + } + + fn fragment_spread( + &mut self, + node: &hir::FragmentSpread, + ) -> Result, BoxError> { + let fragments = self.compiler.db.fragments(self.file_id); + let condition = fragments + .get(node.name()) + .ok_or("MissingFragmentDefinition")? + .type_condition(); + self.current_path + .push(PathElement::Fragment(condition.into())); + + let fragment_is_authorized = self + .compiler + .db + .types_definitions_by_name() + .get(condition) + .is_some_and(|ty| self.is_type_authorized(ty)); + + let res = if !fragment_is_authorized { + self.query_requires_scopes = true; + self.unauthorized_paths.push(self.current_path.clone()); + + Ok(None) + } else { + transform::fragment_spread(self, node) + }; + + self.current_path.pop(); + res + } + + fn inline_fragment( + &mut self, + parent_type: &str, + + node: &hir::InlineFragment, + ) -> Result, BoxError> { + match node.type_condition() { + None => { + self.current_path.push(PathElement::Fragment(String::new())); + let res = transform::inline_fragment(self, parent_type, node); + self.current_path.pop(); + res + } + Some(name) => { + self.current_path.push(PathElement::Fragment(name.into())); + + let fragment_is_authorized = self + .compiler + .db + .types_definitions_by_name() + .get(name) + .is_some_and(|ty| self.is_type_authorized(ty)); + + let res = if !fragment_is_authorized { + self.query_requires_scopes = true; + self.unauthorized_paths.push(self.current_path.clone()); + Ok(None) + } else { + transform::inline_fragment(self, parent_type, node) + }; + + self.current_path.pop(); + + res + } + } + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeSet; + use std::collections::HashSet; + + use apollo_compiler::ApolloCompiler; + use apollo_encoder::Document; + + use crate::json_ext::Path; + use crate::plugins::authorization::scopes::ScopeExtractionVisitor; + use crate::plugins::authorization::scopes::ScopeFilteringVisitor; + use crate::spec::query::transform; + use crate::spec::query::traverse; + + static BASIC_SCHEMA: &str = r#" + directive @requiresScopes(scopes: [String]) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM + + type Query { + topProducts: Product + customer: User + me: User @requiresScopes(scopes: ["profile"]) + itf: I + } + + type Mutation { + ping: User @requiresScopes(scopes: ["ping"]) + } + + interface I { + id: ID + } + + type Product { + type: String + price(setPrice: Int): Int + reviews: [Review] + internal: Internal + publicReviews: [Review] + } + + scalar Internal @requiresScopes(scopes: ["internal", "test"]) @specifiedBy(url: "http///example.com/test") + + type Review @requiresScopes(scopes: ["review"]) { + body: String + author: User + } + + type User implements I @requiresScopes(scopes: ["read:user"]) { + id: ID + name: String @requiresScopes(scopes: ["read:username"]) + } + "#; + + fn extract(query: &str) -> BTreeSet { + let mut compiler = ApolloCompiler::new(); + + let _schema_id = compiler.add_type_system(BASIC_SCHEMA, "schema.graphql"); + let id = compiler.add_executable(query, "query.graphql"); + + let diagnostics = compiler.validate(); + for diagnostic in &diagnostics { + println!("{diagnostic}"); + } + assert!(diagnostics.is_empty()); + + let mut visitor = ScopeExtractionVisitor::new(&compiler); + traverse::document(&mut visitor, id).unwrap(); + + visitor.extracted_scopes.into_iter().collect() + } + + #[test] + fn extract_scopes() { + static QUERY: &str = r#" + { + topProducts { + type + internal + } + + me { + name + } + } + "#; + + let doc = extract(QUERY); + + insta::assert_debug_snapshot!(doc); + } + + fn filter(query: &str, scopes: HashSet) -> (Document, Vec) { + let mut compiler = ApolloCompiler::new(); + + let _schema_id = compiler.add_type_system(BASIC_SCHEMA, "schema.graphql"); + let file_id = compiler.add_executable(query, "query.graphql"); + + let diagnostics = compiler.validate(); + for diagnostic in &diagnostics { + println!("{diagnostic}"); + } + assert!(diagnostics.is_empty()); + + let mut visitor = ScopeFilteringVisitor::new(&compiler, file_id, scopes); + ( + transform::document(&mut visitor, file_id).unwrap(), + visitor.unauthorized_paths, + ) + } + + #[test] + fn filter_basic_query() { + static QUERY: &str = r#" + { + topProducts { + type + internal + } + + me { + id + name + } + } + "#; + + let (doc, paths) = filter(QUERY, HashSet::new()); + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + + let (doc, paths) = filter( + QUERY, + ["profile".to_string(), "internal".to_string()] + .into_iter() + .collect(), + ); + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + + let (doc, paths) = filter( + QUERY, + [ + "profile".to_string(), + "read:user".to_string(), + "internal".to_string(), + "test".to_string(), + ] + .into_iter() + .collect(), + ); + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + + let (doc, paths) = filter( + QUERY, + [ + "profile".to_string(), + "read:user".to_string(), + "read:username".to_string(), + ] + .into_iter() + .collect(), + ); + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + } + + #[test] + fn mutation() { + static QUERY: &str = r#" + mutation { + ping { + name + } + } + "#; + + let (doc, paths) = filter(QUERY, HashSet::new()); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + } + + #[test] + fn query_field() { + static QUERY: &str = r#" + query { + topProducts { + type + } + + me { + name + } + } + "#; + + let (doc, paths) = filter(QUERY, HashSet::new()); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + } + + #[test] + fn scalar() { + static QUERY: &str = r#" + query { + topProducts { + type + internal + } + } + "#; + + let (doc, paths) = filter(QUERY, HashSet::new()); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + } + + #[test] + fn array() { + static QUERY: &str = r#" + query { + topProducts { + type + publicReviews { + body + author { + name + } + } + } + } + "#; + + let (doc, paths) = filter(QUERY, HashSet::new()); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + } + + #[test] + fn interface_inline_fragment() { + static QUERY: &str = r#" + query { + topProducts { + type + } + itf { + id + ... on User { + name + } + } + } + "#; + + let (doc, paths) = filter(QUERY, HashSet::new()); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + } + + #[test] + fn interface_fragment() { + static QUERY: &str = r#" + query { + topProducts { + type + } + itf { + id + ...F + } + } + + fragment F on User { + name + } + "#; + + let (doc, paths) = filter(QUERY, HashSet::new()); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + + let (doc, paths) = filter( + QUERY, + ["read:user".to_string(), "read:username".to_string()] + .into_iter() + .collect(), + ); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + } +} diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__array-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__array-2.snap new file mode 100644 index 0000000000..8cbf1dc214 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__array-2.snap @@ -0,0 +1,20 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: paths +--- +[ + Path( + [ + Key( + "topProducts", + ), + Key( + "publicReviews", + ), + Flatten, + Key( + "author", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__array.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__array.snap new file mode 100644 index 0000000000..d1e7a69ddb --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__array.snap @@ -0,0 +1,13 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: doc +--- +query { + topProducts { + type + publicReviews { + body + } + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__authenticated_request.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__authenticated_request.snap new file mode 100644 index 0000000000..9798d65324 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__authenticated_request.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: response +--- +{ + "data": { + "orga": { + "id": 1, + "creatorUser": { + "id": 0, + "name": "Ada", + "phone": "1234" + } + } + } +} diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__defer-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__defer-2.snap new file mode 100644 index 0000000000..eb1ced7add --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__defer-2.snap @@ -0,0 +1,19 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: paths +--- +[ + Path( + [ + Key( + "topProducts", + ), + Fragment( + "", + ), + Key( + "nonNullId", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__defer.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__defer.snap new file mode 100644 index 0000000000..78ccae8dc0 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__defer.snap @@ -0,0 +1,10 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: doc +--- +query { + topProducts { + type + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_fragment-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_fragment-2.snap new file mode 100644 index 0000000000..43a002b1ba --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_fragment-2.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: paths +--- +[ + Path( + [ + Key( + "itf", + ), + Fragment( + "User", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_fragment.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_fragment.snap new file mode 100644 index 0000000000..ab5f39ec12 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_fragment.snap @@ -0,0 +1,13 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: doc +--- +query { + topProducts { + type + } + itf { + id + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_inline_fragment-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_inline_fragment-2.snap new file mode 100644 index 0000000000..43a002b1ba --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_inline_fragment-2.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: paths +--- +[ + Path( + [ + Key( + "itf", + ), + Fragment( + "User", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_inline_fragment.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_inline_fragment.snap new file mode 100644 index 0000000000..ab5f39ec12 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_inline_fragment.snap @@ -0,0 +1,13 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: doc +--- +query { + topProducts { + type + } + itf { + id + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__mutation-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__mutation-2.snap new file mode 100644 index 0000000000..974d958186 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__mutation-2.snap @@ -0,0 +1,13 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: paths +--- +[ + Path( + [ + Key( + "ping", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__mutation.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__mutation.snap new file mode 100644 index 0000000000..865d64decd --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__mutation.snap @@ -0,0 +1,5 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: doc +--- + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__query_field-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__query_field-2.snap new file mode 100644 index 0000000000..4a1bcd42be --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__query_field-2.snap @@ -0,0 +1,13 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: paths +--- +[ + Path( + [ + Key( + "me", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__query_field.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__query_field.snap new file mode 100644 index 0000000000..78ccae8dc0 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__query_field.snap @@ -0,0 +1,10 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: doc +--- +query { + topProducts { + type + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__scalar-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__scalar-2.snap new file mode 100644 index 0000000000..d4cfac90a7 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__scalar-2.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: paths +--- +[ + Path( + [ + Key( + "topProducts", + ), + Key( + "internal", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__scalar.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__scalar.snap new file mode 100644 index 0000000000..78ccae8dc0 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__scalar.snap @@ -0,0 +1,10 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: doc +--- +query { + topProducts { + type + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__test-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__test-2.snap new file mode 100644 index 0000000000..4327e9ef8b --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__test-2.snap @@ -0,0 +1,24 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: paths +--- +[ + Path( + [ + Key( + "topProducts", + ), + Key( + "reviews", + ), + Flatten, + ], + ), + Path( + [ + Key( + "customer", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__test.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__test.snap new file mode 100644 index 0000000000..78ccae8dc0 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__test.snap @@ -0,0 +1,10 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: doc +--- +query { + topProducts { + type + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__unauthenticated_request.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__unauthenticated_request.snap new file mode 100644 index 0000000000..67efe8e47e --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__unauthenticated_request.snap @@ -0,0 +1,29 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: response +--- +{ + "data": { + "orga": { + "id": 1, + "creatorUser": { + "id": 0, + "name": "Ada", + "phone": null + } + } + }, + "errors": [ + { + "message": "Unauthorized field or type", + "path": [ + "orga", + "creatorUser", + "phone" + ], + "extensions": { + "code": "UNAUTHORIZED_FIELD_OR_TYPE" + } + } + ] +} diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__unauthenticated_request_defer.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__unauthenticated_request_defer.snap new file mode 100644 index 0000000000..834762190d --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__unauthenticated_request_defer.snap @@ -0,0 +1,14 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: first_response +--- +{ + "data": { + "orga": { + "id": 1, + "creatorUser": { + "id": 0 + } + } + } +} diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__array-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__array-2.snap new file mode 100644 index 0000000000..31370c2aef --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__array-2.snap @@ -0,0 +1,17 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: paths +--- +[ + Path( + [ + Key( + "topProducts", + ), + Key( + "publicReviews", + ), + Flatten, + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__array.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__array.snap new file mode 100644 index 0000000000..930db08570 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__array.snap @@ -0,0 +1,10 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: doc +--- +query { + topProducts { + type + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__extract_scopes.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__extract_scopes.snap new file mode 100644 index 0000000000..c2b0de1dbf --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__extract_scopes.snap @@ -0,0 +1,11 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: doc +--- +{ + "internal", + "profile", + "read:user", + "read:username", + "test", +} diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-2.snap new file mode 100644 index 0000000000..f0192cbe84 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-2.snap @@ -0,0 +1,23 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: paths +--- +[ + Path( + [ + Key( + "topProducts", + ), + Key( + "internal", + ), + ], + ), + Path( + [ + Key( + "me", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-3.snap new file mode 100644 index 0000000000..930db08570 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-3.snap @@ -0,0 +1,10 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: doc +--- +query { + topProducts { + type + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-4.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-4.snap new file mode 100644 index 0000000000..f0192cbe84 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-4.snap @@ -0,0 +1,23 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: paths +--- +[ + Path( + [ + Key( + "topProducts", + ), + Key( + "internal", + ), + ], + ), + Path( + [ + Key( + "me", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-5.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-5.snap new file mode 100644 index 0000000000..c673d28a79 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-5.snap @@ -0,0 +1,14 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: doc +--- +query { + topProducts { + type + internal + } + me { + id + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-6.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-6.snap new file mode 100644 index 0000000000..e45e102000 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-6.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: paths +--- +[ + Path( + [ + Key( + "me", + ), + Key( + "name", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-7.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-7.snap new file mode 100644 index 0000000000..df7b1a5852 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-7.snap @@ -0,0 +1,14 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: doc +--- +query { + topProducts { + type + } + me { + id + name + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-8.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-8.snap new file mode 100644 index 0000000000..3b37534097 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-8.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: paths +--- +[ + Path( + [ + Key( + "topProducts", + ), + Key( + "internal", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query.snap new file mode 100644 index 0000000000..930db08570 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query.snap @@ -0,0 +1,10 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: doc +--- +query { + topProducts { + type + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-2.snap new file mode 100644 index 0000000000..3461adf398 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-2.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: paths +--- +[ + Path( + [ + Key( + "itf", + ), + Fragment( + "User", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-3.snap new file mode 100644 index 0000000000..ade7ec8f25 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-3.snap @@ -0,0 +1,17 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: doc +--- +query { + topProducts { + type + } + itf { + id + ...F + } +} +fragment F on User { + name +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-4.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-4.snap new file mode 100644 index 0000000000..f521439d55 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-4.snap @@ -0,0 +1,5 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: paths +--- +[] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment.snap new file mode 100644 index 0000000000..cd12631865 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment.snap @@ -0,0 +1,13 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: doc +--- +query { + topProducts { + type + } + itf { + id + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-2.snap new file mode 100644 index 0000000000..3461adf398 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-2.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: paths +--- +[ + Path( + [ + Key( + "itf", + ), + Fragment( + "User", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment.snap new file mode 100644 index 0000000000..cd12631865 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment.snap @@ -0,0 +1,13 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: doc +--- +query { + topProducts { + type + } + itf { + id + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__mutation-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__mutation-2.snap new file mode 100644 index 0000000000..ea3ede98ba --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__mutation-2.snap @@ -0,0 +1,13 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: paths +--- +[ + Path( + [ + Key( + "ping", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__mutation.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__mutation.snap new file mode 100644 index 0000000000..a241249702 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__mutation.snap @@ -0,0 +1,6 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: doc +--- + + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field-2.snap new file mode 100644 index 0000000000..70241af91d --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field-2.snap @@ -0,0 +1,13 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: paths +--- +[ + Path( + [ + Key( + "me", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field.snap new file mode 100644 index 0000000000..930db08570 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field.snap @@ -0,0 +1,10 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: doc +--- +query { + topProducts { + type + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__scalar-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__scalar-2.snap new file mode 100644 index 0000000000..3b37534097 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__scalar-2.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: paths +--- +[ + Path( + [ + Key( + "topProducts", + ), + Key( + "internal", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__scalar.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__scalar.snap new file mode 100644 index 0000000000..930db08570 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__scalar.snap @@ -0,0 +1,10 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: doc +--- +query { + topProducts { + type + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__tests__authenticated_directive-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__tests__authenticated_directive-2.snap new file mode 100644 index 0000000000..8794f28359 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__tests__authenticated_directive-2.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/plugins/authorization/tests.rs +expression: response +--- +{ + "data": { + "orga": { + "id": 1, + "creatorUser": { + "id": 0, + "name": "Ada", + "phone": "1234" + } + } + } +} diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__tests__authenticated_directive.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__tests__authenticated_directive.snap new file mode 100644 index 0000000000..a3394abe3b --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__tests__authenticated_directive.snap @@ -0,0 +1,39 @@ +--- +source: apollo-router/src/plugins/authorization/tests.rs +expression: response +--- +{ + "data": { + "orga": { + "id": null, + "creatorUser": { + "id": 0, + "name": "Ada", + "phone": null + } + } + }, + "errors": [ + { + "message": "Unauthorized field or type", + "path": [ + "orga", + "id" + ], + "extensions": { + "code": "UNAUTHORIZED_FIELD_OR_TYPE" + } + }, + { + "message": "Unauthorized field or type", + "path": [ + "orga", + "creatorUser", + "phone" + ], + "extensions": { + "code": "UNAUTHORIZED_FIELD_OR_TYPE" + } + } + ] +} diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__tests__scopes_directive-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__tests__scopes_directive-2.snap new file mode 100644 index 0000000000..0b4b8966eb --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__tests__scopes_directive-2.snap @@ -0,0 +1,29 @@ +--- +source: apollo-router/src/plugins/authorization/tests.rs +expression: response +--- +{ + "data": { + "orga": { + "id": 1, + "creatorUser": { + "id": 0, + "name": "Ada", + "phone": null + } + } + }, + "errors": [ + { + "message": "Unauthorized field or type", + "path": [ + "orga", + "creatorUser", + "phone" + ], + "extensions": { + "code": "UNAUTHORIZED_FIELD_OR_TYPE" + } + } + ] +} diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__tests__scopes_directive-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__tests__scopes_directive-3.snap new file mode 100644 index 0000000000..8794f28359 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__tests__scopes_directive-3.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/plugins/authorization/tests.rs +expression: response +--- +{ + "data": { + "orga": { + "id": 1, + "creatorUser": { + "id": 0, + "name": "Ada", + "phone": "1234" + } + } + } +} diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__tests__scopes_directive.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__tests__scopes_directive.snap new file mode 100644 index 0000000000..cb1a1b4cc4 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__tests__scopes_directive.snap @@ -0,0 +1,24 @@ +--- +source: apollo-router/src/plugins/authorization/tests.rs +expression: response +--- +{ + "data": { + "orga": { + "id": 1, + "creatorUser": null + } + }, + "errors": [ + { + "message": "Unauthorized field or type", + "path": [ + "orga", + "creatorUser" + ], + "extensions": { + "code": "UNAUTHORIZED_FIELD_OR_TYPE" + } + } + ] +} diff --git a/apollo-router/src/plugins/authorization/tests.rs b/apollo-router/src/plugins/authorization/tests.rs new file mode 100644 index 0000000000..de6ca69c83 --- /dev/null +++ b/apollo-router/src/plugins/authorization/tests.rs @@ -0,0 +1,527 @@ +use futures::StreamExt; +use http::header::ACCEPT; +use http::header::CONTENT_TYPE; +use serde_json_bytes::json; +use tower::ServiceExt; + +use crate::graphql; +use crate::plugin::test::MockSubgraph; +use crate::services::router; +use crate::services::supergraph; +use crate::Context; +use crate::MockedSubgraphs; +use crate::TestHarness; + +const SCHEMA: &str = r#"schema + @core(feature: "https://specs.apollo.dev/core/v0.1") + @core(feature: "https://specs.apollo.dev/join/v0.1") + @core(feature: "https://specs.apollo.dev/inaccessible/v0.1") + { + query: Query +} +directive @core(feature: String!) repeatable on SCHEMA +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION +directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE +directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__graph(name: String!, url: String!) on ENUM_VALUE +directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION +scalar join__FieldSet +enum join__Graph { + USER @join__graph(name: "user", url: "http://localhost:4001/graphql") + ORGA @join__graph(name: "orga", url: "http://localhost:4002/graphql") +} +type Query { + currentUser: User @join__field(graph: USER) + orga(id: ID): Organization @join__field(graph: ORGA) +} +type User +@join__owner(graph: USER) +@join__type(graph: ORGA, key: "id") +@join__type(graph: USER, key: "id"){ + id: ID! + name: String + phone: String + activeOrganization: Organization +} +type Organization +@join__owner(graph: ORGA) +@join__type(graph: ORGA, key: "id") +@join__type(graph: USER, key: "id") { + id: ID + creatorUser: User + name: String + nonNullId: ID! + suborga: [Organization] +}"#; + +#[tokio::test] +async fn authenticated_request() { + let subgraphs = MockedSubgraphs([ + ("user", MockSubgraph::builder().with_json( + serde_json::json!{{ + "query": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{name phone}}}", + "variables": { + "representations": [ + { "__typename": "User", "id":0 } + ], + } + }}, + serde_json::json! {{ + "data": { + "_entities":[ + { + "name":"Ada", + "phone": "1234" + } + ] + } + }}, + ).build()), + ("orga", MockSubgraph::builder().with_json( + serde_json::json!{{"query":"{orga(id:1){id creatorUser{__typename id}}}"}}, + serde_json::json!{{"data": {"orga": { "id": 1, "creatorUser": { "__typename": "User", "id": 0 } }}}} + ).build()) +].into_iter().collect()); + + let service = TestHarness::builder() + .configuration_json(serde_json::json!({ + "include_subgraph_errors": { + "all": true + }, + "authorization": { + "require_authentication": true + }})) + .unwrap() + .schema(SCHEMA) + .extra_plugin(subgraphs) + .build_supergraph() + .await + .unwrap(); + + let context = Context::new(); + context + .insert( + "apollo_authentication::JWT::claims", + "placeholder".to_string(), + ) + .unwrap(); + let request = supergraph::Request::fake_builder() + .query("query { orga(id: 1) { id creatorUser { id name phone } } }") + .variables( + json! {{ "isAuthenticated": true }} + .as_object() + .unwrap() + .clone(), + ) + .context(context) + .build() + .unwrap(); + let response = service + .oneshot(request) + .await + .unwrap() + .next_response() + .await + .unwrap(); + + insta::assert_json_snapshot!(response); +} + +#[tokio::test] +async fn unauthenticated_request() { + let subgraphs = MockedSubgraphs([ + ("user", MockSubgraph::builder().with_json( + serde_json::json!{{ + "query": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{name}}}", + "variables": { + "representations": [ + { "__typename": "User", "id":0 } + ], + } + }}, + serde_json::json! {{ + "data": { + "_entities":[ + { + "name":"Ada" + } + ] + } + }}, + ).build()), + ("orga", MockSubgraph::builder().with_json( + serde_json::json!{{"query":"{orga(id:1){id creatorUser{__typename id}}}"}}, + serde_json::json!{{"data": {"orga": { "id": 1, "creatorUser": { "__typename": "User", "id": 0 } }}}} + ).build()) +].into_iter().collect()); + + let service = TestHarness::builder() + .configuration_json(serde_json::json!({ + "include_subgraph_errors": { + "all": true + }, + "authorization": { + "require_authentication": true + }})) + .unwrap() + .schema(SCHEMA) + .extra_plugin(subgraphs) + .build_supergraph() + .await + .unwrap(); + + let context = Context::new(); + let request = supergraph::Request::fake_builder() + .query("query { orga(id: 1) { id creatorUser { id name phone } } }") + .variables( + json! {{ "isAuthenticated": false }} + .as_object() + .unwrap() + .clone(), + ) + .context(context) + // Request building here + .build() + .unwrap(); + let response = service + .oneshot(request) + .await + .unwrap() + .next_response() + .await + .unwrap(); + + insta::assert_json_snapshot!(response); +} + +const AUTHENTICATED_SCHEMA: &str = r#"schema + @core(feature: "https://specs.apollo.dev/core/v0.1") + @core(feature: "https://specs.apollo.dev/join/v0.1") + @core(feature: "https://specs.apollo.dev/inaccessible/v0.1") + { + query: Query +} +directive @core(feature: String!) repeatable on SCHEMA +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION +directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE +directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__graph(name: String!, url: String!) on ENUM_VALUE +directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION + +directive @authenticated on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM + +scalar join__FieldSet +enum join__Graph { + USER @join__graph(name: "user", url: "http://localhost:4001/graphql") + ORGA @join__graph(name: "orga", url: "http://localhost:4002/graphql") +} +type Query { + currentUser: User @join__field(graph: USER) + orga(id: ID): Organization @join__field(graph: ORGA) +} +type User +@join__owner(graph: USER) +@join__type(graph: ORGA, key: "id") +@join__type(graph: USER, key: "id"){ + id: ID! + name: String + phone: String @authenticated + activeOrganization: Organization +} +type Organization +@join__owner(graph: ORGA) +@join__type(graph: ORGA, key: "id") +@join__type(graph: USER, key: "id") { + id: ID @authenticated + creatorUser: User + name: String + nonNullId: ID! + suborga: [Organization] +}"#; + +#[tokio::test] +async fn authenticated_directive() { + let subgraphs = MockedSubgraphs([ + ("user", MockSubgraph::builder().with_json( + serde_json::json!{{ + "query": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{name}}}", + "variables": {"representations": [{ "__typename": "User", "id":0 }],} + }}, + serde_json::json! {{ "data": {"_entities":[{"name":"Ada" }] }}}, + ) + .with_json( + serde_json::json!{{ + "query": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{name phone}}}", + "variables": {"representations": [{ "__typename": "User", "id":0 }],} + }}, + serde_json::json! {{ "data": {"_entities":[{"name":"Ada", "phone": "1234"}] }}}, + ).build()), + ("orga", MockSubgraph::builder().with_json( + serde_json::json!{{"query":"{orga(id:1){creatorUser{__typename id}}}"}}, + serde_json::json!{{"data": {"orga": { "creatorUser": { "__typename": "User", "id": 0 } }}}} + ).with_json( + serde_json::json!{{"query":"{orga(id:1){id creatorUser{__typename id}}}"}}, + serde_json::json!{{"data": {"orga": { "id": 1, "creatorUser": { "__typename": "User", "id": 0 } }}}} + ).build()) +].into_iter().collect()); + + let service = TestHarness::builder() + .configuration_json(serde_json::json!({ + "include_subgraph_errors": { + "all": true + }, + "authorization": { + "experimental_enable_authorization_directives": true + }})) + .unwrap() + .schema(AUTHENTICATED_SCHEMA) + .extra_plugin(subgraphs) + .build_router() + .await + .unwrap(); + + let req = graphql::Request { + query: Some("query { orga(id: 1) { id creatorUser { id name phone } } }".to_string()), + variables: json! {{ "isAuthenticated": false }} + .as_object() + .unwrap() + .clone(), + ..Default::default() + }; + + let context = Context::new(); + let request = router::Request { + context, + router_request: http::Request::builder() + .method("POST") + .header(CONTENT_TYPE, "application/json") + .header(ACCEPT, "application/json") + .body(serde_json::to_vec(&req).unwrap().into()) + .unwrap(), + }; + + let response = service + .clone() + .oneshot(request) + .await + .unwrap() + .into_graphql_response_stream() + .await + .next() + .await + .unwrap() + .unwrap(); + + insta::assert_json_snapshot!(response); + + let context = Context::new(); + context + .insert( + "apollo_authentication::JWT::claims", + json! {{ "scope": "user:read" }}, + ) + .unwrap(); + let request = router::Request { + context, + router_request: http::Request::builder() + .method("POST") + .header(CONTENT_TYPE, "application/json") + .header(ACCEPT, "application/json") + .body(serde_json::to_vec(&req).unwrap().into()) + .unwrap(), + }; + + let response = service + .clone() + .oneshot(request) + .await + .unwrap() + .into_graphql_response_stream() + .await + .next() + .await + .unwrap() + .unwrap(); + + insta::assert_json_snapshot!(response); +} + +const SCOPES_SCHEMA: &str = r#"schema + @core(feature: "https://specs.apollo.dev/core/v0.1") + @core(feature: "https://specs.apollo.dev/join/v0.1") + @core(feature: "https://specs.apollo.dev/inaccessible/v0.1") + { + query: Query +} +directive @core(feature: String!) repeatable on SCHEMA +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION +directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on OBJECT | INTERFACE +directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE +directive @join__graph(name: String!, url: String!) on ENUM_VALUE +directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION + +directive @requiresScopes(scopes: [String]) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM + +scalar join__FieldSet +enum join__Graph { + USER @join__graph(name: "user", url: "http://localhost:4001/graphql") + ORGA @join__graph(name: "orga", url: "http://localhost:4002/graphql") +} +type Query { + currentUser: User @join__field(graph: USER) + orga(id: ID): Organization @join__field(graph: ORGA) +} +type User +@join__owner(graph: USER) +@join__type(graph: ORGA, key: "id") +@join__type(graph: USER, key: "id") +@requiresScopes(scopes: ["user:read"]) { + id: ID! + name: String + phone: String @requiresScopes(scopes: ["pii"]) + activeOrganization: Organization +} +type Organization +@join__owner(graph: ORGA) +@join__type(graph: ORGA, key: "id") +@join__type(graph: USER, key: "id") { + id: ID + creatorUser: User + name: String + nonNullId: ID! + suborga: [Organization] +}"#; + +#[tokio::test] +async fn scopes_directive() { + let subgraphs = MockedSubgraphs([ + ("user", MockSubgraph::builder().with_json( + serde_json::json!{{ + "query": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{name}}}", + "variables": {"representations": [{ "__typename": "User", "id":0 }],} + }}, + serde_json::json! {{ "data": { "_entities":[{"name":"Ada"}] } }}, + ).with_json( + serde_json::json!{{ + "query": "query($representations:[_Any!]!){_entities(representations:$representations){...on User{name phone}}}", + "variables": {"representations": [{ "__typename": "User", "id":0 }],} + }}, + serde_json::json! {{ "data": { "_entities":[{"name":"Ada", "phone": "1234"}] } }}, + ).build()), + ("orga", MockSubgraph::builder().with_json( + serde_json::json!{{"query":"{orga(id:1){id}}"}}, + serde_json::json!{{"data": {"orga": { "id": 1 }}}} + ).with_json( + serde_json::json!{{"query":"{orga(id:1){id creatorUser{__typename id}}}"}}, + serde_json::json!{{"data": {"orga": { "id": 1, "creatorUser": { "__typename": "User", "id": 0 } }}}} + ).build()) +].into_iter().collect()); + + let service = TestHarness::builder() + .configuration_json(serde_json::json!({ + "include_subgraph_errors": { + "all": true + }, + "authorization": { + "experimental_enable_authorization_directives": true + }})) + .unwrap() + .schema(SCOPES_SCHEMA) + .extra_plugin(subgraphs) + .build_router() + .await + .unwrap(); + + let req = graphql::Request { + query: Some("query { orga(id: 1) { id creatorUser { id name phone } } }".to_string()), + variables: json! {{ "isAuthenticated": false }} + .as_object() + .unwrap() + .clone(), + ..Default::default() + }; + let request = router::Request { + context: Context::new(), + router_request: http::Request::builder() + .method("POST") + .header(CONTENT_TYPE, "application/json") + .header(ACCEPT, "application/json") + .body(serde_json::to_vec(&req).unwrap().into()) + .unwrap(), + }; + + let response = service + .clone() + .oneshot(request) + .await + .unwrap() + .into_graphql_response_stream() + .await + .next() + .await + .unwrap() + .unwrap(); + + insta::assert_json_snapshot!(response); + + let context = Context::new(); + context + .insert( + "apollo_authentication::JWT::claims", + json! {{ "scope": "user:read" }}, + ) + .unwrap(); + let request = router::Request { + context, + router_request: http::Request::builder() + .method("POST") + .header(CONTENT_TYPE, "application/json") + .header(ACCEPT, "application/json") + .body(serde_json::to_vec(&req).unwrap().into()) + .unwrap(), + }; + + let response = service + .clone() + .oneshot(request) + .await + .unwrap() + .into_graphql_response_stream() + .await + .next() + .await + .unwrap() + .unwrap(); + + insta::assert_json_snapshot!(response); + + let context = Context::new(); + context + .insert( + "apollo_authentication::JWT::claims", + json! {{ "scope": "user:read pii" }}, + ) + .unwrap(); + let request = router::Request { + context, + router_request: http::Request::builder() + .method("POST") + .header(CONTENT_TYPE, "application/json") + .header(ACCEPT, "application/json") + .body(serde_json::to_vec(&req).unwrap().into()) + .unwrap(), + }; + + let response = service + .oneshot(request) + .await + .unwrap() + .into_graphql_response_stream() + .await + .next() + .await + .unwrap() + .unwrap(); + + insta::assert_json_snapshot!(response); +} diff --git a/apollo-router/src/plugins/mod.rs b/apollo-router/src/plugins/mod.rs index 2bd2270df1..df3252d3df 100644 --- a/apollo-router/src/plugins/mod.rs +++ b/apollo-router/src/plugins/mod.rs @@ -21,7 +21,7 @@ macro_rules! schemar_fn { } pub(crate) mod authentication; -mod authorization; +pub(crate) mod authorization; mod coprocessor; #[cfg(test)] mod coprocessor_test; diff --git a/apollo-router/src/plugins/traffic_shaping/deduplication.rs b/apollo-router/src/plugins/traffic_shaping/deduplication.rs index 59bd7e6255..4a5a38328a 100644 --- a/apollo-router/src/plugins/traffic_shaping/deduplication.rs +++ b/apollo-router/src/plugins/traffic_shaping/deduplication.rs @@ -17,7 +17,9 @@ use tower::ServiceExt; use crate::graphql::Request; use crate::http_ext; +use crate::json_ext::Object; use crate::query_planner::fetch::OperationKind; +use crate::query_planner::QUERY_PLANNER_CACHE_KEY_METADATA; use crate::services::SubgraphRequest; use crate::services::SubgraphResponse; @@ -35,8 +37,9 @@ where } } -type WaitMap = - Arc, Sender>>>>; +type CacheKey = (http_ext::Request, Object); + +type WaitMap = Arc>>>>; struct CloneSubgraphResponse(SubgraphResponse); @@ -73,7 +76,16 @@ where ) -> Result { loop { let mut locked_wait_map = wait_map.lock().await; - match locked_wait_map.get_mut(&(&request.subgraph_request).into()) { + let cache_key = ( + (&request.subgraph_request).into(), + request + .context + .get::<_, Object>(QUERY_PLANNER_CACHE_KEY_METADATA) + .unwrap_or_default() + .unwrap_or_default(), + ); + + match locked_wait_map.get_mut(&cache_key) { Some(waiter) => { // Register interest in key let mut receiver = waiter.subscribe(); @@ -97,11 +109,18 @@ where None => { let (tx, _rx) = broadcast::channel(1); - locked_wait_map.insert((&request.subgraph_request).into(), tx.clone()); + locked_wait_map.insert(cache_key, tx.clone()); drop(locked_wait_map); let context = request.context.clone(); - let http_request = (&request.subgraph_request).into(); + let cache_key = ( + (&request.subgraph_request).into(), + request + .context + .get::<_, Object>(QUERY_PLANNER_CACHE_KEY_METADATA) + .unwrap_or_default() + .unwrap_or_default(), + ); let res = { // when _drop_signal is dropped, either by getting out of the block, returning // the error from ready_oneshot or by cancellation, the drop_sentinel future will @@ -110,7 +129,7 @@ where tokio::task::spawn(async move { let _ = drop_sentinel.await; let mut locked_wait_map = wait_map.lock().await; - locked_wait_map.remove(&http_request); + locked_wait_map.remove(&cache_key); }); service diff --git a/apollo-router/src/query_planner/bridge_query_planner.rs b/apollo-router/src/query_planner/bridge_query_planner.rs index 25d13dc093..32df051f7d 100644 --- a/apollo-router/src/query_planner/bridge_query_planner.rs +++ b/apollo-router/src/query_planner/bridge_query_planner.rs @@ -26,7 +26,11 @@ use crate::error::QueryPlannerError; use crate::error::ServiceBuildError; use crate::graphql; use crate::introspection::Introspection; +use crate::json_ext::Object; +use crate::json_ext::Path; +use crate::plugins::authorization::AuthorizationPlugin; use crate::query_planner::labeler::add_defer_labels; +use crate::query_planner::QUERY_PLANNER_CACHE_KEY_METADATA; use crate::services::layers::query_analysis::Compiler; use crate::services::QueryPlannerContent; use crate::services::QueryPlannerRequest; @@ -46,6 +50,7 @@ pub(crate) struct BridgeQueryPlanner { schema: Arc, introspection: Option>, configuration: Arc, + enable_authorization_directives: bool, } impl BridgeQueryPlanner { @@ -107,10 +112,14 @@ impl BridgeQueryPlanner { } else { None }; + + let enable_authorization_directives = + AuthorizationPlugin::enable_directives(&configuration, &schema)?; Ok(Self { planner, schema, introspection, + enable_authorization_directives, configuration, }) } @@ -147,10 +156,13 @@ impl BridgeQueryPlanner { None }; + let enable_authorization_directives = + AuthorizationPlugin::enable_directives(&configuration, &schema)?; Ok(Self { planner, schema, introspection, + enable_authorization_directives, configuration, }) } @@ -165,10 +177,10 @@ impl BridgeQueryPlanner { async fn parse_selections( &self, - key: QueryKey, + query: String, + operation_name: Option, compiler: Arc>, ) -> Result { - let (query, operation_name) = key; let compiler_guard = compiler.lock().await; crate::spec::operation_limits::check( @@ -212,6 +224,7 @@ impl BridgeQueryPlanner { fragments, operations, filtered_query: None, + unauthorized_paths: vec![], subselections, defer_stats, is_original: true, @@ -387,9 +400,15 @@ impl Service for BridgeQueryPlanner { let res = this .get( - original_query, - filtered_query.to_string(), - operation_name.to_owned(), + QueryKey { + original_query, + filtered_query: filtered_query.to_string(), + operation_name: operation_name.to_owned(), + metadata: context + .get::<_, Object>(QUERY_PLANNER_CACHE_KEY_METADATA) + .unwrap_or_default() + .unwrap_or_default(), + }, compiler, ) .await; @@ -427,21 +446,35 @@ impl Service for BridgeQueryPlanner { } } +// Appease clippy::type_complexity +pub(crate) type FilteredQuery = (String, Vec, Arc>); + impl BridgeQueryPlanner { async fn get( &self, - original_query: String, - filtered_query: String, - operation_name: Option, - compiler: Arc>, + mut key: QueryKey, + mut compiler: Arc>, ) -> Result { + let filter_res = if self.enable_authorization_directives { + AuthorizationPlugin::filter_query(&key, &self.schema)? + } else { + None + }; + let mut selections = self .parse_selections( - (original_query.clone(), operation_name.clone()), + key.original_query.clone(), + key.operation_name.clone(), compiler.clone(), ) .await?; + if let Some((filtered_query, unauthorized_paths, new_compiler)) = filter_res { + key.filtered_query = filtered_query; + compiler = new_compiler; + selections.unauthorized_paths = unauthorized_paths; + } + if selections.contains_introspection() { // If we have only one operation containing only the root field `__typename` // (possibly aliased or repeated). (This does mean we fail to properly support @@ -461,20 +494,29 @@ impl BridgeQueryPlanner { response: Box::new(graphql::Response::builder().data(data).build()), }); } else { - return self.introspection(original_query).await; + return self.introspection(key.original_query).await; } } - if filtered_query != original_query { + if key.filtered_query != key.original_query { let mut filtered = self - .parse_selections((filtered_query.clone(), operation_name.clone()), compiler) + .parse_selections( + key.filtered_query.clone(), + key.operation_name.clone(), + compiler, + ) .await?; filtered.is_original = false; selections.filtered_query = Some(Arc::new(filtered)); } - self.plan(original_query, filtered_query, operation_name, selections) - .await + self.plan( + key.original_query, + key.filtered_query, + key.operation_name, + selections, + ) + .await } } @@ -562,24 +604,29 @@ mod tests { #[test(tokio::test)] async fn empty_query_plan_should_be_a_planner_error() { - let query = Query::parse( - include_str!("testdata/unknown_introspection_query.graphql"), - &Schema::parse(EXAMPLE_SCHEMA, &Default::default()).unwrap(), - &Configuration::default(), - ) - .unwrap(); - let err = BridgeQueryPlanner::new(EXAMPLE_SCHEMA.to_string(), Default::default()) + let schema = Schema::parse(EXAMPLE_SCHEMA, &Default::default()).unwrap(); + let query = include_str!("testdata/unknown_introspection_query.graphql"); + + let planner = BridgeQueryPlanner::new(EXAMPLE_SCHEMA.to_string(), Default::default()) + .await + .unwrap(); + + let compiler = Query::make_compiler(query, &schema, &Configuration::default()).0; + + let selections = planner + .parse_selections(query.to_string(), None, Arc::new(Mutex::new(compiler))) .await - .unwrap() + .unwrap(); + let err = // test the planning part separately because it is a valid introspection query // it should be caught by the introspection part, but just in case, we check // that the query planner would return an empty plan error if it received an // introspection query - .plan( + planner.plan( include_str!("testdata/unknown_introspection_query.graphql").to_string(), include_str!("testdata/unknown_introspection_query.graphql").to_string(), None, - query, + selections, ) .await .unwrap_err(); @@ -947,9 +994,12 @@ mod tests { planner .get( - original_query.to_string(), - filtered_query.to_string(), - operation_name, + QueryKey { + original_query: original_query.to_string(), + filtered_query: filtered_query.to_string(), + operation_name, + metadata: Object::new(), + }, Arc::new(Mutex::new(compiler)), ) .await diff --git a/apollo-router/src/query_planner/caching_query_planner.rs b/apollo-router/src/query_planner/caching_query_planner.rs index 717dbf74f5..e0621cc88c 100644 --- a/apollo-router/src/query_planner/caching_query_planner.rs +++ b/apollo-router/src/query_planner/caching_query_planner.rs @@ -20,6 +20,7 @@ use tracing::Instrument; use crate::cache::DeduplicatingCache; use crate::error::CacheResolverError; use crate::error::QueryPlannerError; +use crate::json_ext::Object; use crate::query_planner::labeler::add_defer_labels; use crate::query_planner::BridgeQueryPlanner; use crate::query_planner::QueryPlanResult; @@ -36,6 +37,7 @@ use crate::spec::SpecError; use crate::Configuration; use crate::Context; +pub(crate) const QUERY_PLANNER_CACHE_KEY_METADATA: &str = "QUERY_PLANNER_CACHE_KEY_METADATA"; /// An [`IndexMap`] of available plugins. pub(crate) type Plugins = IndexMap>; @@ -83,18 +85,23 @@ where } } - pub(crate) async fn cache_keys(&self, count: usize) -> Vec<(String, Option)> { + pub(crate) async fn cache_keys(&self, count: usize) -> Vec { let keys = self.cache.in_memory_keys().await; keys.into_iter() .take(count) - .map(|key| (key.query, key.operation)) + .map(|key| WarmUpCachingQueryKey { + query: key.query, + operation: key.operation, + metadata: key.metadata, + }) .collect() } pub(crate) async fn warm_up( &mut self, query_analysis: &QueryAnalysisLayer, - cache_keys: Vec<(String, Option)>, + //FIXME: use cache keys that return is_authenticated and scopes + cache_keys: Vec, ) { let schema_id = self.schema.schema_id.clone(); @@ -108,11 +115,17 @@ where ); let mut count = 0usize; - for (mut query, operation) in cache_keys { + for WarmUpCachingQueryKey { + mut query, + operation, + metadata, + } in cache_keys + { let caching_key = CachingQueryKey { schema_id: schema_id.clone(), query: query.clone(), operation: operation.clone(), + metadata, }; let context = Context::new(); @@ -198,6 +211,11 @@ where schema_id, query: request.query.clone(), operation: request.operation_name.to_owned(), + metadata: request + .context + .get::<_, Object>(QUERY_PLANNER_CACHE_KEY_METADATA) + .unwrap_or_default() + .unwrap_or_default(), }; let context = request.context.clone(); @@ -361,6 +379,7 @@ pub(crate) struct CachingQueryKey { pub(crate) schema_id: Option, pub(crate) query: String, pub(crate) operation: Option, + pub(crate) metadata: Object, } impl std::fmt::Display for CachingQueryKey { @@ -372,17 +391,26 @@ impl std::fmt::Display for CachingQueryKey { let mut hasher = Sha256::new(); hasher.update(self.operation.as_deref().unwrap_or("-")); let operation = hex::encode(hasher.finalize()); - + let metadata = + serde_json::to_string(&self.metadata).expect("serialization should not fail"); write!( f, - "plan.{}.{}.{}", + "plan.{}.{}.{}.{}", self.schema_id.as_deref().unwrap_or("-"), query, - operation + operation, + metadata, ) } } +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub(crate) struct WarmUpCachingQueryKey { + pub(crate) query: String, + pub(crate) operation: Option, + pub(crate) metadata: Object, +} + #[cfg(test)] mod tests { use mockall::mock; diff --git a/apollo-router/src/query_planner/plan.rs b/apollo-router/src/query_planner/plan.rs index 506904b681..6383acf11c 100644 --- a/apollo-router/src/query_planner/plan.rs +++ b/apollo-router/src/query_planner/plan.rs @@ -15,7 +15,13 @@ use crate::spec::Query; /// A planner key. /// /// This type consists of a query string and an optional operation string -pub(crate) type QueryKey = (String, Option); +#[derive(Clone)] +pub(crate) struct QueryKey { + pub(crate) filtered_query: String, + pub(crate) original_query: String, + pub(crate) operation_name: Option, + pub(crate) metadata: Object, +} /// A plan for a given GraphQL query #[derive(Debug, Serialize, Deserialize)] diff --git a/apollo-router/src/services/execution_service.rs b/apollo-router/src/services/execution_service.rs index 8662e9eb69..9ae612171e 100644 --- a/apollo-router/src/services/execution_service.rs +++ b/apollo-router/src/services/execution_service.rs @@ -21,12 +21,15 @@ use tower::BoxError; use tower::ServiceBuilder; use tower::ServiceExt; use tower_service::Service; +use tracing::event; use tracing::Instrument; +use tracing_core::Level; use super::layers::allow_only_http_post_mutations::AllowOnlyHttpPostMutationsLayer; use super::new_service::ServiceFactory; use super::Plugins; use super::SubgraphServiceFactory; +use crate::graphql::Error; use crate::graphql::IncrementalResponse; use crate::graphql::Response; use crate::json_ext::Object; @@ -216,6 +219,11 @@ impl ExecutionService { tracing::debug_span!("format_response").in_scope(|| { let mut paths = Vec::new(); if let Some(filtered_query) = query.filtered_query.as_ref() { + let unauthorized_paths = query.unauthorized_paths.iter().map(|path| path.to_string()).collect::>(); + if !unauthorized_paths.is_empty() { + event!(Level::ERROR, unauthorized_query_paths = ?unauthorized_paths, "Authorization error",); + } + paths = filtered_query.format_response( &mut response, operation_name, @@ -223,6 +231,13 @@ impl ExecutionService { schema.api_schema(), variables_set, ); + + for path in &query.unauthorized_paths { + response.errors.push(Error::builder() + .message("Unauthorized field or type") + .path(path.clone()) + .extension_code("UNAUTHORIZED_FIELD_OR_TYPE").build()); + } } paths.extend( diff --git a/apollo-router/src/services/layers/query_analysis.rs b/apollo-router/src/services/layers/query_analysis.rs index 8e9de8ada0..8a23afc163 100644 --- a/apollo-router/src/services/layers/query_analysis.rs +++ b/apollo-router/src/services/layers/query_analysis.rs @@ -8,6 +8,7 @@ use lru::LruCache; use tokio::sync::Mutex; use crate::context::OPERATION_NAME; +use crate::plugins::authorization::AuthorizationPlugin; use crate::services::SupergraphRequest; use crate::services::SupergraphResponse; use crate::spec::Query; @@ -22,6 +23,7 @@ pub(crate) struct QueryAnalysisLayer { schema: Arc, configuration: Arc, cache: Arc>)>>>, + enable_authorization_directives: bool, } #[derive(Debug, Clone, Hash, PartialEq, Eq)] @@ -32,6 +34,8 @@ struct QueryAnalysisKey { impl QueryAnalysisLayer { pub(crate) async fn new(schema: Arc, configuration: Arc) -> Self { + let enable_authorization_directives = + AuthorizationPlugin::enable_directives(&configuration, &schema).unwrap_or(false); Self { schema, cache: Arc::new(Mutex::new(LruCache::new( @@ -42,6 +46,7 @@ impl QueryAnalysisLayer { .in_memory .limit, ))), + enable_authorization_directives, configuration, } } @@ -110,6 +115,16 @@ impl QueryAnalysisLayer { context.insert(OPERATION_NAME, operation_name).unwrap(); + if self.enable_authorization_directives { + AuthorizationPlugin::query_analysis( + &query, + &self.schema, + &self.configuration, + &context, + ) + .await; + } + (*self.cache.lock().await).put( QueryAnalysisKey { query, diff --git a/apollo-router/src/services/router_service.rs b/apollo-router/src/services/router_service.rs index e6148acb09..c883b2f933 100644 --- a/apollo-router/src/services/router_service.rs +++ b/apollo-router/src/services/router_service.rs @@ -50,6 +50,7 @@ use crate::plugin::test::MockSupergraphService; use crate::protocols::multipart::Multipart; use crate::protocols::multipart::ProtocolMode; use crate::query_planner::QueryPlanResult; +use crate::query_planner::WarmUpCachingQueryKey; use crate::router_factory::RouterFactory; use crate::services::layers::content_negociation::GRAPHQL_JSON_RESPONSE_HEADER_VALUE; use crate::services::RouterRequest; @@ -521,7 +522,7 @@ impl RouterCreator { } impl RouterCreator { - pub(crate) async fn cache_keys(&self, count: usize) -> Vec<(String, Option)> { + pub(crate) async fn cache_keys(&self, count: usize) -> Vec { self.supergraph_creator.cache_keys(count).await } diff --git a/apollo-router/src/services/supergraph_service.rs b/apollo-router/src/services/supergraph_service.rs index 486b0bf25f..3ee62b1ac0 100644 --- a/apollo-router/src/services/supergraph_service.rs +++ b/apollo-router/src/services/supergraph_service.rs @@ -36,6 +36,7 @@ use crate::plugins::traffic_shaping::APOLLO_TRAFFIC_SHAPING; use crate::query_planner::BridgeQueryPlanner; use crate::query_planner::CachingQueryPlanner; use crate::query_planner::QueryPlanResult; +use crate::query_planner::WarmUpCachingQueryKey; use crate::services::query_planner; use crate::services::supergraph; use crate::services::ExecutionRequest; @@ -460,7 +461,7 @@ impl SupergraphCreator { ) } - pub(crate) async fn cache_keys(&self, count: usize) -> Vec<(String, Option)> { + pub(crate) async fn cache_keys(&self, count: usize) -> Vec { self.query_planner_service.cache_keys(count).await } @@ -471,7 +472,7 @@ impl SupergraphCreator { pub(crate) async fn warm_up_query_planner( &mut self, query_parser: &QueryAnalysisLayer, - cache_keys: Vec<(String, Option)>, + cache_keys: Vec, ) { self.query_planner_service .warm_up(query_parser, cache_keys) diff --git a/apollo-router/src/spec/mod.rs b/apollo-router/src/spec/mod.rs index 85bb93b0f7..4589813c91 100644 --- a/apollo-router/src/spec/mod.rs +++ b/apollo-router/src/spec/mod.rs @@ -43,12 +43,14 @@ pub(crate) enum SpecError { SubscriptionNotSupported, } +pub(crate) const GRAPHQL_VALIDATION_FAILURE_ERROR_KEY: &str = "## GraphQLValidationFailure\n"; + impl SpecError { pub(crate) const fn get_error_key(&self) -> &'static str { match self { SpecError::ParsingError(_) => "## GraphQLParseFailure\n", SpecError::UnknownOperation(_) => "## GraphQLUnknownOperationName\n", - _ => "## GraphQLValidationFailure\n", + _ => GRAPHQL_VALIDATION_FAILURE_ERROR_KEY, } } } diff --git a/apollo-router/src/spec/query.rs b/apollo-router/src/spec/query.rs index fa3514290b..10b9775310 100644 --- a/apollo-router/src/spec/query.rs +++ b/apollo-router/src/spec/query.rs @@ -60,6 +60,8 @@ pub(crate) struct Query { #[derivative(PartialEq = "ignore", Hash = "ignore")] pub(crate) subselections: HashMap, #[derivative(PartialEq = "ignore", Hash = "ignore")] + pub(crate) unauthorized_paths: Vec, + #[derivative(PartialEq = "ignore", Hash = "ignore")] pub(crate) filtered_query: Option>, #[derivative(PartialEq = "ignore", Hash = "ignore")] pub(crate) defer_stats: DeferStats, @@ -93,6 +95,7 @@ impl Query { }, operations: Vec::new(), subselections: HashMap::new(), + unauthorized_paths: Vec::new(), filtered_query: None, defer_stats: DeferStats { has_defer: false, @@ -286,6 +289,7 @@ impl Query { fragments, operations, subselections: HashMap::new(), + unauthorized_paths: Vec::new(), filtered_query: None, defer_stats, is_original: true, diff --git a/apollo-router/src/spec/query/tests.rs b/apollo-router/src/spec/query/tests.rs index 5f23c4e86c..6da93dc0d4 100644 --- a/apollo-router/src/spec/query/tests.rs +++ b/apollo-router/src/spec/query/tests.rs @@ -5545,6 +5545,7 @@ fn filtered_defer_fragment() { subselections, defer_stats, is_original: true, + unauthorized_paths: vec![], validation_error: None, }; @@ -5569,6 +5570,7 @@ fn filtered_defer_fragment() { subselections, defer_stats, is_original: false, + unauthorized_paths: vec![], validation_error: None, }; diff --git a/apollo-router/src/uplink/license_enforcement.rs b/apollo-router/src/uplink/license_enforcement.rs index f6c328fa1e..71d1c8c978 100644 --- a/apollo-router/src/uplink/license_enforcement.rs +++ b/apollo-router/src/uplink/license_enforcement.rs @@ -134,6 +134,10 @@ impl LicenseEnforcementReport { .path("$.authentication") .name("Authentication plugin") .build(), + ConfigurationRestriction::builder() + .path("$.authorization.experimental_enable_authorization_directives") + .name("Authorization directives") + .build(), ConfigurationRestriction::builder() .path("$.coprocessor") .name("Coprocessor plugin") diff --git a/apollo-router/tests/redis_test.rs b/apollo-router/tests/redis_test.rs index fccd0a93fe..f3f72f012f 100644 --- a/apollo-router/tests/redis_test.rs +++ b/apollo-router/tests/redis_test.rs @@ -52,7 +52,7 @@ mod test { let _ = supergraph.oneshot(request).await?.next_response().await; let s:String = connection - .get("plan.5abb5fecf7df056396fb90fdf38d430b8c1fec55ec132fde878161608af18b76.4c45433039407593557f8a982dafd316a66ec03f0e1ed5fa1b7ef8060d76e8ec.3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112") + .get("plan.5abb5fecf7df056396fb90fdf38d430b8c1fec55ec132fde878161608af18b76.4c45433039407593557f8a982dafd316a66ec03f0e1ed5fa1b7ef8060d76e8ec.3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112.{}") .await .unwrap(); let query_plan_res: serde_json::Value = serde_json::from_str(&s).unwrap(); diff --git a/apollo-router/tests/snapshots/lifecycle_tests__cli_config_experimental.snap b/apollo-router/tests/snapshots/lifecycle_tests__cli_config_experimental.snap index 8f377f6c81..864887ca6b 100644 --- a/apollo-router/tests/snapshots/lifecycle_tests__cli_config_experimental.snap +++ b/apollo-router/tests/snapshots/lifecycle_tests__cli_config_experimental.snap @@ -9,6 +9,7 @@ stderr: stdout: List of all experimental configurations with related GitHub discussions: + - experimental_enable_authorization_directives: https://github.com/apollographql/router/discussions/??? - experimental_http_max_request_bytes: https://github.com/apollographql/router/discussions/3220 - experimental_logging: https://github.com/apollographql/router/discussions/1961 - experimental_response_trace_id: https://github.com/apollographql/router/discussions/2147 diff --git a/docs/source/config.json b/docs/source/config.json index ae2e27627c..00f3fa5b5e 100644 --- a/docs/source/config.json +++ b/docs/source/config.json @@ -42,6 +42,13 @@ "enterprise" ] ], + "Authorization": [ + "/configuration/authorization", + [ + "enterprise", + "experimental" + ] + ], "Operation limits": [ "/configuration/operation-limits", [ diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx new file mode 100644 index 0000000000..477c10abc3 --- /dev/null +++ b/docs/source/configuration/authorization.mdx @@ -0,0 +1,302 @@ +--- +title: Authorization in the Apollo Router +--- + +> ⚠️ **This is an [Enterprise feature](../enterprise-features/) of the Apollo Router.** It requires an organization with a [GraphOS Enterprise plan](https://www.apollographql.com/pricing/). +> +> If your organization _doesn't_ currently have an Enterprise plan, you can test out this functionality by signing up for a free [Enterprise trial](/graphos/org/plans/#enterprise-trials). + +The Apollo Router supports graph based authorization policies, through the `@authenticated` and `@requiresScopes` directives. They are used to authorize access to specific types or fields. + +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 +``` + +To use them, you need either to configure [JWT authentication](./authn-jwt) or to add a [router service coprocessor](../customizations/coprocessor). THe JWT plugin will validate the token, extract its claims and set them in the request context at the key `apollo_authentication::JWT::claims`. To participate in the authorization process, a coprocessor would need to set that key in the context. + +## `@authenticated` + +The `@authenticated` directive restricts access to fields and types if the request was not authenticated, by checking for the presence of the `apollo_authentication::JWT::claims` key in the request context. If those fields are restricted, the router will remove them entirely before planning the query, so the unauthenticated parts will still be executed and returned to the client, but the parts requiring authentication will never be requested from subgraphs, avoiding entire subgraph requests in some cases. + +As an example, assuming we have this schema: + +```graphql +type Query { + me: User @authenticated + user(id: ID): User +} + +type User { + id: ID + name: String + email: @authenticated +} +``` + +And this query: + +```graphql +{ + me { + name + email + } + + user(id: 1234) { + name + email + } +} +``` + +If the request was authenticated, the entire query would be executed, as expected. But if it wasn't, the router would remove fields before execution, and create this filtered query: + +```graphql +{ + user(id: 1234) { + name + } +} +``` + +So the `me` top level operation would not even be executed. When returning the response, the router will follow the initial request's shape and insert a null in the unauthorized fields, and apply the null propagation rules. It will generate a response of the following shape: + +```json +{ + "data": { + "me": null, + "user": { + "name": "Ada", + "email": null + } + }, + "errors": [ + { + "message": "Unauthorized field or type", + "path": [ + "me" + ], + "extensions": { + "code": "UNAUTHORIZED_FIELD_OR_TYPE" + } + }, + { + "message": "Unauthorized field or type", + "path": [ + "user", + "email" + ], + "extensions": { + "code": "UNAUTHORIZED_FIELD_OR_TYPE" + } + } + ] +} +``` + +If all the fields were removed, then the router would generate a query planner error indicating that the query is unauthorized. + +## `@requiresScopes` + +The `@requiresScopes` directive restricts access to fields and types if the request did not present the right set of scopes, by loading from the request the context the object at key `apollo_authentication::JWT::claims`, and in that object the `scope` key, in the format defined by [OAuth2 access token scopes](https://datatracker.ietf.org/doc/html/rfc6749#section-3.3), as a space separated list of scopes. + +Depending on which scope set is presented by the request, different parts of the query may be available. + +Assuming we have this schema: + +```graphql +type Query { + me: User @requiresScopes(scopes: ["profile:read"]) + products: [Products] +} + +type User { + id: ID + name: String + email: @requiresScopes(["user:read"]) +} + +type Product { + id: ID + name: String + amount: Int @requiresScopes(["employee", "inventory"]) + orders: [User] @requiresScopes(["employee"]) +} +``` + +And this query: + +```graphql +{ + me { + name + email + } + + products { + name + amount + orders { + name + } + } +} +``` + +If the request presented the scopes `profile:read user:read`, then it would be interpreted as: + + +```graphql +{ + me { + name + email + } + + products { + name + } +} +``` + +And generate an error at path `/products/@/amount`. + +While if it presented the scopes `employee inventory`, it would filter the query as: + +```graphql +{ + products { + name + amount + orders { + name + } + } +} +``` + +And generate an error at path `/me`. + + +## Composition and federation + +The authorization directives are defined by the subgraph author, and will be carried into the supergraph schema: +- `@authenticated`: if defined on a field or type by any of the subgraphs, it will be set in the supergraph too +- `@requiresScopes`: same as `@authenticated`, and if there are multiple applications, the supergraph schema will merge the sets of scopes required by each subgraph + +### Authorization and `@key` + +When the authorization directives are set on fields used in `@key`, they will be usable by the router to join between subgraphs, but cannot be queried directly by the client. This behavior is similar to [Contracts](/graphos/delivery/contracts/). + +As an example, assuming we have these subgraphs definition: + +```graphql +# subgraph A +type Query { + t: T +} + +type T @key(fields: "id") { + id: ID! @authenticated +} +``` + +```graphql +# subgraph B +type T @key(fields: "id") { + id: ID! @authenticated + value: Int +} +``` + +The following query can be done when not authenticated: + +```graphql +{ + t { + value + } +} +``` + +But this one would end up with a `null` in `id`, which would trigger nullability rules and nullify `t` too. + +```graphql +{ + t { + id + value + } +} +``` + +### Interfaces + +In the case one of the types implementing an interface requires authorization, then querying the interface will be allowed, but any parts that require access to that type will be filtered. + +As an example, with this schema, where the interface `I` does not require authentication, but the `User` type does: + +```graphql +type Query { + itf: I! +} + +interface I { + id: ID +} + +type User +implements I +@authenticated { + id: ID + name: String +} +``` + +If we send this query with a fragment and a type condition on `User`: + +```graphql +query { + topProducts { + type + } + itf { + id + ...F + } +} + +fragment F on User { + name +} +``` + +The query would be filtered as follows: + +```graphql +query { + topProducts { + type + } + itf { + id + } +} +``` + +### Introspection + +Introspection is not affected by authorization, so all types fields will be accessible. The directives applied to them will not be visible though. If introspection might reveal too much information about internal types, then it should be deactivated like this: + +```yaml +supergraph: + introspection: false +``` + +### Query deduplication + +When [query deduplication](/configuration/traffic-shaping/#query-deduplication) is activated for subgraphs, the authorization status is accounted for: unauthenticated queries will be grouped together, and authenticated queries will be grouped by scope set. \ No newline at end of file From d2c5107ae8e9257e4d5a3b6cd6ab2bce69e07478 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Fri, 7 Jul 2023 17:48:36 +0200 Subject: [PATCH 02/82] changeset --- .../feat_geal_authorization_directives.md | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .changesets/feat_geal_authorization_directives.md diff --git a/.changesets/feat_geal_authorization_directives.md b/.changesets/feat_geal_authorization_directives.md new file mode 100644 index 0000000000..3af6e10851 --- /dev/null +++ b/.changesets/feat_geal_authorization_directives.md @@ -0,0 +1,21 @@ +### GraphOS Enterprise: authorization directives ([PR #3397](https://github.com/apollographql/router/pull/3397)) + +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 + +By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/3397 \ No newline at end of file From 19a332561c9264c6bb923ad20831670087496c88 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Fri, 7 Jul 2023 18:31:28 +0200 Subject: [PATCH 03/82] Apply suggestions from code review Co-authored-by: Lenny Burdette --- docs/source/configuration/authorization.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 477c10abc3..6d7a12b712 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -16,7 +16,7 @@ directive @authenticated on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENU directive @requiresScopes(scopes: [String!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM ``` -To use them, you need either to configure [JWT authentication](./authn-jwt) or to add a [router service coprocessor](../customizations/coprocessor). THe JWT plugin will validate the token, extract its claims and set them in the request context at the key `apollo_authentication::JWT::claims`. To participate in the authorization process, a coprocessor would need to set that key in the context. +To use them, you need either to configure [JWT authentication](./authn-jwt) or to add a [router service coprocessor](../customizations/coprocessor). The JWT plugin will validate the token, extract its claims and set them in the request context at the key `apollo_authentication::JWT::claims`. To participate in the authorization process, a coprocessor would need to set that key in the context. ## `@authenticated` @@ -190,7 +190,7 @@ The authorization directives are defined by the subgraph author, and will be car ### Authorization and `@key` -When the authorization directives are set on fields used in `@key`, they will be usable by the router to join between subgraphs, but cannot be queried directly by the client. This behavior is similar to [Contracts](/graphos/delivery/contracts/). +When the authorization directives are set on fields used in `@key`, they will be usable by the router to join between subgraphs, but cannot be queried directly by the client. This behavior is similar to [Contracts](/graphos/delivery/contracts/) and the [`@inaccessible` directive](https://www.apollographql.com/docs/federation/federated-types/federated-directives/#inaccessible). As an example, assuming we have these subgraphs definition: From dfe2c9c0c658fb0380a94798cc2529bf4d7e0292 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Fri, 7 Jul 2023 18:35:39 +0200 Subject: [PATCH 04/82] simplify examples --- docs/source/configuration/authorization.mdx | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 6d7a12b712..fb866c5a81 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -257,31 +257,23 @@ implements I } ``` -If we send this query with a fragment and a type condition on `User`: +If we send this query with an inline fragment and a type condition on `User`: ```graphql query { - topProducts { - type - } itf { id - ...F + ... on User { + name + } } } - -fragment F on User { - name -} ``` The query would be filtered as follows: ```graphql query { - topProducts { - type - } itf { id } From 225ebd7f31fd18a828be993bf78279ccb6578560 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Mon, 10 Jul 2023 16:29:39 +0200 Subject: [PATCH 05/82] test scope extraction on all test queries this uncovers an issue with type condition on fragments, fragment spreads and inline fragments: we should check if the type is authorized there --- .../src/plugins/authorization/scopes.rs | 21 +++++++++++++ ...authorization__scopes__tests__array-2.snap | 21 +++++-------- ...authorization__scopes__tests__array-3.snap | 17 ++++++++++ ...__authorization__scopes__tests__array.snap | 9 +++--- ...__scopes__tests__filter_basic_query-2.snap | 27 +++++----------- ...__scopes__tests__filter_basic_query-3.snap | 27 +++++++++++----- ...__scopes__tests__filter_basic_query-4.snap | 27 +++++----------- ...__scopes__tests__filter_basic_query-5.snap | 31 ++++++++++++------- ...__scopes__tests__filter_basic_query-6.snap | 24 +++++++------- ...__scopes__tests__filter_basic_query-7.snap | 24 +++++++------- ...__scopes__tests__filter_basic_query-8.snap | 24 +++++++------- ...__scopes__tests__filter_basic_query-9.snap | 16 ++++++++++ ...on__scopes__tests__filter_basic_query.snap | 11 ++++--- ...on__scopes__tests__interface_fragment.snap | 11 ++----- ...pes__tests__interface_inline_fragment.snap | 11 ++----- ...horization__scopes__tests__mutation-2.snap | 12 ++----- ...horization__scopes__tests__mutation-3.snap | 13 ++++++++ ...uthorization__scopes__tests__mutation.snap | 7 +++-- ...ization__scopes__tests__query_field-2.snap | 17 +++++----- ...ization__scopes__tests__query_field-3.snap | 13 ++++++++ ...orization__scopes__tests__query_field.snap | 9 +++--- ...uthorization__scopes__tests__scalar-2.snap | 20 +++++------- ...uthorization__scopes__tests__scalar-3.snap | 16 ++++++++++ ..._authorization__scopes__tests__scalar.snap | 8 ++--- 24 files changed, 236 insertions(+), 180 deletions(-) create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__array-3.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-9.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__mutation-3.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field-3.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__scalar-3.snap diff --git a/apollo-router/src/plugins/authorization/scopes.rs b/apollo-router/src/plugins/authorization/scopes.rs index da67a136ea..e77891e61b 100644 --- a/apollo-router/src/plugins/authorization/scopes.rs +++ b/apollo-router/src/plugins/authorization/scopes.rs @@ -399,6 +399,9 @@ mod tests { } "#; + let doc = extract(QUERY); + insta::assert_debug_snapshot!(doc); + let (doc, paths) = filter(QUERY, HashSet::new()); insta::assert_display_snapshot!(doc); insta::assert_debug_snapshot!(paths); @@ -450,6 +453,9 @@ mod tests { } "#; + let doc = extract(QUERY); + insta::assert_debug_snapshot!(doc); + let (doc, paths) = filter(QUERY, HashSet::new()); insta::assert_display_snapshot!(doc); @@ -470,6 +476,9 @@ mod tests { } "#; + let doc = extract(QUERY); + insta::assert_debug_snapshot!(doc); + let (doc, paths) = filter(QUERY, HashSet::new()); insta::assert_display_snapshot!(doc); @@ -487,6 +496,9 @@ mod tests { } "#; + let doc = extract(QUERY); + insta::assert_debug_snapshot!(doc); + let (doc, paths) = filter(QUERY, HashSet::new()); insta::assert_display_snapshot!(doc); @@ -509,6 +521,9 @@ mod tests { } "#; + let doc = extract(QUERY); + insta::assert_debug_snapshot!(doc); + let (doc, paths) = filter(QUERY, HashSet::new()); insta::assert_display_snapshot!(doc); @@ -531,6 +546,9 @@ mod tests { } "#; + let doc = extract(QUERY); + insta::assert_debug_snapshot!(doc); + let (doc, paths) = filter(QUERY, HashSet::new()); insta::assert_display_snapshot!(doc); @@ -555,6 +573,9 @@ mod tests { } "#; + let doc = extract(QUERY); + insta::assert_debug_snapshot!(doc); + let (doc, paths) = filter(QUERY, HashSet::new()); insta::assert_display_snapshot!(doc); diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__array-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__array-2.snap index 31370c2aef..930db08570 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__array-2.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__array-2.snap @@ -1,17 +1,10 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: paths +expression: doc --- -[ - Path( - [ - Key( - "topProducts", - ), - Key( - "publicReviews", - ), - Flatten, - ], - ), -] +query { + topProducts { + type + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__array-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__array-3.snap new file mode 100644 index 0000000000..31370c2aef --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__array-3.snap @@ -0,0 +1,17 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: paths +--- +[ + Path( + [ + Key( + "topProducts", + ), + Key( + "publicReviews", + ), + Flatten, + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__array.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__array.snap index 930db08570..d89d19d331 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__array.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__array.snap @@ -2,9 +2,8 @@ source: apollo-router/src/plugins/authorization/scopes.rs expression: doc --- -query { - topProducts { - type - } +{ + "read:user", + "read:username", + "review", } - diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-2.snap index f0192cbe84..930db08570 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-2.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-2.snap @@ -1,23 +1,10 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: paths +expression: doc --- -[ - Path( - [ - Key( - "topProducts", - ), - Key( - "internal", - ), - ], - ), - Path( - [ - Key( - "me", - ), - ], - ), -] +query { + topProducts { + type + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-3.snap index 930db08570..f0192cbe84 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-3.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-3.snap @@ -1,10 +1,23 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: doc +expression: paths --- -query { - topProducts { - type - } -} - +[ + Path( + [ + Key( + "topProducts", + ), + Key( + "internal", + ), + ], + ), + Path( + [ + Key( + "me", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-4.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-4.snap index f0192cbe84..930db08570 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-4.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-4.snap @@ -1,23 +1,10 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: paths +expression: doc --- -[ - Path( - [ - Key( - "topProducts", - ), - Key( - "internal", - ), - ], - ), - Path( - [ - Key( - "me", - ), - ], - ), -] +query { + topProducts { + type + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-5.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-5.snap index c673d28a79..f0192cbe84 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-5.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-5.snap @@ -1,14 +1,23 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: doc +expression: paths --- -query { - topProducts { - type - internal - } - me { - id - } -} - +[ + Path( + [ + Key( + "topProducts", + ), + Key( + "internal", + ), + ], + ), + Path( + [ + Key( + "me", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-6.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-6.snap index e45e102000..c673d28a79 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-6.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-6.snap @@ -1,16 +1,14 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: paths +expression: doc --- -[ - Path( - [ - Key( - "me", - ), - Key( - "name", - ), - ], - ), -] +query { + topProducts { + type + internal + } + me { + id + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-7.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-7.snap index df7b1a5852..e45e102000 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-7.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-7.snap @@ -1,14 +1,16 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: doc +expression: paths --- -query { - topProducts { - type - } - me { - id - name - } -} - +[ + Path( + [ + Key( + "me", + ), + Key( + "name", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-8.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-8.snap index 3b37534097..df7b1a5852 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-8.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-8.snap @@ -1,16 +1,14 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: paths +expression: doc --- -[ - Path( - [ - Key( - "topProducts", - ), - Key( - "internal", - ), - ], - ), -] +query { + topProducts { + type + } + me { + id + name + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-9.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-9.snap new file mode 100644 index 0000000000..3b37534097 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-9.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: paths +--- +[ + Path( + [ + Key( + "topProducts", + ), + Key( + "internal", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query.snap index 930db08570..c2b0de1dbf 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query.snap @@ -2,9 +2,10 @@ source: apollo-router/src/plugins/authorization/scopes.rs expression: doc --- -query { - topProducts { - type - } +{ + "internal", + "profile", + "read:user", + "read:username", + "test", } - diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment.snap index cd12631865..eff331c158 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment.snap @@ -2,12 +2,7 @@ source: apollo-router/src/plugins/authorization/scopes.rs expression: doc --- -query { - topProducts { - type - } - itf { - id - } +{ + "read:user", + "read:username", } - diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment.snap index cd12631865..eff331c158 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment.snap @@ -2,12 +2,7 @@ source: apollo-router/src/plugins/authorization/scopes.rs expression: doc --- -query { - topProducts { - type - } - itf { - id - } +{ + "read:user", + "read:username", } - diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__mutation-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__mutation-2.snap index ea3ede98ba..94cd4087ba 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__mutation-2.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__mutation-2.snap @@ -1,13 +1,5 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: paths +expression: doc --- -[ - Path( - [ - Key( - "ping", - ), - ], - ), -] + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__mutation-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__mutation-3.snap new file mode 100644 index 0000000000..ea3ede98ba --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__mutation-3.snap @@ -0,0 +1,13 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: paths +--- +[ + Path( + [ + Key( + "ping", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__mutation.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__mutation.snap index a241249702..8c0ff84521 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__mutation.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__mutation.snap @@ -2,5 +2,8 @@ source: apollo-router/src/plugins/authorization/scopes.rs expression: doc --- - - +{ + "ping", + "read:user", + "read:username", +} diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field-2.snap index 70241af91d..930db08570 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field-2.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field-2.snap @@ -1,13 +1,10 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: paths +expression: doc --- -[ - Path( - [ - Key( - "me", - ), - ], - ), -] +query { + topProducts { + type + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field-3.snap new file mode 100644 index 0000000000..70241af91d --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field-3.snap @@ -0,0 +1,13 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: paths +--- +[ + Path( + [ + Key( + "me", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field.snap index 930db08570..b2b2038103 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field.snap @@ -2,9 +2,8 @@ source: apollo-router/src/plugins/authorization/scopes.rs expression: doc --- -query { - topProducts { - type - } +{ + "profile", + "read:user", + "read:username", } - diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__scalar-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__scalar-2.snap index 3b37534097..930db08570 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__scalar-2.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__scalar-2.snap @@ -1,16 +1,10 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: paths +expression: doc --- -[ - Path( - [ - Key( - "topProducts", - ), - Key( - "internal", - ), - ], - ), -] +query { + topProducts { + type + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__scalar-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__scalar-3.snap new file mode 100644 index 0000000000..3b37534097 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__scalar-3.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: paths +--- +[ + Path( + [ + Key( + "topProducts", + ), + Key( + "internal", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__scalar.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__scalar.snap index 930db08570..8549daef56 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__scalar.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__scalar.snap @@ -2,9 +2,7 @@ source: apollo-router/src/plugins/authorization/scopes.rs expression: doc --- -query { - topProducts { - type - } +{ + "internal", + "test", } - From 8f9842f8b8e0a64a539ecfd9b60905b897e9bdc1 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Mon, 10 Jul 2023 17:04:10 +0200 Subject: [PATCH 06/82] fix scope extraction from fragments and inline fragments --- .../src/plugins/authorization/mod.rs | 2 +- .../src/plugins/authorization/scopes.rs | 88 +++++++++++++++++-- ...__scopes__tests__interface_fragment-2.snap | 23 +++-- ...__scopes__tests__interface_fragment-3.snap | 27 +++--- ...__scopes__tests__interface_fragment-4.snap | 16 +++- ...__scopes__tests__interface_fragment-5.snap | 13 +++ ...__scopes__tests__interface_fragment-6.snap | 18 ++++ ...__scopes__tests__interface_fragment-7.snap | 5 ++ ...s__tests__interface_inline_fragment-2.snap | 23 +++-- ...s__tests__interface_inline_fragment-3.snap | 16 ++++ ...s__tests__interface_inline_fragment-4.snap | 16 ++++ ...s__tests__interface_inline_fragment-5.snap | 19 ++++ ...s__tests__interface_inline_fragment-6.snap | 17 ++++ ...s__tests__interface_inline_fragment-7.snap | 5 ++ 14 files changed, 240 insertions(+), 48 deletions(-) create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-5.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-6.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-7.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-3.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-4.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-5.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-6.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-7.snap diff --git a/apollo-router/src/plugins/authorization/mod.rs b/apollo-router/src/plugins/authorization/mod.rs index e17d1d2480..4ce687272e 100644 --- a/apollo-router/src/plugins/authorization/mod.rs +++ b/apollo-router/src/plugins/authorization/mod.rs @@ -112,7 +112,7 @@ impl AuthorizationPlugin { ) { let (compiler, file_id) = Query::make_compiler(query, schema, configuration); - let mut visitor = ScopeExtractionVisitor::new(&compiler); + let mut visitor = ScopeExtractionVisitor::new(&compiler, file_id); // if this fails, the query is invalid and will fail at the query planning phase. // We do not return validation errors here for now because that would imply a huge diff --git a/apollo-router/src/plugins/authorization/scopes.rs b/apollo-router/src/plugins/authorization/scopes.rs index e77891e61b..694315d0f2 100644 --- a/apollo-router/src/plugins/authorization/scopes.rs +++ b/apollo-router/src/plugins/authorization/scopes.rs @@ -18,6 +18,7 @@ use crate::spec::query::traverse; pub(crate) struct ScopeExtractionVisitor<'a> { compiler: &'a ApolloCompiler, + file_id: FileId, pub(crate) extracted_scopes: HashSet, } @@ -25,9 +26,10 @@ pub(crate) const REQUIRES_SCOPES_DIRECTIVE_NAME: &str = "requiresScopes"; impl<'a> ScopeExtractionVisitor<'a> { #[allow(dead_code)] - pub(crate) fn new(compiler: &'a ApolloCompiler) -> Self { + pub(crate) fn new(compiler: &'a ApolloCompiler, file_id: FileId) -> Self { Self { compiler, + file_id, extracted_scopes: HashSet::new(), } } @@ -38,11 +40,14 @@ impl<'a> ScopeExtractionVisitor<'a> { ); if let Some(ty) = field.ty().type_def(&self.compiler.db) { - self.extracted_scopes.extend( - scopes_argument(ty.directive_by_name(REQUIRES_SCOPES_DIRECTIVE_NAME)).cloned(), - ); + self.get_scopes_from_type(&ty) } } + + fn get_scopes_from_type(&mut self, ty: &TypeDefinition) { + self.extracted_scopes + .extend(scopes_argument(ty.directive_by_name(REQUIRES_SCOPES_DIRECTIVE_NAME)).cloned()); + } } fn scopes_argument(opt_directive: Option<&hir::Directive>) -> impl Iterator { @@ -79,6 +84,55 @@ impl<'a> traverse::Visitor for ScopeExtractionVisitor<'a> { traverse::field(self, parent_type, node) } + + fn fragment_definition(&mut self, node: &hir::FragmentDefinition) -> Result<(), BoxError> { + if let Some(ty) = self + .compiler + .db + .types_definitions_by_name() + .get(node.type_condition()) + { + self.get_scopes_from_type(ty); + } + traverse::fragment_definition(self, node) + } + + fn fragment_spread(&mut self, node: &hir::FragmentSpread) -> Result<(), BoxError> { + let fragments = self.compiler.db.fragments(self.file_id); + let type_condition = fragments + .get(node.name()) + .ok_or("MissingFragmentDefinition")? + .type_condition(); + + if let Some(ty) = self + .compiler + .db + .types_definitions_by_name() + .get(type_condition) + { + self.get_scopes_from_type(ty); + } + traverse::fragment_spread(self, node) + } + + fn inline_fragment( + &mut self, + parent_type: &str, + + node: &hir::InlineFragment, + ) -> Result<(), BoxError> { + if let Some(type_condition) = node.type_condition() { + if let Some(ty) = self + .compiler + .db + .types_definitions_by_name() + .get(type_condition) + { + self.get_scopes_from_type(ty); + } + } + traverse::inline_fragment(self, parent_type, node) + } } pub(crate) struct ScopeFilteringVisitor<'a> { @@ -221,6 +275,8 @@ impl<'a> transform::Visitor for ScopeFilteringVisitor<'a> { .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 + // starting at the fragment spread, instead of starting at the definition let res = if !fragment_is_authorized { self.query_requires_scopes = true; self.unauthorized_paths.push(self.current_path.clone()); @@ -338,7 +394,7 @@ mod tests { } assert!(diagnostics.is_empty()); - let mut visitor = ScopeExtractionVisitor::new(&compiler); + let mut visitor = ScopeExtractionVisitor::new(&compiler, id); traverse::document(&mut visitor, id).unwrap(); visitor.extracted_scopes.into_iter().collect() @@ -540,6 +596,7 @@ mod tests { itf { id ... on User { + id2: id name } } @@ -553,6 +610,21 @@ mod tests { insta::assert_display_snapshot!(doc); insta::assert_debug_snapshot!(paths); + + let (doc, paths) = filter(QUERY, ["read:user".to_string()].into_iter().collect()); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + + let (doc, paths) = filter( + QUERY, + ["read:user".to_string(), "read:username".to_string()] + .into_iter() + .collect(), + ); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); } #[test] @@ -569,6 +641,7 @@ mod tests { } fragment F on User { + id2: id name } "#; @@ -581,6 +654,11 @@ mod tests { insta::assert_display_snapshot!(doc); insta::assert_debug_snapshot!(paths); + let (doc, paths) = filter(QUERY, ["read:user".to_string()].into_iter().collect()); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + let (doc, paths) = filter( QUERY, ["read:user".to_string(), "read:username".to_string()] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-2.snap index 3461adf398..cd12631865 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-2.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-2.snap @@ -1,16 +1,13 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: paths +expression: doc --- -[ - Path( - [ - Key( - "itf", - ), - Fragment( - "User", - ), - ], - ), -] +query { + topProducts { + type + } + itf { + id + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-3.snap index ade7ec8f25..3461adf398 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-3.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-3.snap @@ -1,17 +1,16 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: doc +expression: paths --- -query { - topProducts { - type - } - itf { - id - ...F - } -} -fragment F on User { - name -} - +[ + Path( + [ + Key( + "itf", + ), + Fragment( + "User", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-4.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-4.snap index f521439d55..66f3830544 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-4.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-4.snap @@ -1,5 +1,17 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: paths +expression: doc --- -[] +query { + topProducts { + type + } + itf { + id + ...F + } +} +fragment F on User { + id2: id +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-5.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-5.snap new file mode 100644 index 0000000000..c72063e600 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-5.snap @@ -0,0 +1,13 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: paths +--- +[ + Path( + [ + Key( + "name", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-6.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-6.snap new file mode 100644 index 0000000000..49a4d3731a --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-6.snap @@ -0,0 +1,18 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: doc +--- +query { + topProducts { + type + } + itf { + id + ...F + } +} +fragment F on User { + id2: id + name +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-7.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-7.snap new file mode 100644 index 0000000000..f521439d55 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-7.snap @@ -0,0 +1,5 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: paths +--- +[] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-2.snap index 3461adf398..cd12631865 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-2.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-2.snap @@ -1,16 +1,13 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: paths +expression: doc --- -[ - Path( - [ - Key( - "itf", - ), - Fragment( - "User", - ), - ], - ), -] +query { + topProducts { + type + } + itf { + id + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-3.snap new file mode 100644 index 0000000000..3461adf398 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-3.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: paths +--- +[ + Path( + [ + Key( + "itf", + ), + Fragment( + "User", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-4.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-4.snap new file mode 100644 index 0000000000..95d7fc9638 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-4.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: doc +--- +query { + topProducts { + type + } + itf { + id + ... on User { + id2: id + } + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-5.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-5.snap new file mode 100644 index 0000000000..962ba94b84 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-5.snap @@ -0,0 +1,19 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: paths +--- +[ + Path( + [ + Key( + "itf", + ), + Fragment( + "User", + ), + Key( + "name", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-6.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-6.snap new file mode 100644 index 0000000000..347b294cd0 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-6.snap @@ -0,0 +1,17 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: doc +--- +query { + topProducts { + type + } + itf { + id + ... on User { + id2: id + name + } + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-7.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-7.snap new file mode 100644 index 0000000000..f521439d55 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-7.snap @@ -0,0 +1,5 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: paths +--- +[] From c1e82f5e6fd8d3d9532005cc3d0672a6d26c84e0 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Mon, 10 Jul 2023 17:55:54 +0200 Subject: [PATCH 07/82] do not forget to set the cache key metadata in warm up --- apollo-router/src/query_planner/caching_query_planner.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apollo-router/src/query_planner/caching_query_planner.rs b/apollo-router/src/query_planner/caching_query_planner.rs index e0621cc88c..72ff071ea0 100644 --- a/apollo-router/src/query_planner/caching_query_planner.rs +++ b/apollo-router/src/query_planner/caching_query_planner.rs @@ -148,6 +148,13 @@ where .lock() .insert(Compiler(Arc::new(Mutex::new(compiler)))); + context + .insert( + QUERY_PLANNER_CACHE_KEY_METADATA, + caching_key.metadata.clone(), + ) + .expect("should not fail"); + let request = QueryPlannerRequest { query, operation_name: operation, From b64b4970b359b9e906fc27fd51fb34ceb2e02bab Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Mon, 10 Jul 2023 18:04:58 +0200 Subject: [PATCH 08/82] update the query planner cache key after supergraph plugins 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 --- .../src/plugins/authorization/mod.rs | 97 ++++++++----------- .../query_planner/caching_query_planner.rs | 11 +++ 2 files changed, 49 insertions(+), 59 deletions(-) diff --git a/apollo-router/src/plugins/authorization/mod.rs b/apollo-router/src/plugins/authorization/mod.rs index 4ce687272e..461d574f19 100644 --- a/apollo-router/src/plugins/authorization/mod.rs +++ b/apollo-router/src/plugins/authorization/mod.rs @@ -64,7 +64,6 @@ pub(crate) struct Conf { pub(crate) struct AuthorizationPlugin { require_authentication: bool, - enable_authorization_directives: bool, } impl AuthorizationPlugin { @@ -124,6 +123,43 @@ impl AuthorizationPlugin { } } + pub(crate) fn update_cache_key(context: &Context) { + let is_authenticated = context.contains_key(APOLLO_AUTHENTICATION_JWT_CLAIMS); + + let request_scopes = context + .get_json_value(APOLLO_AUTHENTICATION_JWT_CLAIMS) + .and_then(|value| { + value.as_object().and_then(|object| { + object.get("scope").and_then(|v| { + v.as_str() + .map(|s| s.split(' ').map(|s| s.to_string()).collect::>()) + }) + }) + }); + let query_scopes = context.get_json_value(REQUIRED_SCOPES_KEY).and_then(|v| { + v.as_array().map(|v| { + v.iter() + .filter_map(|s| s.as_str().map(|s| s.to_string())) + .collect::>() + }) + }); + + let mut scopes = match (request_scopes, query_scopes) { + (None, _) => vec![], + (_, None) => vec![], + (Some(req), Some(query)) => req.intersection(&query).cloned().collect(), + }; + scopes.sort(); + + context + .upsert(QUERY_PLANNER_CACHE_KEY_METADATA, |mut o: Object| { + o.insert("is_authenticated", is_authenticated.into()); + o.insert("scopes", scopes.into()); + o + }) + .unwrap(); + } + pub(crate) fn filter_query( key: &QueryKey, schema: &Schema, @@ -291,14 +327,11 @@ impl Plugin for AuthorizationPlugin { async fn new(init: PluginInit) -> Result { Ok(AuthorizationPlugin { require_authentication: init.config.require_authentication, - enable_authorization_directives: init - .config - .experimental_enable_authorization_directives, }) } fn supergraph_service(&self, service: supergraph::BoxService) -> supergraph::BoxService { - let service = if self.require_authentication { + if self.require_authentication { ServiceBuilder::new() .checkpoint(move |request: supergraph::Request| { if request @@ -329,60 +362,6 @@ impl Plugin for AuthorizationPlugin { .boxed() } else { service - }; - - if self.enable_authorization_directives { - ServiceBuilder::new() - .map_request(move |request: supergraph::Request| { - let is_authenticated = request - .context - .contains_key(APOLLO_AUTHENTICATION_JWT_CLAIMS); - - let request_scopes = request - .context - .get_json_value(APOLLO_AUTHENTICATION_JWT_CLAIMS) - .and_then(|value| { - value.as_object().and_then(|object| { - object.get("scope").and_then(|v| { - v.as_str().map(|s| { - s.split(' ').map(|s| s.to_string()).collect::>() - }) - }) - }) - }); - let query_scopes = request - .context - .get_json_value(REQUIRED_SCOPES_KEY) - .and_then(|v| { - v.as_array().map(|v| { - v.iter() - .filter_map(|s| s.as_str().map(|s| s.to_string())) - .collect::>() - }) - }); - - let mut scopes = match (request_scopes, query_scopes) { - (None, _) => vec![], - (_, None) => vec![], - (Some(req), Some(query)) => req.intersection(&query).cloned().collect(), - }; - scopes.sort(); - - request - .context - .upsert(QUERY_PLANNER_CACHE_KEY_METADATA, |mut o: Object| { - o.insert("is_authenticated", is_authenticated.into()); - o.insert("scopes", scopes.into()); - o - }) - .unwrap(); - - request - }) - .service(service) - .boxed() - } else { - service } } } diff --git a/apollo-router/src/query_planner/caching_query_planner.rs b/apollo-router/src/query_planner/caching_query_planner.rs index 72ff071ea0..1165810e0c 100644 --- a/apollo-router/src/query_planner/caching_query_planner.rs +++ b/apollo-router/src/query_planner/caching_query_planner.rs @@ -21,6 +21,7 @@ use crate::cache::DeduplicatingCache; use crate::error::CacheResolverError; use crate::error::QueryPlannerError; use crate::json_ext::Object; +use crate::plugins::authorization::AuthorizationPlugin; use crate::query_planner::labeler::add_defer_labels; use crate::query_planner::BridgeQueryPlanner; use crate::query_planner::QueryPlanResult; @@ -52,6 +53,7 @@ pub(crate) struct CachingQueryPlanner { delegate: T, schema: Arc, plugins: Arc, + enable_authorization_directives: bool, } impl CachingQueryPlanner @@ -77,11 +79,15 @@ where ) .await, ); + + let enable_authorization_directives = + AuthorizationPlugin::enable_directives(&configuration, &schema).unwrap_or(false); Self { cache, delegate, schema, plugins: Arc::new(plugins), + enable_authorization_directives, } } @@ -213,6 +219,11 @@ where fn call(&mut self, request: query_planner::CachingRequest) -> Self::Future { let mut qp = self.clone(); let schema_id = self.schema.schema_id.clone(); + + if self.enable_authorization_directives { + AuthorizationPlugin::update_cache_key(&request.context); + } + Box::pin(async move { let caching_key = CachingQueryKey { schema_id, From 7930a0665f2885a63b468d2e2288bbe79dfb4adf Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Tue, 11 Jul 2023 18:26:31 +0200 Subject: [PATCH 09/82] lint --- apollo-router/src/query_planner/caching_query_planner.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apollo-router/src/query_planner/caching_query_planner.rs b/apollo-router/src/query_planner/caching_query_planner.rs index 1165810e0c..a3eff4349b 100644 --- a/apollo-router/src/query_planner/caching_query_planner.rs +++ b/apollo-router/src/query_planner/caching_query_planner.rs @@ -81,7 +81,7 @@ where ); let enable_authorization_directives = - AuthorizationPlugin::enable_directives(&configuration, &schema).unwrap_or(false); + AuthorizationPlugin::enable_directives(configuration, &schema).unwrap_or(false); Self { cache, delegate, From 152cfa3a9d25c13b9dcaec4428e12ebcbe04dc1b Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Wed, 12 Jul 2023 16:22:23 +0200 Subject: [PATCH 10/82] comments from reviews --- .../src/plugins/authorization/scopes.rs | 21 +++++++++++-------- .../src/plugins/authorization/tests.rs | 4 ---- .../query_planner/caching_query_planner.rs | 1 - apollo-router/src/query_planner/plan.rs | 2 +- docs/source/configuration/authorization.mdx | 2 ++ 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/apollo-router/src/plugins/authorization/scopes.rs b/apollo-router/src/plugins/authorization/scopes.rs index 694315d0f2..29f0afc317 100644 --- a/apollo-router/src/plugins/authorization/scopes.rs +++ b/apollo-router/src/plugins/authorization/scopes.rs @@ -34,17 +34,17 @@ impl<'a> ScopeExtractionVisitor<'a> { } } - fn get_scopes_from_field(&mut self, field: &FieldDefinition) { + fn scopes_from_field(&mut self, field: &FieldDefinition) { self.extracted_scopes.extend( scopes_argument(field.directive_by_name(REQUIRES_SCOPES_DIRECTIVE_NAME)).cloned(), ); if let Some(ty) = field.ty().type_def(&self.compiler.db) { - self.get_scopes_from_type(&ty) + self.scopes_from_type(&ty) } } - fn get_scopes_from_type(&mut self, ty: &TypeDefinition) { + fn scopes_from_type(&mut self, ty: &TypeDefinition) { self.extracted_scopes .extend(scopes_argument(ty.directive_by_name(REQUIRES_SCOPES_DIRECTIVE_NAME)).cloned()); } @@ -78,7 +78,7 @@ impl<'a> traverse::Visitor for ScopeExtractionVisitor<'a> { .get(parent_type) { if let Some(field) = ty.field(&self.compiler.db, node.name()) { - self.get_scopes_from_field(field); + self.scopes_from_field(field); } } @@ -92,7 +92,7 @@ impl<'a> traverse::Visitor for ScopeExtractionVisitor<'a> { .types_definitions_by_name() .get(node.type_condition()) { - self.get_scopes_from_type(ty); + self.scopes_from_type(ty); } traverse::fragment_definition(self, node) } @@ -110,7 +110,7 @@ impl<'a> traverse::Visitor for ScopeExtractionVisitor<'a> { .types_definitions_by_name() .get(type_condition) { - self.get_scopes_from_type(ty); + self.scopes_from_type(ty); } traverse::fragment_spread(self, node) } @@ -128,7 +128,7 @@ impl<'a> traverse::Visitor for ScopeExtractionVisitor<'a> { .types_definitions_by_name() .get(type_condition) { - self.get_scopes_from_type(ty); + self.scopes_from_type(ty); } } traverse::inline_fragment(self, parent_type, node) @@ -249,6 +249,11 @@ impl<'a> transform::Visitor for ScopeFilteringVisitor<'a> { .get(node.type_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 + // starting at the fragment spread, instead of starting at the definition. + // If we modified the transform visitor implementation to modify the fragment definitions before the + // operations, we would be able to store the list of unauthorized paths per fragment, and at the point + // of application, generate unauthorized paths starting at the operation root if !fragment_is_authorized { Ok(None) } else { @@ -275,8 +280,6 @@ impl<'a> transform::Visitor for ScopeFilteringVisitor<'a> { .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 - // starting at the fragment spread, instead of starting at the definition let res = if !fragment_is_authorized { self.query_requires_scopes = true; self.unauthorized_paths.push(self.current_path.clone()); diff --git a/apollo-router/src/plugins/authorization/tests.rs b/apollo-router/src/plugins/authorization/tests.rs index de6ca69c83..ce3d5bc01a 100644 --- a/apollo-router/src/plugins/authorization/tests.rs +++ b/apollo-router/src/plugins/authorization/tests.rs @@ -434,10 +434,6 @@ async fn scopes_directive() { let req = graphql::Request { query: Some("query { orga(id: 1) { id creatorUser { id name phone } } }".to_string()), - variables: json! {{ "isAuthenticated": false }} - .as_object() - .unwrap() - .clone(), ..Default::default() }; let request = router::Request { diff --git a/apollo-router/src/query_planner/caching_query_planner.rs b/apollo-router/src/query_planner/caching_query_planner.rs index a3eff4349b..c2fe9d7b42 100644 --- a/apollo-router/src/query_planner/caching_query_planner.rs +++ b/apollo-router/src/query_planner/caching_query_planner.rs @@ -106,7 +106,6 @@ where pub(crate) async fn warm_up( &mut self, query_analysis: &QueryAnalysisLayer, - //FIXME: use cache keys that return is_authenticated and scopes cache_keys: Vec, ) { let schema_id = self.schema.schema_id.clone(); diff --git a/apollo-router/src/query_planner/plan.rs b/apollo-router/src/query_planner/plan.rs index 6383acf11c..0496d8b338 100644 --- a/apollo-router/src/query_planner/plan.rs +++ b/apollo-router/src/query_planner/plan.rs @@ -14,7 +14,7 @@ use crate::spec::Query; /// A planner key. /// -/// This type consists of a query string and an optional operation string +/// This type contains everything needed to separate query plan cache entries #[derive(Clone)] pub(crate) struct QueryKey { pub(crate) filtered_query: String, diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index fb866c5a81..8f5529efa8 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -289,6 +289,8 @@ supergraph: introspection: false ``` +Fields can also be hidden using [Contracts](/graphos/delivery/contracts/). + ### Query deduplication When [query deduplication](/configuration/traffic-shaping/#query-deduplication) is activated for subgraphs, the authorization status is accounted for: unauthenticated queries will be grouped together, and authenticated queries will be grouped by scope set. \ No newline at end of file From 413948ade9502aa43be3ea5c3d428324da19e6f9 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Wed, 12 Jul 2023 17:25:11 +0200 Subject: [PATCH 11/82] use private entries for the query plan cache metadata --- .../src/plugins/authorization/mod.rs | 39 +++++++------------ .../plugins/traffic_shaping/deduplication.rs | 17 ++++---- .../src/query_planner/bridge_query_planner.rs | 16 ++++---- .../query_planner/caching_query_planner.rs | 29 +++++++------- apollo-router/src/query_planner/plan.rs | 3 +- 5 files changed, 50 insertions(+), 54 deletions(-) diff --git a/apollo-router/src/plugins/authorization/mod.rs b/apollo-router/src/plugins/authorization/mod.rs index 461d574f19..7e52fa9979 100644 --- a/apollo-router/src/plugins/authorization/mod.rs +++ b/apollo-router/src/plugins/authorization/mod.rs @@ -10,6 +10,7 @@ use http::StatusCode; use router_bridge::planner::UsageReporting; use schemars::JsonSchema; use serde::Deserialize; +use serde::Serialize; use tokio::sync::Mutex; use tower::BoxError; use tower::ServiceBuilder; @@ -25,7 +26,6 @@ use crate::error::QueryPlannerError; use crate::error::SchemaError; use crate::error::ServiceBuildError; use crate::graphql; -use crate::json_ext::Object; use crate::json_ext::Path; use crate::layers::ServiceBuilderExt; use crate::plugin::Plugin; @@ -33,7 +33,6 @@ use crate::plugin::PluginInit; use crate::plugins::authentication::APOLLO_AUTHENTICATION_JWT_CLAIMS; use crate::query_planner::FilteredQuery; use crate::query_planner::QueryKey; -use crate::query_planner::QUERY_PLANNER_CACHE_KEY_METADATA; use crate::register_plugin; use crate::services::supergraph; use crate::spec::query::transform; @@ -50,6 +49,12 @@ pub(crate) mod scopes; const REQUIRED_SCOPES_KEY: &str = "apollo_authorization::scopes::required"; +#[derive(Clone, Debug, Default, Hash, PartialEq, Eq, Serialize)] +pub(crate) struct CacheKeyMetadata { + is_authenticated: bool, + scopes: Vec, +} + /// Authorization plugin #[derive(Clone, Debug, Default, Deserialize, JsonSchema)] #[allow(dead_code)] @@ -151,13 +156,10 @@ impl AuthorizationPlugin { }; scopes.sort(); - context - .upsert(QUERY_PLANNER_CACHE_KEY_METADATA, |mut o: Object| { - o.insert("is_authenticated", is_authenticated.into()); - o.insert("scopes", scopes.into()); - o - }) - .unwrap(); + context.private_entries.lock().insert(CacheKeyMetadata { + is_authenticated, + scopes, + }); } pub(crate) fn filter_query( @@ -171,26 +173,13 @@ impl AuthorizationPlugin { compiler.set_type_system_hir(schema.type_system.clone()); let _id = compiler.add_executable(&key.filtered_query, "query"); - let is_authenticated = key - .metadata - .get("is_authenticated") - .and_then(|v| v.as_bool()) - .unwrap_or_default(); - let scopes = key - .metadata - .get("scopes") - .and_then(|v| v.as_array()) - .map(|v| { - v.iter() - .filter_map(|el| el.as_str().map(|s| s.to_string())) - .collect::>() - }) - .unwrap_or_default(); + let is_authenticated = key.metadata.is_authenticated; + let scopes = &key.metadata.scopes; let filter_res = Self::authenticated_filter_query(&compiler, is_authenticated)?; let filter_res = match filter_res { - None => Self::scopes_filter_query(&compiler, &scopes).map(|opt| { + None => Self::scopes_filter_query(&compiler, scopes).map(|opt| { opt.map(|(query, paths)| (query, paths, Arc::new(Mutex::new(compiler)))) }), Some((query, mut paths)) => { diff --git a/apollo-router/src/plugins/traffic_shaping/deduplication.rs b/apollo-router/src/plugins/traffic_shaping/deduplication.rs index 4a5a38328a..0ade0e0f52 100644 --- a/apollo-router/src/plugins/traffic_shaping/deduplication.rs +++ b/apollo-router/src/plugins/traffic_shaping/deduplication.rs @@ -17,9 +17,8 @@ use tower::ServiceExt; use crate::graphql::Request; use crate::http_ext; -use crate::json_ext::Object; +use crate::plugins::authorization::CacheKeyMetadata; use crate::query_planner::fetch::OperationKind; -use crate::query_planner::QUERY_PLANNER_CACHE_KEY_METADATA; use crate::services::SubgraphRequest; use crate::services::SubgraphResponse; @@ -37,7 +36,7 @@ where } } -type CacheKey = (http_ext::Request, Object); +type CacheKey = (http_ext::Request, CacheKeyMetadata); type WaitMap = Arc>>>>; @@ -80,8 +79,10 @@ where (&request.subgraph_request).into(), request .context - .get::<_, Object>(QUERY_PLANNER_CACHE_KEY_METADATA) - .unwrap_or_default() + .private_entries + .lock() + .get::() + .cloned() .unwrap_or_default(), ); @@ -117,8 +118,10 @@ where (&request.subgraph_request).into(), request .context - .get::<_, Object>(QUERY_PLANNER_CACHE_KEY_METADATA) - .unwrap_or_default() + .private_entries + .lock() + .get::() + .cloned() .unwrap_or_default(), ); let res = { diff --git a/apollo-router/src/query_planner/bridge_query_planner.rs b/apollo-router/src/query_planner/bridge_query_planner.rs index 32df051f7d..92efd3b270 100644 --- a/apollo-router/src/query_planner/bridge_query_planner.rs +++ b/apollo-router/src/query_planner/bridge_query_planner.rs @@ -26,11 +26,10 @@ use crate::error::QueryPlannerError; use crate::error::ServiceBuildError; use crate::graphql; use crate::introspection::Introspection; -use crate::json_ext::Object; use crate::json_ext::Path; use crate::plugins::authorization::AuthorizationPlugin; +use crate::plugins::authorization::CacheKeyMetadata; use crate::query_planner::labeler::add_defer_labels; -use crate::query_planner::QUERY_PLANNER_CACHE_KEY_METADATA; use crate::services::layers::query_analysis::Compiler; use crate::services::QueryPlannerContent; use crate::services::QueryPlannerRequest; @@ -358,6 +357,12 @@ impl Service for BridgeQueryPlanner { context, } = req; + let metadata = context + .private_entries + .lock() + .get::() + .cloned() + .unwrap_or_default(); let this = self.clone(); let fut = async move { let start = Instant::now(); @@ -404,10 +409,7 @@ impl Service for BridgeQueryPlanner { original_query, filtered_query: filtered_query.to_string(), operation_name: operation_name.to_owned(), - metadata: context - .get::<_, Object>(QUERY_PLANNER_CACHE_KEY_METADATA) - .unwrap_or_default() - .unwrap_or_default(), + metadata, }, compiler, ) @@ -998,7 +1000,7 @@ mod tests { original_query: original_query.to_string(), filtered_query: filtered_query.to_string(), operation_name, - metadata: Object::new(), + metadata: CacheKeyMetadata::default(), }, Arc::new(Mutex::new(compiler)), ) diff --git a/apollo-router/src/query_planner/caching_query_planner.rs b/apollo-router/src/query_planner/caching_query_planner.rs index c2fe9d7b42..d613ff6d21 100644 --- a/apollo-router/src/query_planner/caching_query_planner.rs +++ b/apollo-router/src/query_planner/caching_query_planner.rs @@ -20,8 +20,8 @@ use tracing::Instrument; use crate::cache::DeduplicatingCache; use crate::error::CacheResolverError; use crate::error::QueryPlannerError; -use crate::json_ext::Object; use crate::plugins::authorization::AuthorizationPlugin; +use crate::plugins::authorization::CacheKeyMetadata; use crate::query_planner::labeler::add_defer_labels; use crate::query_planner::BridgeQueryPlanner; use crate::query_planner::QueryPlanResult; @@ -38,7 +38,6 @@ use crate::spec::SpecError; use crate::Configuration; use crate::Context; -pub(crate) const QUERY_PLANNER_CACHE_KEY_METADATA: &str = "QUERY_PLANNER_CACHE_KEY_METADATA"; /// An [`IndexMap`] of available plugins. pub(crate) type Plugins = IndexMap>; @@ -153,12 +152,7 @@ where .lock() .insert(Compiler(Arc::new(Mutex::new(compiler)))); - context - .insert( - QUERY_PLANNER_CACHE_KEY_METADATA, - caching_key.metadata.clone(), - ) - .expect("should not fail"); + context.private_entries.lock().insert(caching_key.metadata); let request = QueryPlannerRequest { query, @@ -230,8 +224,10 @@ where operation: request.operation_name.to_owned(), metadata: request .context - .get::<_, Object>(QUERY_PLANNER_CACHE_KEY_METADATA) - .unwrap_or_default() + .private_entries + .lock() + .get::() + .cloned() .unwrap_or_default(), }; @@ -396,7 +392,7 @@ pub(crate) struct CachingQueryKey { pub(crate) schema_id: Option, pub(crate) query: String, pub(crate) operation: Option, - pub(crate) metadata: Object, + pub(crate) metadata: CacheKeyMetadata, } impl std::fmt::Display for CachingQueryKey { @@ -408,8 +404,13 @@ impl std::fmt::Display for CachingQueryKey { let mut hasher = Sha256::new(); hasher.update(self.operation.as_deref().unwrap_or("-")); let operation = hex::encode(hasher.finalize()); - let metadata = - serde_json::to_string(&self.metadata).expect("serialization should not fail"); + + let mut hasher = Sha256::new(); + hasher.update(&serde_json::to_vec(&self.metadata).expect("serialization should not fail")); + let metadata = hex::encode(hasher.finalize()); + + //FIXME for the redis cache test + println!("will write metadata key: {metadata:}"); write!( f, "plan.{}.{}.{}.{}", @@ -425,7 +426,7 @@ impl std::fmt::Display for CachingQueryKey { pub(crate) struct WarmUpCachingQueryKey { pub(crate) query: String, pub(crate) operation: Option, - pub(crate) metadata: Object, + pub(crate) metadata: CacheKeyMetadata, } #[cfg(test)] diff --git a/apollo-router/src/query_planner/plan.rs b/apollo-router/src/query_planner/plan.rs index 0496d8b338..7696b5d8cd 100644 --- a/apollo-router/src/query_planner/plan.rs +++ b/apollo-router/src/query_planner/plan.rs @@ -10,6 +10,7 @@ use super::subscription::SubscriptionNode; use crate::json_ext::Object; use crate::json_ext::Path; use crate::json_ext::Value; +use crate::plugins::authorization::CacheKeyMetadata; use crate::spec::Query; /// A planner key. @@ -20,7 +21,7 @@ pub(crate) struct QueryKey { pub(crate) filtered_query: String, pub(crate) original_query: String, pub(crate) operation_name: Option, - pub(crate) metadata: Object, + pub(crate) metadata: CacheKeyMetadata, } /// A plan for a given GraphQL query From 266d96ae34512d26a258355f9bdd97d4afa9ac1f Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Wed, 12 Jul 2023 17:37:26 +0200 Subject: [PATCH 12/82] lint --- apollo-router/src/plugins/authorization/mod.rs | 2 +- apollo-router/src/query_planner/caching_query_planner.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apollo-router/src/plugins/authorization/mod.rs b/apollo-router/src/plugins/authorization/mod.rs index 7e52fa9979..bca300fa76 100644 --- a/apollo-router/src/plugins/authorization/mod.rs +++ b/apollo-router/src/plugins/authorization/mod.rs @@ -200,7 +200,7 @@ impl AuthorizationPlugin { compiler.set_type_system_hir(schema.type_system.clone()); let _id = compiler.add_executable(&query, "query"); - match Self::scopes_filter_query(&compiler, &scopes)? { + match Self::scopes_filter_query(&compiler, scopes)? { None => Ok(Some((query, paths, Arc::new(Mutex::new(compiler))))), Some((new_query, new_paths)) => { let mut compiler = ApolloCompiler::new(); diff --git a/apollo-router/src/query_planner/caching_query_planner.rs b/apollo-router/src/query_planner/caching_query_planner.rs index d613ff6d21..80ffab7527 100644 --- a/apollo-router/src/query_planner/caching_query_planner.rs +++ b/apollo-router/src/query_planner/caching_query_planner.rs @@ -395,6 +395,7 @@ pub(crate) struct CachingQueryKey { pub(crate) metadata: CacheKeyMetadata, } +#[allow(clippy::print_in_format_impl)] impl std::fmt::Display for CachingQueryKey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut hasher = Sha256::new(); From 13fe51b52504293147ccc34e112be1bcb60cf7c7 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Wed, 12 Jul 2023 17:57:47 +0200 Subject: [PATCH 13/82] fix redis test --- apollo-router/src/query_planner/caching_query_planner.rs | 3 --- apollo-router/tests/redis_test.rs | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/apollo-router/src/query_planner/caching_query_planner.rs b/apollo-router/src/query_planner/caching_query_planner.rs index 80ffab7527..e768d066bf 100644 --- a/apollo-router/src/query_planner/caching_query_planner.rs +++ b/apollo-router/src/query_planner/caching_query_planner.rs @@ -395,7 +395,6 @@ pub(crate) struct CachingQueryKey { pub(crate) metadata: CacheKeyMetadata, } -#[allow(clippy::print_in_format_impl)] impl std::fmt::Display for CachingQueryKey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut hasher = Sha256::new(); @@ -410,8 +409,6 @@ impl std::fmt::Display for CachingQueryKey { hasher.update(&serde_json::to_vec(&self.metadata).expect("serialization should not fail")); let metadata = hex::encode(hasher.finalize()); - //FIXME for the redis cache test - println!("will write metadata key: {metadata:}"); write!( f, "plan.{}.{}.{}.{}", diff --git a/apollo-router/tests/redis_test.rs b/apollo-router/tests/redis_test.rs index f3f72f012f..cf075e37db 100644 --- a/apollo-router/tests/redis_test.rs +++ b/apollo-router/tests/redis_test.rs @@ -22,7 +22,7 @@ mod test { .expect("got redis connection"); connection - .del::<&'static str, ()>("plan.5abb5fecf7df056396fb90fdf38d430b8c1fec55ec132fde878161608af18b76.4c45433039407593557f8a982dafd316a66ec03f0e1ed5fa1b7ef8060d76e8ec.3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112").await.unwrap(); + .del::<&'static str, ()>("plan.5abb5fecf7df056396fb90fdf38d430b8c1fec55ec132fde878161608af18b76.4c45433039407593557f8a982dafd316a66ec03f0e1ed5fa1b7ef8060d76e8ec.3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112.0368f4b0f505ba3991b3131f834d07baad8bf5c4085585d8520db2c51fd4be9f").await.unwrap(); let supergraph = apollo_router::TestHarness::builder() .with_subgraph_network_requests() @@ -52,7 +52,7 @@ mod test { let _ = supergraph.oneshot(request).await?.next_response().await; let s:String = connection - .get("plan.5abb5fecf7df056396fb90fdf38d430b8c1fec55ec132fde878161608af18b76.4c45433039407593557f8a982dafd316a66ec03f0e1ed5fa1b7ef8060d76e8ec.3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112.{}") + .get("plan.5abb5fecf7df056396fb90fdf38d430b8c1fec55ec132fde878161608af18b76.4c45433039407593557f8a982dafd316a66ec03f0e1ed5fa1b7ef8060d76e8ec.3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112.0368f4b0f505ba3991b3131f834d07baad8bf5c4085585d8520db2c51fd4be9f") .await .unwrap(); let query_plan_res: serde_json::Value = serde_json::from_str(&s).unwrap(); From c7363469bb48aad9c9a52a60867542526a44be3f Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 14 Jul 2023 14:01:55 -0600 Subject: [PATCH 14/82] Add sub-header for authentication and authorization --- docs/source/config.json | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/source/config.json b/docs/source/config.json index 00f3fa5b5e..d773d63647 100644 --- a/docs/source/config.json +++ b/docs/source/config.json @@ -36,19 +36,21 @@ "Security": { "CORS": "/configuration/cors", "CSRF prevention": "/configuration/csrf", - "JWT Authentication": [ - "/configuration/authn-jwt", - [ - "enterprise" - ] - ], - "Authorization": [ + "Access control": { + "JWT Authentication": [ + "/configuration/authn-jwt", + [ + "enterprise" + ] + ], + "Authorization directives": [ "/configuration/authorization", [ "enterprise", "experimental" ] - ], + ] + }, "Operation limits": [ "/configuration/operation-limits", [ From d53751441596ea7e5145d51582ce322f6205e0ff Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 14 Jul 2023 14:08:24 -0600 Subject: [PATCH 15/82] Restructure Authorization page --- docs/source/config.json | 2 +- docs/source/configuration/authorization.mdx | 20 +++++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/source/config.json b/docs/source/config.json index d773d63647..2c552fb556 100644 --- a/docs/source/config.json +++ b/docs/source/config.json @@ -43,7 +43,7 @@ "enterprise" ] ], - "Authorization directives": [ + "Authorization": [ "/configuration/authorization", [ "enterprise", diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 8f5529efa8..a107b10d9d 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -6,6 +6,9 @@ title: Authorization in the Apollo Router > > If your organization _doesn't_ currently have an Enterprise plan, you can test out this functionality by signing up for a free [Enterprise trial](/graphos/org/plans/#enterprise-trials). + +## Authorization directives + The Apollo Router supports graph based authorization policies, through the `@authenticated` and `@requiresScopes` directives. They are used to authorize access to specific types or fields. They are defined as follows: @@ -18,7 +21,7 @@ directive @requiresScopes(scopes: [String!]!) on OBJECT | FIELD_DEFINITION | INT To use them, you need either to configure [JWT authentication](./authn-jwt) or to add a [router service coprocessor](../customizations/coprocessor). The JWT plugin will validate the token, extract its claims and set them in the request context at the key `apollo_authentication::JWT::claims`. To participate in the authorization process, a coprocessor would need to set that key in the context. -## `@authenticated` +### `@authenticated` The `@authenticated` directive restricts access to fields and types if the request was not authenticated, by checking for the presence of the `apollo_authentication::JWT::claims` key in the request context. If those fields are restricted, the router will remove them entirely before planning the query, so the unauthenticated parts will still be executed and returned to the client, but the parts requiring authentication will never be requested from subgraphs, avoiding entire subgraph requests in some cases. @@ -100,7 +103,7 @@ So the `me` top level operation would not even be executed. When returning the r If all the fields were removed, then the router would generate a query planner error indicating that the query is unauthorized. -## `@requiresScopes` +### `@requiresScopes` The `@requiresScopes` directive restricts access to fields and types if the request did not present the right set of scopes, by loading from the request the context the object at key `apollo_authentication::JWT::claims`, and in that object the `scope` key, in the format defined by [OAuth2 access token scopes](https://datatracker.ietf.org/doc/html/rfc6749#section-3.3), as a space separated list of scopes. @@ -181,7 +184,6 @@ While if it presented the scopes `employee inventory`, it would filter the query And generate an error at path `/me`. - ## Composition and federation The authorization directives are defined by the subgraph author, and will be carried into the supergraph schema: @@ -280,7 +282,11 @@ query { } ``` -### Introspection +### Query deduplication + +When [query deduplication](/configuration/traffic-shaping/#query-deduplication) is activated for subgraphs, the authorization status is accounted for: unauthenticated queries will be grouped together, and authenticated queries will be grouped by scope set. + +## Introspection Introspection is not affected by authorization, so all types fields will be accessible. The directives applied to them will not be visible though. If introspection might reveal too much information about internal types, then it should be deactivated like this: @@ -289,8 +295,4 @@ supergraph: introspection: false ``` -Fields can also be hidden using [Contracts](/graphos/delivery/contracts/). - -### Query deduplication - -When [query deduplication](/configuration/traffic-shaping/#query-deduplication) is activated for subgraphs, the authorization status is accounted for: unauthenticated queries will be grouped together, and authenticated queries will be grouped by scope set. \ No newline at end of file +Fields can also be hidden using [Contracts](/graphos/delivery/contracts/). \ No newline at end of file From 64e43e8649fdd596d3854c17136497d84263d2b6 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 14 Jul 2023 16:06:56 -0600 Subject: [PATCH 16/82] Copy edit `@authenticated` section --- docs/source/configuration/authorization.mdx | 112 +++++++++++++------- 1 file changed, 76 insertions(+), 36 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index a107b10d9d..0a88535770 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -6,67 +6,103 @@ title: Authorization in the Apollo Router > > If your organization _doesn't_ currently have an Enterprise plan, you can test out this functionality by signing up for a free [Enterprise trial](/graphos/org/plans/#enterprise-trials). +You may need to restrict sensitive information to authorized users or roles or enforce conditional access rules for particular fields. +The Apollo Router allows you to restrict access to specific fields and types through the `@authenticated` and `@requiresScopes` directives: + +- The `@authenticated` directive works in a binary way: requests are either authorized to access a specific field or type or they aren't. +- The `@requiresScopes` directive allows you granular access control through scopes that you define. + +You define and use the directives on a sub-graph level, and the router [composes](#composition) them to the supergraph level. + +## Prequisites + +To use the router's authorization directives, you need to either configure [JWT authentication](./authn-jwt) or add a [router service coprocessor](../customizations/coprocessor). The JWT plugin validates the token, extract its claims, and sets them in the request context at the key `apollo_authentication::JWT::claims`. +To participate in the authorization process, a coprocessor needs to set the key `apollo_authentication::JWT::claims` in requests' context. ## Authorization directives -The Apollo Router supports graph based authorization policies, through the `@authenticated` and `@requiresScopes` directives. They are used to authorize access to specific types or fields. +### `@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. +If the request is authenticated, the router executes the query in its entirety. -They are defined as follows: +For unauthenticated requests, the router removes `@authenticated` fields before planning the query. +The router filters out fields that require authentication and only executes the parts of the query that don't require it. +If every field in a particular subgraph's query is marked as requiring authentication, this can eliminate entire subgraph requests. + +Define the `@authenticated` directive on any subgraph schema you want to use it on. ```graphql directive @authenticated on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM - -directive @requiresScopes(scopes: [String!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM ``` -To use them, you need either to configure [JWT authentication](./authn-jwt) or to add a [router service coprocessor](../customizations/coprocessor). The JWT plugin will validate the token, extract its claims and set them in the request context at the key `apollo_authentication::JWT::claims`. To participate in the authorization process, a coprocessor would need to set that key in the context. +#### Example `@authenticated` use case -### `@authenticated` - -The `@authenticated` directive restricts access to fields and types if the request was not authenticated, by checking for the presence of the `apollo_authentication::JWT::claims` key in the request context. If those fields are restricted, the router will remove them entirely before planning the query, so the unauthenticated parts will still be executed and returned to the client, but the parts requiring authentication will never be requested from subgraphs, avoiding entire subgraph requests in some cases. +Suppose you are building a social media platform and you only want authenticated users to be able to view other users' emails. +You also want to have a query called `me` that returns the authenticated user. -As an example, assuming we have this schema: +Your schema may look something like this: ```graphql type Query { - me: User @authenticated - user(id: ID): User + me: User @authenticated + user(id: ID): User } type User { - id: ID - name: String - email: @authenticated + id: ID + name: String + email: String @authenticated } ``` -And this query: +Consider the following query: ```graphql -{ - me { - name - email - } - - user(id: 1234) { - name - email - } +query { + me { + name + email + } + user(id: "1234") { + name + email + } } ``` -If the request was authenticated, the entire query would be executed, as expected. But if it wasn't, the router would remove fields before execution, and create this filtered query: +An authenticated request would execute the entire query. +For an unauthenticated request, the router would remove the `@authenticated` fields before execution, and create a filtered query. -```graphql -{ - user(id: 1234) { - name - } + + +```graphql title="Query for an authenticated request" +query { + me { + name + email + } + user(id: "1234") { + name + email + } } ``` -So the `me` top level operation would not even be executed. When returning the response, the router will follow the initial request's shape and insert a null in the unauthorized fields, and apply the null propagation rules. It will generate a response of the following shape: +```graphql title="Filtered query for an unauthenticated request" +query { + user(id: "1234") { + name + } +} +``` + + +For an unauthenticated request, the router doesn't attempt to resolve the top-level `me` query nor the email for the user with `id: "1234"`. +The response retains the initial request's shape but returns `null` for unauthorized fields and applies the [standard GraphQL null propagation rules](https://www.apollographql.com/blog/graphql/basics/using-nullability-in-graphql/#what-happens-if-you-try-to-return-null-for-a-non-null-field). + + ```json { @@ -101,10 +137,14 @@ So the `me` top level operation would not even be executed. When returning the r } ``` -If all the fields were removed, then the router would generate a query planner error indicating that the query is unauthorized. +If every requested field requires authentication and a request is unauthenticated, the router generates a query planner error indicating that the query is unauthorized. ### `@requiresScopes` +```graphql +directive @requiresScopes(scopes: [String!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM +``` + The `@requiresScopes` directive restricts access to fields and types if the request did not present the right set of scopes, by loading from the request the context the object at key `apollo_authentication::JWT::claims`, and in that object the `scope` key, in the format defined by [OAuth2 access token scopes](https://datatracker.ietf.org/doc/html/rfc6749#section-3.3), as a space separated list of scopes. Depending on which scope set is presented by the request, different parts of the query may be available. @@ -192,7 +232,7 @@ The authorization directives are defined by the subgraph author, and will be car ### Authorization and `@key` -When the authorization directives are set on fields used in `@key`, they will be usable by the router to join between subgraphs, but cannot be queried directly by the client. This behavior is similar to [Contracts](/graphos/delivery/contracts/) and the [`@inaccessible` directive](https://www.apollographql.com/docs/federation/federated-types/federated-directives/#inaccessible). +When the authorization directives are set on fields used in `@key`, they will be usable by the router to join between subgraphs, but cannot be queried directly by the client. This behavior is similar to [contracts](/graphos/delivery/contracts/) and the [`@inaccessible` directive](https://www.apollographql.com/docs/federation/federated-types/federated-directives/#inaccessible). As an example, assuming we have these subgraphs definition: @@ -282,7 +322,7 @@ query { } ``` -### Query deduplication +## Query deduplication When [query deduplication](/configuration/traffic-shaping/#query-deduplication) is activated for subgraphs, the authorization status is accounted for: unauthenticated queries will be grouped together, and authenticated queries will be grouped by scope set. @@ -295,4 +335,4 @@ supergraph: introspection: false ``` -Fields can also be hidden using [Contracts](/graphos/delivery/contracts/). \ No newline at end of file +Fields can also be hidden using [contracts](/graphos/delivery/contracts/). \ No newline at end of file From a92ddae02b630361077fd5ab11aee61d97f26b7f Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 14 Jul 2023 17:01:17 -0600 Subject: [PATCH 17/82] Copy edit `@requiredScopes` section --- docs/source/configuration/authorization.mdx | 151 ++++++++++++-------- 1 file changed, 88 insertions(+), 63 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 0a88535770..8e1b225a59 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -31,7 +31,7 @@ For unauthenticated requests, the router removes `@authenticated` fields before The router filters out fields that require authentication and only executes the parts of the query that don't require it. If every field in a particular subgraph's query is marked as requiring authentication, this can eliminate entire subgraph requests. -Define the `@authenticated` directive on any subgraph schema you want to use it on. +You can define the `@authenticated` directive on any subgraph schema like this: ```graphql directive @authenticated on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM @@ -97,13 +97,12 @@ query { } } ``` + For an unauthenticated request, the router doesn't attempt to resolve the top-level `me` query nor the email for the user with `id: "1234"`. The response retains the initial request's shape but returns `null` for unauthorized fields and applies the [standard GraphQL null propagation rules](https://www.apollographql.com/blog/graphql/basics/using-nullability-in-graphql/#what-happens-if-you-try-to-return-null-for-a-non-null-field). - - ```json { "data": { @@ -141,88 +140,128 @@ If every requested field requires authentication and a request is unauthenticate ### `@requiresScopes` +The `@requiresScopes` directive marks fields and types as restricted based on the scopes you require. +You can define the `@requiredScopes` directive on any subgraph schema like this: + ```graphql directive @requiresScopes(scopes: [String!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM ``` -The `@requiresScopes` directive restricts access to fields and types if the request did not present the right set of scopes, by loading from the request the context the object at key `apollo_authentication::JWT::claims`, and in that object the `scope` key, in the format defined by [OAuth2 access token scopes](https://datatracker.ietf.org/doc/html/rfc6749#section-3.3), as a space separated list of scopes. +The directive should include a `scopes` argument that expects an array of required scopes. + +The directive validates the required scopes by loading the object at the `apollo_authentication::JWT::claims` key in a request's context. +That object's `scope` key should contain a space separated list of scopes in the format defined by [OAuth2 access token scopes](https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). -Depending on which scope set is presented by the request, different parts of the query may be available. +Depending on the scopes present on the request, the router filters out unauthorized fields and types. +If a field's `scopes` array includes multiple scopes, the request must include _all_ required scopes to resolve it. -Assuming we have this schema: +#### Example `@requiresScopes` use case + +Imagine your social media platform has an ecommerce component. +You need to be able to query for the following: + +- An authenticated user's profile (`me`) +- Other user's emails +- Products and the users who have ordered it + +Your schema may look something like this: ```graphql type Query { - me: User @requiresScopes(scopes: ["profile:read"]) - products: [Products] + me: User @requiresScopes(scopes: ["profile:read"]) + products: [Products] } type User { - id: ID - name: String - email: @requiresScopes(["user:read"]) + id: ID + name: String + email: @requiresScopes(["user:read"]) } type Product { - id: ID - name: String - amount: Int @requiresScopes(["employee", "inventory"]) - orders: [User] @requiresScopes(["employee"]) + id: ID + name: String + amount: Int @requiresScopes(["inventory:read"]) + orders: [User] @requiresScopes(["user:read", "inventory:read"]) } ``` -And this query: +The router executes the following query differently, depending on the request's attached scopes: ```graphql -{ - me { - name - email - } +query { + me { + name + email + } - products { - name - amount - orders { - name - } + products { + name + amount + orders { + name } + } } ``` -If the request presented the scopes `profile:read user:read`, then it would be interpreted as: +If the request includes the `profile:read user:read` scope set, then the router would execute the following filtered query: +```graphql title="Request scopes: 'profile:read user:read'" +query { + me { + name + email + } -```graphql -{ - me { - name - email - } - - products { - name - } + products { + name + /* The following fields are filtered out: + amount # Requires "inventory:read" scope + orders # Requires "user:read" and "inventory:read" scopes + */ + } } ``` -And generate an error at path `/products/@/amount`. +The response would include errors at the `/products/@/amount` and `/products/@/amount` paths. -While if it presented the scopes `employee inventory`, it would filter the query as: +If the request includes the `user:read inventory:read` scope set, then the router would execute the following filtered query: -```graphql -{ - products { - name - amount - orders { - name - } +```graphql title="Request scopes: 'user:read inventory:read'" +query { + /* The following fields are filtered out + me { # Requires the "profile:read" scope + name + email + } + */ + products { + name + amount + orders { + name } + } } ``` -And generate an error at path `/me`. +The response would include an error at the `/me` path. + +## Introspection + +Authorization directives don't affect introspection; all fields that require authorization remain accessible. Directives applied to fields aren't visible. If introspection might reveal too much information about internal types, then be sure to deactivate it: + +```yaml +supergraph: + introspection: false +``` + +You can also hide fields using [contracts](/graphos/delivery/contracts/). + +## Query deduplication + +When [query deduplication](/configuration/traffic-shaping/#query-deduplication) is activated for subgraphs, the authorization status is accounted for: the router groups unauthenticated queries together, and authenticated queries are grouped by scope set. ## Composition and federation @@ -322,17 +361,3 @@ query { } ``` -## Query deduplication - -When [query deduplication](/configuration/traffic-shaping/#query-deduplication) is activated for subgraphs, the authorization status is accounted for: unauthenticated queries will be grouped together, and authenticated queries will be grouped by scope set. - -## Introspection - -Introspection is not affected by authorization, so all types fields will be accessible. The directives applied to them will not be visible though. If introspection might reveal too much information about internal types, then it should be deactivated like this: - -```yaml -supergraph: - introspection: false -``` - -Fields can also be hidden using [contracts](/graphos/delivery/contracts/). \ No newline at end of file From 5e2d6251dbf614cdcdb9c33a2331f84686f068b8 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 14 Jul 2023 17:29:31 -0600 Subject: [PATCH 18/82] Copy edit "Authorization and `@key` types --- docs/source/configuration/authorization.mdx | 92 ++++++++++++++------- 1 file changed, 62 insertions(+), 30 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 8e1b225a59..00de28c909 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -248,73 +248,91 @@ query { The response would include an error at the `/me` path. -## Introspection +## Composition and federation -Authorization directives don't affect introspection; all fields that require authorization remain accessible. Directives applied to fields aren't visible. If introspection might reveal too much information about internal types, then be sure to deactivate it: +Authorization directives are defined at the subgraph level and the router composes them in the supergraph schema. In other words, if subgraph fields or types include either `@authenticated` or `@requiresScopes` directives, they are set on the supergraph too. -```yaml -supergraph: - introspection: false +If multiple shared subgraph fields include `@requiresScopes`, the supergraph schema merges the sets of scopes required by each subgraph. For example, imagine one subgraph requires the `profile:read` scope on the `users` query: + +```graphql title="Subgraph A" +type Query { + users: [User!]! @requiresScopes(scopes: ["profile:read"]) +} ``` -You can also hide fields using [contracts](/graphos/delivery/contracts/). +And another subgraph requires the `user:read` scope on `users` query: -## Query deduplication +```graphql title="Subgraph B" +type Query { + users: [User!]! @requiresScopes(scopes: ["user:read"]) +} +``` -When [query deduplication](/configuration/traffic-shaping/#query-deduplication) is activated for subgraphs, the authorization status is accounted for: the router groups unauthenticated queries together, and authenticated queries are grouped by scope set. +Then the supergraph schema would require both scopes for it. -## Composition and federation +```graphql title="Supergraph" +type Query { + users: [User!]! @requiresScopes(scopes: ["profile:read", "user:read"]) +} +``` -The authorization directives are defined by the subgraph author, and will be carried into the supergraph schema: -- `@authenticated`: if defined on a field or type by any of the subgraphs, it will be set in the supergraph too -- `@requiresScopes`: same as `@authenticated`, and if there are multiple applications, the supergraph schema will merge the sets of scopes required by each subgraph +### Authorization and `@key` types -### Authorization and `@key` +The [`@key` directive](https://www.apollographql.com/docs/federation/entities/) lets you create an entity whose fields resolve across multiple subgraphs. -When the authorization directives are set on fields used in `@key`, they will be usable by the router to join between subgraphs, but cannot be queried directly by the client. This behavior is similar to [contracts](/graphos/delivery/contracts/) and the [`@inaccessible` directive](https://www.apollographql.com/docs/federation/federated-types/federated-directives/#inaccessible). +If you use authorization directives on fields in [`@key` types](https://www.apollographql.com/docs/federation/entities/), Apollo still joins those fields between the subgraphs, but the client cannot query them directly. -As an example, assuming we have these subgraphs definition: +Consider these example subgraph schemas: -```graphql -# subgraph A +```graphql title="Product subgraph" type Query { - t: T + product: Product } -type T @key(fields: "id") { +type Product @key(fields: "id") { id: ID! @authenticated + name: String! + price: Int @authenticated } ``` -```graphql -# subgraph B -type T @key(fields: "id") { +```graphql title="Inventory subgraph" +type Query { + product: Product +} + +type Product @key(fields: "id") { id: ID! @authenticated - value: Int + inStock: Boolean! } ``` -The following query can be done when not authenticated: +An unauthenticated request would successfully execute this query: ```graphql -{ - t { - value +query { + product { + name + inStock } } ``` -But this one would end up with a `null` in `id`, which would trigger nullability rules and nullify `t` too. +For this query: ```graphql -{ - t { +query { + product { id value } } ``` +An unauthenticated request would resolve `null` for `id`. And since `id` is a non-nullable field, `product` would return `null`. + +This behavior is similar to [contracts](/graphos/delivery/contracts/) and the [`@inaccessible` directive](https://www.apollographql.com/docs/federation/federated-types/federated-directives/#inaccessible). + ### Interfaces In the case one of the types implementing an interface requires authorization, then querying the interface will be allowed, but any parts that require access to that type will be filtered. @@ -361,3 +379,17 @@ query { } ``` +## Introspection + +Authorization directives don't affect introspection; all fields that require authorization remain accessible. Directives applied to fields aren't visible. If introspection might reveal too much information about internal types, then be sure to deactivate it: + +```yaml +supergraph: + introspection: false +``` + +You can also hide fields using [contracts](/graphos/delivery/contracts/). + +## Query deduplication + +When [query deduplication](/configuration/traffic-shaping/#query-deduplication) is activated for subgraphs, the authorization status is accounted for: the router groups unauthenticated queries together, and authenticated queries are grouped by scope set. \ No newline at end of file From 2011701041dd50682056dd5e07855959a467f08e Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 14 Jul 2023 17:48:35 -0600 Subject: [PATCH 19/82] Copy edit Interfaces --- docs/source/configuration/authorization.mdx | 47 +++++++++++---------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 00de28c909..277d5227a6 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -279,7 +279,6 @@ type Query { ### Authorization and `@key` types The [`@key` directive](https://www.apollographql.com/docs/federation/entities/) lets you create an entity whose fields resolve across multiple subgraphs. - If you use authorization directives on fields in [`@key` types](https://www.apollographql.com/docs/federation/entities/), Apollo still joins those fields between the subgraphs, but the client cannot query them directly. Consider these example subgraph schemas: @@ -318,70 +317,74 @@ query { } ``` -For this query: +But for this query: ```graphql query { product { id - value + name } } ``` -An unauthenticated request would resolve `null` for `id`. And since `id` is a non-nullable field, `product` would return `null`. +an unauthenticated request would resolve `null` for `id`. And since `id` is a non-nullable field, `product` would return `null`. -This behavior is similar to [contracts](/graphos/delivery/contracts/) and the [`@inaccessible` directive](https://www.apollographql.com/docs/federation/federated-types/federated-directives/#inaccessible). +This behavior is similar to what you can create with [contracts](/graphos/delivery/contracts/) and the [`@inaccessible` directive](https://www.apollographql.com/docs/federation/federated-types/federated-directives/#inaccessible). -### Interfaces +### Authorization and interfaces -In the case one of the types implementing an interface requires authorization, then querying the interface will be allowed, but any parts that require access to that type will be filtered. +If a type [implementing an interface](https://www.apollographql.com/docs/apollo-server/schema/unions-interfaces/#interface-type) requires authorization, unauthorized requests can query the interface, but not any parts of the type that require authorization. -As an example, with this schema, where the interface `I` does not require authentication, but the `User` type does: +For example, consider this schema where the `User` interface doesn't require authentication, but the `Admin` type which implements `User` does: ```graphql type Query { - itf: I! + users: [User!]! } -interface I { - id: ID +interface User { + id: ID + name: String } -type User -implements I +type Admin +implements User @authenticated { id: ID name: String + role: String } ``` -If we send this query with an inline fragment and a type condition on `User`: +If an unauthenticated request were to make this query: ```graphql query { - itf { - id - ... on User { - name - } + users { + id + name + ... on Admin { + role } + } } ``` -The query would be filtered as follows: +The router woudl filter the query as follows: ```graphql query { - itf { + users { id + name } } ``` ## Introspection -Authorization directives don't affect introspection; all fields that require authorization remain accessible. Directives applied to fields aren't visible. If introspection might reveal too much information about internal types, then be sure to deactivate it: +Authorization directives don't affect introspection; all fields that require authorization remain visible. However, directives applied to fields _aren't_ visible. If introspection might reveal too much information about internal types, then be sure to deactivate it: ```yaml supergraph: From 00f589641400e8c2b568a5dfe7144dbb909e5d7d Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 14 Jul 2023 17:59:17 -0600 Subject: [PATCH 20/82] Copy edits --- docs/source/configuration/authorization.mdx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 277d5227a6..b3f3f15e6f 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -6,17 +6,19 @@ title: Authorization in the Apollo Router > > If your organization _doesn't_ currently have an Enterprise plan, you can test out this functionality by signing up for a free [Enterprise trial](/graphos/org/plans/#enterprise-trials). -You may need to restrict sensitive information to authorized users or roles or enforce conditional access rules for particular fields. -The Apollo Router allows you to restrict access to specific fields and types through the `@authenticated` and `@requiresScopes` directives: +You may need to restrict sensitive information to authorized users or enforce conditional access rules for particular fields. +The Apollo Router allows you to control access to specific fields and types through the `@authenticated` and `@requiresScopes` directives: - The `@authenticated` directive works in a binary way: requests are either authorized to access a specific field or type or they aren't. - The `@requiresScopes` directive allows you granular access control through scopes that you define. -You define and use the directives on a sub-graph level, and the router [composes](#composition) them to the supergraph level. +You define and use the directives on a subgraph level, and the router [composes](#composition-and-federation) them to the supergraph. ## Prequisites -To use the router's authorization directives, you need to either configure [JWT authentication](./authn-jwt) or add a [router service coprocessor](../customizations/coprocessor). The JWT plugin validates the token, extract its claims, and sets them in the request context at the key `apollo_authentication::JWT::claims`. +To use the router's authorization directives, you need to either configure [JWT authentication](./authn-jwt) or add a [router service coprocessor](../customizations/coprocessor). + +The JWT plugin validates the token, extract its claims, and sets them in the request context at the key `apollo_authentication::JWT::claims`. To participate in the authorization process, a coprocessor needs to set the key `apollo_authentication::JWT::claims` in requests' context. ## Authorization directives From 1ffdfd3c7d6a7bdbd18dbe38d0510f83f3370f36 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 14 Jul 2023 18:06:34 -0600 Subject: [PATCH 21/82] Typo --- docs/source/configuration/authorization.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index b3f3f15e6f..9033ab92ce 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -373,7 +373,7 @@ query { } ``` -The router woudl filter the query as follows: +The router would filter the query as follows: ```graphql query { From 03cbade434edcb9d8a0acb34302d08990f3e8010 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Mon, 17 Jul 2023 16:42:23 -0700 Subject: [PATCH 22/82] Add pre-req information --- docs/source/configuration/authorization.mdx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 9033ab92ce..027277d478 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -12,14 +12,16 @@ The Apollo Router allows you to control access to specific fields and types thro - The `@authenticated` directive works in a binary way: requests are either authorized to access a specific field or type or they aren't. - The `@requiresScopes` directive allows you granular access control through scopes that you define. -You define and use the directives on a subgraph level, and the router [composes](#composition-and-federation) them to the supergraph. +You define and use the directives on a subgraph level, and the router [composes](#composition-and-federation) them on to the supergraph schema. ## Prequisites To use the router's authorization directives, you need to either configure [JWT authentication](./authn-jwt) or add a [router service coprocessor](../customizations/coprocessor). +Regardless of which you choose, you need to include **claims** in a request's context. +**Claims** are the individual details of a requests' scope. They might include details like the ID of the associated user and/or any scopes assigned to that user. -The JWT plugin validates the token, extract its claims, and sets them in the request context at the key `apollo_authentication::JWT::claims`. -To participate in the authorization process, a coprocessor needs to set the key `apollo_authentication::JWT::claims` in requests' context. +If you configure [JWT authentication](./authn-jwt), the Apollo Router automatically adds a JWT token's claims to the request's context at the `apollo_authentication::JWT::claims` key. +To participate in the authorization process, the coprocessor you add needs to set the key `apollo_authentication::JWT::claims` in request contexts. ## Authorization directives From cc0b141a85380ed37669e0ce712805208fae18a8 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Mon, 17 Jul 2023 16:59:58 -0700 Subject: [PATCH 23/82] Remove nested headers --- docs/source/config.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/source/config.json b/docs/source/config.json index 2c552fb556..f7bb37511d 100644 --- a/docs/source/config.json +++ b/docs/source/config.json @@ -36,8 +36,7 @@ "Security": { "CORS": "/configuration/cors", "CSRF prevention": "/configuration/csrf", - "Access control": { - "JWT Authentication": [ + "JWT Authentication": [ "/configuration/authn-jwt", [ "enterprise" @@ -49,8 +48,7 @@ "enterprise", "experimental" ] - ] - }, + ], "Operation limits": [ "/configuration/operation-limits", [ From a7c5949a85032c6a8a28897eeb67881703fe5390 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Mon, 17 Jul 2023 17:00:59 -0700 Subject: [PATCH 24/82] Remove unnecessary space --- docs/source/config.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/source/config.json b/docs/source/config.json index f7bb37511d..00f3fa5b5e 100644 --- a/docs/source/config.json +++ b/docs/source/config.json @@ -37,11 +37,11 @@ "CORS": "/configuration/cors", "CSRF prevention": "/configuration/csrf", "JWT Authentication": [ - "/configuration/authn-jwt", - [ - "enterprise" - ] - ], + "/configuration/authn-jwt", + [ + "enterprise" + ] + ], "Authorization": [ "/configuration/authorization", [ From c6f355439dd5a4c0ca5989eb2379bec8f738ebbf Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Tue, 18 Jul 2023 12:45:53 -0700 Subject: [PATCH 25/82] Copy edit --- docs/source/configuration/authorization.mdx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 027277d478..e088b4d308 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -6,18 +6,17 @@ title: Authorization in the Apollo Router > > If your organization _doesn't_ currently have an Enterprise plan, you can test out this functionality by signing up for a free [Enterprise trial](/graphos/org/plans/#enterprise-trials). -You may need to restrict sensitive information to authorized users or enforce conditional access rules for particular fields. -The Apollo Router allows you to control access to specific fields and types through the `@authenticated` and `@requiresScopes` directives: +You may need to restrict sensitive information to authenticated users or enforce conditional access rules for particular fields. +The Apollo Router lets you control access to specific fields and types through the `@authenticated` and `@requiresScopes` directives: -- The `@authenticated` directive works in a binary way: requests are either authorized to access a specific field or type or they aren't. -- The `@requiresScopes` directive allows you granular access control through scopes that you define. +- The `@authenticated` directive works in a binary way: authenticated requests can either access a specific field or type or they can't. +- The `@requiresScopes` directive allows you granular access control through scopes you define. -You define and use the directives on a subgraph level, and the router [composes](#composition-and-federation) them on to the supergraph schema. +You define and use these directives on subgraph schemas, and the router [composes](#composition-and-federation) them on to the supergraph schema. ## Prequisites -To use the router's authorization directives, you need to either configure [JWT authentication](./authn-jwt) or add a [router service coprocessor](../customizations/coprocessor). -Regardless of which you choose, you need to include **claims** in a request's context. +To use the router's authorization directives, you need to either configure [JWT authentication](./authn-jwt) or add a [router service coprocessor](../customizations/coprocessor) that includes **claims** in a request's context. **Claims** are the individual details of a requests' scope. They might include details like the ID of the associated user and/or any scopes assigned to that user. If you configure [JWT authentication](./authn-jwt), the Apollo Router automatically adds a JWT token's claims to the request's context at the `apollo_authentication::JWT::claims` key. From 21f5a21ada3ef7f12c86571794299d4f993a664c Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Thu, 20 Jul 2023 11:21:10 -0600 Subject: [PATCH 26/82] Apply suggestions from code review Co-authored-by: Lucas Leadbetter <5595530+lleadbet@users.noreply.github.com> --- docs/source/configuration/authorization.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 027277d478..fe17c07ecb 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -154,7 +154,7 @@ directive @requiresScopes(scopes: [String!]!) on OBJECT | FIELD_DEFINITION | INT The directive should include a `scopes` argument that expects an array of required scopes. The directive validates the required scopes by loading the object at the `apollo_authentication::JWT::claims` key in a request's context. -That object's `scope` key should contain a space separated list of scopes in the format defined by [OAuth2 access token scopes](https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). +That object's `scope` key should contain a space separated list of scopes in the format defined by the [OAuth2 RFC for access token scopes](https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). Depending on the scopes present on the request, the router filters out unauthorized fields and types. If a field's `scopes` array includes multiple scopes, the request must include _all_ required scopes to resolve it. @@ -280,10 +280,10 @@ type Query { } ``` -### Authorization and `@key` types +### Authorization and `@key` fields The [`@key` directive](https://www.apollographql.com/docs/federation/entities/) lets you create an entity whose fields resolve across multiple subgraphs. -If you use authorization directives on fields in [`@key` types](https://www.apollographql.com/docs/federation/entities/), Apollo still joins those fields between the subgraphs, but the client cannot query them directly. +If you use authorization directives on fields in [`@key` directives](https://www.apollographql.com/docs/federation/entities/), Apollo still joins those fields between the subgraphs, but the client cannot query them directly. Consider these example subgraph schemas: From e006b24cdeb767339f0e66d44cebf0ab8769c463 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Thu, 20 Jul 2023 11:38:01 -0600 Subject: [PATCH 27/82] Clarify that `requiredScopes` can also eliminate entire subgraph requests --- docs/source/configuration/authorization.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index d6c55d26c8..45ab06153a 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -157,6 +157,7 @@ That object's `scope` key should contain a space separated list of scopes in the Depending on the scopes present on the request, the router filters out unauthorized fields and types. If a field's `scopes` array includes multiple scopes, the request must include _all_ required scopes to resolve it. +If every field on a particular subgraph query requires scopes that aren't present, this can eliminate entire subgraph requests. #### Example `@requiresScopes` use case From 2dc22d2661aa2368f098419e9da6da93fdd603a0 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Thu, 20 Jul 2023 14:17:32 -0600 Subject: [PATCH 28/82] Add authorization directives to list of router enterprise features --- docs/source/enterprise-features.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/enterprise-features.mdx b/docs/source/enterprise-features.mdx index aa2b2664a8..3b20406095 100644 --- a/docs/source/enterprise-features.mdx +++ b/docs/source/enterprise-features.mdx @@ -11,6 +11,7 @@ The Apollo Router provides expanded performance, security, and customization fea - **Real-time updates** via [GraphQL subscriptions](./executing-operations/subscription-support/) - **Authentication of inbound requests** via [JSON Web Token (JWT)](./configuration/authn-jwt/) +- **Access control** to specific fields and types through the [`@authenticated`](./configuration/authorization#authenticated) and [`@requiresScopes`](./configuration/authorization#requiresscopes) 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) From 2bd587f3bb52da77103e18f56df7ba9fed44dd53 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Fri, 28 Jul 2023 16:12:30 +0200 Subject: [PATCH 29/82] update router-bridge --- Cargo.lock | 4 ++-- apollo-router/Cargo.toml | 2 +- apollo-router/src/services/supergraph_service.rs | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b73aef9f3d..e6f5e389a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4927,9 +4927,9 @@ dependencies = [ [[package]] name = "router-bridge" -version = "0.4.0+v2.4.10" +version = "0.5.1+v2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ca7a000e3c4e1f6539581443354403f50d9a85b22c9a9a5572be0cf581c25df" +checksum = "6b16165d85954933e84512b7c34805d2b876c8ea4e9f206fe0812ad201eefb05" dependencies = [ "anyhow", "async-channel", diff --git a/apollo-router/Cargo.toml b/apollo-router/Cargo.toml index 6e6c85acb6..f4adf0ae9c 100644 --- a/apollo-router/Cargo.toml +++ b/apollo-router/Cargo.toml @@ -171,7 +171,7 @@ reqwest = { version = "0.11.18", default-features = false, features = [ "stream", ] } # note: this dependency should _always_ be pinned, prefix the version with an `=` -router-bridge = "=0.4.0+v2.4.10" +router-bridge = "=0.5.1+v2.5.1" rust-embed="6.8.1" rustls = "0.20.8" rustls-pemfile = "1.0.3" diff --git a/apollo-router/src/services/supergraph_service.rs b/apollo-router/src/services/supergraph_service.rs index 61daf10f7d..bb963da4eb 100644 --- a/apollo-router/src/services/supergraph_service.rs +++ b/apollo-router/src/services/supergraph_service.rs @@ -943,7 +943,7 @@ mod tests { ) .with_json( serde_json::json!{{ - "query":"{computer(id:\"Computer1\"){errorField id}}", + "query":"{computer(id:\"Computer1\"){id errorField}}", }}, serde_json::json!{{ "data": { @@ -2667,6 +2667,7 @@ mod tests { } type Query + @join__type(graph: S1) { foo: Foo! @join__field(graph: S1) } From 765c7f82cdc4154ac37462f15b4bb736f4d5ba44 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 28 Jul 2023 17:09:47 -0600 Subject: [PATCH 30/82] Update docs/source/configuration/authorization.mdx Co-authored-by: Simon Sapin --- docs/source/configuration/authorization.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 45ab06153a..5264fcb80a 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -16,7 +16,7 @@ You define and use these directives on subgraph schemas, and the router [compose ## Prequisites -To use the router's authorization directives, you need to either configure [JWT authentication](./authn-jwt) or add a [router service coprocessor](../customizations/coprocessor) that includes **claims** in a request's context. +To use the router's authorization directives, you need to either configure [JWT authentication](./authn-jwt) or add a [router service coprocessor](../customizations/coprocessor) that adds **claims** to a request's context. **Claims** are the individual details of a requests' scope. They might include details like the ID of the associated user and/or any scopes assigned to that user. If you configure [JWT authentication](./authn-jwt), the Apollo Router automatically adds a JWT token's claims to the request's context at the `apollo_authentication::JWT::claims` key. From 1ff841fb2f68eff450e98d2e56a6f019b78d7a30 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 4 Aug 2023 16:55:14 -0600 Subject: [PATCH 31/82] Apply suggestions from code review Co-authored-by: Chandrika Srinivasan --- docs/source/configuration/authorization.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 5264fcb80a..3be429d480 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -17,7 +17,7 @@ You define and use these directives on subgraph schemas, and the router [compose ## Prequisites To use the router's authorization directives, you need to either configure [JWT authentication](./authn-jwt) or add a [router service coprocessor](../customizations/coprocessor) that adds **claims** to a request's context. -**Claims** are the individual details of a requests' scope. They might include details like the ID of the associated user and/or any scopes assigned to that user. +**Claims** are the individual details of a request's scope. They might include details like the ID of the associated user and/or any scopes assigned to that user. If you configure [JWT authentication](./authn-jwt), the Apollo Router automatically adds a JWT token's claims to the request's context at the `apollo_authentication::JWT::claims` key. To participate in the authorization process, the coprocessor you add needs to set the key `apollo_authentication::JWT::claims` in request contexts. @@ -144,7 +144,7 @@ If every requested field requires authentication and a request is unauthenticate ### `@requiresScopes` The `@requiresScopes` directive marks fields and types as restricted based on the scopes you require. -You can define the `@requiredScopes` directive on any subgraph schema like this: +You can define the `@requiresScopes` directive on any subgraph schema like this: ```graphql directive @requiresScopes(scopes: [String!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM From ce51ab07e992d87947a18545b1becfe623ae7903 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 21 Jul 2023 12:50:20 -0600 Subject: [PATCH 32/82] Copy edits --- docs/source/configuration/authorization.mdx | 55 +++++++++++++-------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 3be429d480..63682fb57a 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -80,7 +80,7 @@ For an unauthenticated request, the router would remove the `@authenticated` fie -```graphql title="Query for an authenticated request" +```graphql title="Query executed for an authenticated request" query { me { name @@ -93,7 +93,7 @@ query { } ``` -```graphql title="Filtered query for an unauthenticated request" +```graphql title="Query executed for an unauthenticated request" query { user(id: "1234") { name @@ -103,7 +103,7 @@ query { -For an unauthenticated request, the router doesn't attempt to resolve the top-level `me` query nor the email for the user with `id: "1234"`. +For an unauthenticated request, the router doesn't attempt to resolve the top-level `me` query, nor the email for the user with `id: "1234"`. The response retains the initial request's shape but returns `null` for unauthorized fields and applies the [standard GraphQL null propagation rules](https://www.apollographql.com/blog/graphql/basics/using-nullability-in-graphql/#what-happens-if-you-try-to-return-null-for-a-non-null-field). ```json @@ -139,7 +139,7 @@ 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 a query planner error indicating that the query is unauthorized. +If _every_ requested field requires authentication and a request is unauthenticated, the router generates a query planner error indicating that the query is unauthorized. ### `@requiresScopes` @@ -190,9 +190,13 @@ type Product { } ``` -The router executes the following query differently, depending on the request's attached scopes: +The router executes the following query differently, depending on the request's attached scopes. -```graphql +If the request includes the `profile:read user:read` scope set, then the router would execute the following filtered query: + + + +```graphql title="Raw query to router" query { me { name @@ -209,9 +213,7 @@ query { } ``` -If the request includes the `profile:read user:read` scope set, then the router would execute the following filtered query: - -```graphql title="Request scopes: 'profile:read user:read'" +```graphql title="Scopes: 'profile:read user:read'" query { me { name @@ -220,26 +222,25 @@ query { products { name - /* The following fields are filtered out: - amount # Requires "inventory:read" scope - orders # Requires "user:read" and "inventory:read" scopes - */ } } ``` -The response would include errors at the `/products/@/amount` and `/products/@/amount` paths. + + +The response would include errors at the `/products/@/amount` and `/products/@/orders` paths. If the request includes the `user:read inventory:read` scope set, then the router would execute the following filtered query: -```graphql title="Request scopes: 'user:read inventory:read'" + + +```graphql title="Raw query to router" query { - /* The following fields are filtered out - me { # Requires the "profile:read" scope + me { name email } - */ + products { name amount @@ -250,6 +251,20 @@ query { } ``` +```graphql title="Scopes: 'user:read inventory:read'" +query { + products { + name + amount + orders { + name + } + } +} +``` + + + The response would include an error at the `/me` path. ## Composition and federation @@ -321,7 +336,7 @@ query { } ``` -But for this query: +But for the following query, an unauthenticated request would resolve `null` for `id`. And since `id` is a non-nullable field, `product` would return `null`. ```graphql query { @@ -332,8 +347,6 @@ query { } ``` -an unauthenticated request would resolve `null` for `id`. And since `id` is a non-nullable field, `product` would return `null`. - This behavior is similar to what you can create with [contracts](/graphos/delivery/contracts/) and the [`@inaccessible` directive](https://www.apollographql.com/docs/federation/federated-types/federated-directives/#inaccessible). ### Authorization and interfaces From d74bd61148114ee231e249a8c8d1cca8eb137e6b Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 4 Aug 2023 17:03:33 -0600 Subject: [PATCH 33/82] Copy edits and add to-do sections --- docs/source/configuration/authorization.mdx | 22 +++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 63682fb57a..538832d193 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -6,13 +6,13 @@ title: Authorization in the Apollo Router > > If your organization _doesn't_ currently have an Enterprise plan, you can test out this functionality by signing up for a free [Enterprise trial](/graphos/org/plans/#enterprise-trials). -You may need to restrict sensitive information to authenticated users or enforce conditional access rules for particular fields. -The Apollo Router lets you control access to specific fields and types through the `@authenticated` and `@requiresScopes` directives: +Whether your graph is small or large, you may need to restrict sensitive information to authenticated users or enforce conditional access rules. It's more efficient to do this on the supergraph level, rather than in your underlying services. +The Apollo Router lets you **control access to specific fields and types across your supergraph** through the `@authenticated` and `@requiresScopes` directives: - The `@authenticated` directive works in a binary way: authenticated requests can either access a specific field or type or they can't. - The `@requiresScopes` directive allows you granular access control through scopes you define. -You define and use these directives on subgraph schemas, and the router [composes](#composition-and-federation) them on to the supergraph schema. +You define and use these directives on subgraph schemas, and GraphOS [composes](#composition-and-federation) them on to the supergraph schema. ## Prequisites @@ -22,6 +22,14 @@ To use the router's authorization directives, you need to either configure [JWT If you configure [JWT authentication](./authn-jwt), the Apollo Router automatically adds a JWT token's claims to the request's context at the `apollo_authentication::JWT::claims` key. To participate in the authorization process, the coprocessor you add needs to set the key `apollo_authentication::JWT::claims` in request contexts. +### JWT authentication configuration + +To-do: Recipe for JWT auth with these directives + +### Coprocessors for authorization + +To-do: Coprocessors specifications + ## Authorization directives ### `@authenticated` @@ -269,7 +277,7 @@ The response would include an error at the `/me` path. ## Composition and federation -Authorization directives are defined at the subgraph level and the router composes them in the supergraph schema. In other words, if subgraph fields or types include either `@authenticated` or `@requiresScopes` directives, they are set on the supergraph too. +Authorization directives are defined at the subgraph level and the GraphOS composes them in the supergraph schema. In other words, if subgraph fields or types include either `@authenticated` or `@requiresScopes` directives, they are set on the supergraph too. If multiple shared subgraph fields include `@requiresScopes`, the supergraph schema merges the sets of scopes required by each subgraph. For example, imagine one subgraph requires the `profile:read` scope on the `users` query: @@ -287,7 +295,7 @@ type Query { } ``` -Then the supergraph schema would require both scopes for it. +Then the supergraph schema would require _both_ scopes for it. ```graphql title="Supergraph" type Query { @@ -336,7 +344,9 @@ query { } ``` -But for the following query, an unauthenticated request would resolve `null` for `id`. And since `id` is a non-nullable field, `product` would return `null`. +Specifically, under the hood, the router would use the `id` field to resolve the `Product` entity, but it wouldn't return it. + +For the following query, an unauthenticated request would resolve `null` for `id`. And since `id` is a non-nullable field, `product` would return `null`. ```graphql query { From 813e7a8171b323d1e24f50b0c646f006988427c7 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Sun, 6 Aug 2023 15:52:33 -0600 Subject: [PATCH 34/82] Copy edits --- docs/source/configuration/authorization.mdx | 66 +++++++++++++++------ 1 file changed, 48 insertions(+), 18 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 538832d193..d1db18caac 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -1,5 +1,7 @@ --- title: Authorization in the Apollo Router +description: Strengthen your supergraph's security with advanced access controls +minVersion: 1.27.0 --- > ⚠️ **This is an [Enterprise feature](../enterprise-features/) of the Apollo Router.** It requires an organization with a [GraphOS Enterprise plan](https://www.apollographql.com/pricing/). @@ -9,10 +11,11 @@ title: Authorization in the Apollo Router Whether your graph is small or large, you may need to restrict sensitive information to authenticated users or enforce conditional access rules. It's more efficient to do this on the supergraph level, rather than in your underlying services. The Apollo Router lets you **control access to specific fields and types across your supergraph** through the `@authenticated` and `@requiresScopes` directives: -- The `@authenticated` directive works in a binary way: authenticated requests can either access a specific field or type or they can't. -- The `@requiresScopes` directive allows you granular access control through scopes you define. +- The `@authenticated` directive works in a binary way: authenticated requests can access the specified field or type and unauthenticated requests can't. +- The `@requiresScopes` directive allows granular access control through scopes you define. You define and use these directives on subgraph schemas, and GraphOS [composes](#composition-and-federation) them on to the supergraph schema. +The router then enforces these directives on all incoming requests. ## Prequisites @@ -42,7 +45,19 @@ For unauthenticated requests, the router removes `@authenticated` fields before The router filters out fields that require authentication and only executes the parts of the query that don't require it. If every field in a particular subgraph's query is marked as requiring authentication, this can eliminate entire subgraph requests. -You can define the `@authenticated` directive on any subgraph schema like this: +#### Prequisites + +To use the `@authenticated` directive in a subgraph you can either: +- [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.5", + import: [..., "@authenticated"]) +``` + +- or define the directive like this: ```graphql directive @authenticated on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM @@ -70,7 +85,7 @@ type User { Consider the following query: -```graphql +```graphql title="Sample query" query { me { name @@ -84,7 +99,7 @@ query { ``` An authenticated request would execute the entire query. -For an unauthenticated request, the router would remove the `@authenticated` fields before execution, and create a filtered query. +For an unauthenticated request, the router would remove the `@authenticated` fields and execute the filtered query. @@ -114,7 +129,7 @@ query { For an unauthenticated request, the router doesn't attempt to resolve the top-level `me` query, nor the email for the user with `id: "1234"`. The response retains the initial request's shape but returns `null` for unauthorized fields and applies the [standard GraphQL null propagation rules](https://www.apollographql.com/blog/graphql/basics/using-nullability-in-graphql/#what-happens-if-you-try-to-return-null-for-a-non-null-field). -```json +```json title="Unauthenticated request's response" { "data": { "me": null, @@ -152,12 +167,6 @@ If _every_ requested field requires authentication and a request is unauthentica ### `@requiresScopes` The `@requiresScopes` directive marks fields and types as restricted based on the scopes you require. -You can define the `@requiresScopes` directive on any subgraph schema like this: - -```graphql -directive @requiresScopes(scopes: [String!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM -``` - The directive should include a `scopes` argument that expects an array of required scopes. The directive validates the required scopes by loading the object at the `apollo_authentication::JWT::claims` key in a request's context. @@ -167,34 +176,53 @@ Depending on the scopes present on the request, the router filters out unauthori If a field's `scopes` array includes multiple scopes, the request must include _all_ required scopes to resolve it. If every field on a particular subgraph query requires scopes that aren't present, this can eliminate entire subgraph requests. +#### Prequisites + +To use the `@requiresScopes` directive in a subgraph you can either: +- [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.5", + import: [..., "@requiresScopes"]) +``` + +- or define the directive like this: + +```graphql +directive @requiresScopes(scopes: [String!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM +``` + #### Example `@requiresScopes` use case Imagine your social media platform has an ecommerce component. You need to be able to query for the following: - An authenticated user's profile (`me`) -- Other user's emails -- Products and the users who have ordered it +- Other users' emails +- Products and the users who have ordered them Your schema may look something like this: -```graphql +```graphql title="" type Query { me: User @requiresScopes(scopes: ["profile:read"]) - products: [Products] + products: [Product!] } type User { id: ID name: String email: @requiresScopes(["user:read"]) + products: [Product!] @requiresScopes(["user:read", "inventory:read"]) } type Product { id: ID name: String amount: Int @requiresScopes(["inventory:read"]) - orders: [User] @requiresScopes(["user:read", "inventory:read"]) + orders: [User!] @requiresScopes(["user:read", "inventory:read"]) } ``` @@ -363,7 +391,7 @@ This behavior is similar to what you can create with [contracts](/graphos/delive If a type [implementing an interface](https://www.apollographql.com/docs/apollo-server/schema/unions-interfaces/#interface-type) requires authorization, unauthorized requests can query the interface, but not any parts of the type that require authorization. -For example, consider this schema where the `User` interface doesn't require authentication, but the `Admin` type which implements `User` does: +For example, consider this schema where the `User` interface doesn't require authentication, but the `Admin` type which implements `User` does: ```graphql type Query { @@ -409,6 +437,8 @@ query { } ``` +The response would include an `"UNAUTHORIZED_FIELD_OR_TYPE"` error at the `/users/@/role` path. + ## Introspection Authorization directives don't affect introspection; all fields that require authorization remain visible. However, directives applied to fields _aren't_ visible. If introspection might reveal too much information about internal types, then be sure to deactivate it: From 497f06bee11319b04434124956224fc2e2cd2a29 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Sun, 6 Aug 2023 16:32:39 -0600 Subject: [PATCH 35/82] Add to-dos --- docs/source/configuration/authorization.mdx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index d1db18caac..f4e9a786dc 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -63,6 +63,8 @@ extend schema directive @authenticated on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM ``` +To-do: Enable them in some other way? Warn that turning them on can break things since previously unauthenticated requests will start returning errors. + #### Example `@authenticated` use case Suppose you are building a social media platform and you only want authenticated users to be able to view other users' emails. @@ -194,6 +196,8 @@ extend schema directive @requiresScopes(scopes: [String!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM ``` +To-do: Enable them in some other way? Warn that turning them on can break things since previously unauthenticated requests will start returning errors. + #### Example `@requiresScopes` use case Imagine your social media platform has an ecommerce component. From 479961b04d29935ef92320bb0c1a3e3386476bdf Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Mon, 7 Aug 2023 07:27:37 -0600 Subject: [PATCH 36/82] Update error message for completely filtered query --- docs/source/configuration/authorization.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index f4e9a786dc..8cb2a2f8b1 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -164,7 +164,7 @@ 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 a query planner error indicating that the query is unauthorized. +If _every_ requested field requires authentication and a request is unauthenticated, the router generates an error indicating that the query is unauthorized. ### `@requiresScopes` @@ -196,7 +196,7 @@ extend schema directive @requiresScopes(scopes: [String!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM ``` -To-do: Enable them in some other way? Warn that turning them on can break things since previously unauthenticated requests will start returning errors. + #### Example `@requiresScopes` use case From 94b92820ff83e226a5fd796ad6f5dc87d5496922 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Mon, 7 Aug 2023 07:28:21 -0600 Subject: [PATCH 37/82] Typo --- docs/source/configuration/authorization.mdx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 8cb2a2f8b1..c5e96ee110 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -45,7 +45,7 @@ For unauthenticated requests, the router removes `@authenticated` fields before The router filters out fields that require authentication and only executes the parts of the query that don't require it. If every field in a particular subgraph's query is marked as requiring authentication, this can eliminate entire subgraph requests. -#### Prequisites +#### Prerequisites To use the `@authenticated` directive in a subgraph you can either: - [import it from the `@link` directive](/federation/federated-types/federated-directives/#importing-directives) like so: @@ -178,7 +178,7 @@ Depending on the scopes present on the request, the router filters out unauthori If a field's `scopes` array includes multiple scopes, the request must include _all_ required scopes to resolve it. If every field on a particular subgraph query requires scopes that aren't present, this can eliminate entire subgraph requests. -#### Prequisites +#### Prerequisites To use the `@requiresScopes` directive in a subgraph you can either: - [import it from the `@link` directive](/federation/federated-types/federated-directives/#importing-directives) like so: @@ -196,8 +196,6 @@ extend schema directive @requiresScopes(scopes: [String!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM ``` - - #### Example `@requiresScopes` use case Imagine your social media platform has an ecommerce component. From a5ed4ab389a07d6ad3102665e7d341d881fe33bd Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Mon, 7 Aug 2023 07:56:46 -0600 Subject: [PATCH 38/82] Typo --- docs/source/configuration/authorization.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index c5e96ee110..295e82c9fb 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -17,7 +17,7 @@ The Apollo Router lets you **control access to specific fields and types across You define and use these directives on subgraph schemas, and GraphOS [composes](#composition-and-federation) them on to the supergraph schema. The router then enforces these directives on all incoming requests. -## Prequisites +## Prerequisites To use the router's authorization directives, you need to either configure [JWT authentication](./authn-jwt) or add a [router service coprocessor](../customizations/coprocessor) that adds **claims** to a request's context. **Claims** are the individual details of a request's scope. They might include details like the ID of the associated user and/or any scopes assigned to that user. From 5e2f3e13c18c7cce67afd79d0b8f0ea59a22f221 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Tue, 8 Aug 2023 11:52:11 -0600 Subject: [PATCH 39/82] Rewrite intro --- docs/source/configuration/authorization.mdx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 295e82c9fb..bcc887b8c1 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -8,7 +8,10 @@ minVersion: 1.27.0 > > If your organization _doesn't_ currently have an Enterprise plan, you can test out this functionality by signing up for a free [Enterprise trial](/graphos/org/plans/#enterprise-trials). -Whether your graph is small or large, you may need to restrict sensitive information to authenticated users or enforce conditional access rules. It's more efficient to do this on the supergraph level, rather than in your underlying services. +APIs provide access to business-critical data. Unrestricted access can result in data breaches, monetary losses, or potential denial of service. Even for internal APIs, checks are essential to limit data to authorized parties. + +Enforcing authorization before processing requests is more efficient and secure because it allows for early request termination and creates an initial checkpoint that can be reinforced in other service layers. + The Apollo Router lets you **control access to specific fields and types across your supergraph** through the `@authenticated` and `@requiresScopes` directives: - The `@authenticated` directive works in a binary way: authenticated requests can access the specified field or type and unauthenticated requests can't. From d5ac25332503db25f67463471a86661df9a5acf4 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Tue, 8 Aug 2023 15:01:12 -0600 Subject: [PATCH 40/82] Align code examples to demo --- docs/source/configuration/authorization.mdx | 270 +++++++++----------- 1 file changed, 126 insertions(+), 144 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index bcc887b8c1..66e6606875 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -8,11 +8,11 @@ minVersion: 1.27.0 > > If your organization _doesn't_ currently have an Enterprise plan, you can test out this functionality by signing up for a free [Enterprise trial](/graphos/org/plans/#enterprise-trials). -APIs provide access to business-critical data. Unrestricted access can result in data breaches, monetary losses, or potential denial of service. Even for internal APIs, checks are essential to limit data to authorized parties. +APIs provide access to business-critical data. Unrestricted access can result in data breaches, monetary losses, or potential denial of service. Even for internal APIs, checks can be essential to limit data to authorized parties. -Enforcing authorization before processing requests is more efficient and secure because it allows for early request termination and creates an initial checkpoint that can be reinforced in other service layers. +Enforcing authorization before processing requests is more efficient because it allows for early request termination. It also enhances security by creating an initial checkpoint that can be reinforced in other service layers. -The Apollo Router lets you **control access to specific fields and types across your supergraph** through the `@authenticated` and `@requiresScopes` directives: +The Apollo Router provides fine-grained access control at your graph's edge. Using the `@authenticated` and `@requiresScopes` directives, you can define access to specific fields and types across your supergraph: - The `@authenticated` directive works in a binary way: authenticated requests can access the specified field or type and unauthenticated requests can't. - The `@requiresScopes` directive allows granular access control through scopes you define. @@ -25,15 +25,16 @@ The router then enforces these directives on all incoming requests. To use the router's authorization directives, you need to either configure [JWT authentication](./authn-jwt) or add a [router service coprocessor](../customizations/coprocessor) that adds **claims** to a request's context. **Claims** are the individual details of a request's scope. They might include details like the ID of the associated user and/or any scopes assigned to that user. -If you configure [JWT authentication](./authn-jwt), the Apollo Router automatically adds a JWT token's claims to the request's context at the `apollo_authentication::JWT::claims` key. -To participate in the authorization process, the coprocessor you add needs to set the key `apollo_authentication::JWT::claims` in request contexts. - ### JWT authentication configuration +If you configure [JWT authentication](./authn-jwt), the Apollo Router automatically adds a JWT token's claims to the request's context at the `apollo_authentication::JWT::claims` key. + To-do: Recipe for JWT auth with these directives ### Coprocessors for authorization +To participate in the authorization process, the coprocessor you add needs to set the key `apollo_authentication::JWT::claims` in request contexts. + To-do: Coprocessors specifications ## Authorization directives @@ -41,17 +42,15 @@ To-do: Coprocessors specifications ### `@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. +It works by checking for the `apollo_authentication::JWT::claims` key in a request's context. If the request is authenticated, the router executes the query in its entirety. -For unauthenticated requests, the router removes `@authenticated` fields before planning the query. -The router filters out fields that require authentication and only executes the parts of the query that don't require it. +For unauthenticated requests, the router removes `@authenticated` fields before planning the query and only executes the parts of the query that don't require authentication. If every field in a particular subgraph's query is marked as requiring authentication, this can eliminate entire subgraph requests. #### Prerequisites -To use the `@authenticated` directive in a subgraph you can either: -- [import it from the `@link` directive](/federation/federated-types/federated-directives/#importing-directives) like so: +To use the `@authenticated` directive in a subgraph you can [import it from the `@link` directive](/federation/federated-types/federated-directives/#importing-directives) like so: ```graphql extend schema @@ -60,32 +59,34 @@ extend schema import: [..., "@authenticated"]) ``` -- or define the directive like this: - -```graphql -directive @authenticated on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM -``` - -To-do: Enable them in some other way? Warn that turning them on can break things since previously unauthenticated requests will start returning errors. - #### Example `@authenticated` use case -Suppose you are building a social media platform and you only want authenticated users to be able to view other users' emails. -You also want to have a query called `me` that returns the authenticated user. +Suppose you are building a social media platform. Unauthenticated users can view all other parts of a public post—its title, author, etc. +However, you only want authenticated users to be able to see a post's number of views. +You also want to be able to query for an authenticated user's information. Your schema may look something like this: ```graphql type Query { me: User @authenticated - user(id: ID): User + post(id: ID!): Post } type User { - id: ID + id: ID! name: String - email: String @authenticated + posts: [Post!]! +} + +type Post { + id: ID! + author: User! + title: String! + content: String! + views: Int @authenticated } + ``` Consider the following query: @@ -94,11 +95,10 @@ Consider the following query: query { me { name - email } - user(id: "1234") { - name - email + post(id: "1234") { + title + views } } ``` @@ -112,36 +112,34 @@ For an unauthenticated request, the router would remove the `@authenticated` fie query { me { name - email } - user(id: "1234") { - name - email + post(id: "1234") { + title + views } } ``` ```graphql title="Query executed for an unauthenticated request" query { - user(id: "1234") { - name + post(id: "1234") { + title } } ``` -For an unauthenticated request, the router doesn't attempt to resolve the top-level `me` query, nor the email for the user with `id: "1234"`. +For an unauthenticated request, the router doesn't attempt to resolve the top-level `me` query, nor the views for the post with `id: "1234"`. The response retains the initial request's shape but returns `null` for unauthorized fields and applies the [standard GraphQL null propagation rules](https://www.apollographql.com/blog/graphql/basics/using-nullability-in-graphql/#what-happens-if-you-try-to-return-null-for-a-non-null-field). ```json title="Unauthenticated request's response" { "data": { "me": null, - "user": { - "name": "Ada", - "email": null - } + "post": { + "title": "Securing supergraphs", + } }, "errors": [ { @@ -156,8 +154,8 @@ The response retains the initial request's shape but returns `null` for unauthor { "message": "Unauthorized field or type", "path": [ - "user", - "email" + "post", + "views" ], "extensions": { "code": "UNAUTHORIZED_FIELD_OR_TYPE" @@ -171,20 +169,28 @@ If _every_ requested field requires authentication and a request is unauthentica ### `@requiresScopes` -The `@requiresScopes` directive marks fields and types as restricted based on the scopes you require. -The directive should include a `scopes` argument that expects an array of required scopes. +The `@requiresScopes` directive marks fields and types as restricted based on required scopes. +The directive should include a `scopes` argument that defines an array of the required scopes. + +```graphql +@requiresScopes(scopes: ["scope1", "scope2", "scope3"]) +``` -The directive validates the required scopes by loading the object at the `apollo_authentication::JWT::claims` key in a request's context. -That object's `scope` key should contain a space separated list of scopes in the format defined by the [OAuth2 RFC for access token scopes](https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). +The directive validates the required scopes by loading the claims object at the `apollo_authentication::JWT::claims` key in a request's context. +The claims object's `scope` key's value should be a space separated string of scopes in the format defined by the [OAuth2 RFC for access token scopes](https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). + +``` +claims = context["apollo_authentication::JWT::claims"] +claims["scope"] = "scope1 scope2 scope3" +``` Depending on the scopes present on the request, the router filters out unauthorized fields and types. -If a field's `scopes` array includes multiple scopes, the request must include _all_ required scopes to resolve it. +If a field's required `scopes` array includes multiple scopes, the request must include _all_ required scopes to resolve it. If every field on a particular subgraph query requires scopes that aren't present, this can eliminate entire subgraph requests. #### Prerequisites -To use the `@requiresScopes` directive in a subgraph you can either: -- [import it from the `@link` directive](/federation/federated-types/federated-directives/#importing-directives) like so: +To use the `@requiresScopes` directive in a subgraph you can [import it from the `@link` directive](/federation/federated-types/federated-directives/#importing-directives) like so: ```graphql extend schema @@ -193,138 +199,90 @@ extend schema import: [..., "@requiresScopes"]) ``` -- or define the directive like this: - -```graphql -directive @requiresScopes(scopes: [String!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM -``` - #### Example `@requiresScopes` use case -Imagine your social media platform has an ecommerce component. -You need to be able to query for the following: - -- An authenticated user's profile (`me`) -- Other users' emails -- Products and the users who have ordered them +Imagine your social media platform lets users view other users' information only if they have the required permissions. Your schema may look something like this: ```graphql title="" type Query { - me: User @requiresScopes(scopes: ["profile:read"]) - products: [Product!] + me: User @authenticated + user(id: ID!): User @requiresScopes(scopes: ["read:others"]) + users: [User!]! @requiresScopes(scopes: ["read:others"]) } type User { - id: ID + id: ID! name: String - email: @requiresScopes(["user:read"]) - products: [Product!] @requiresScopes(["user:read", "inventory:read"]) + email: String @requiresScopes(scopes: ["read:email"]) + profileImage: String + posts: [Post!]! } -type Product { - id: ID - name: String - amount: Int @requiresScopes(["inventory:read"]) - orders: [User!] @requiresScopes(["user:read", "inventory:read"]) +type Post { + id: ID! + author: User! + title: String! + content: String! + views: Int @authenticated } ``` The router executes the following query differently, depending on the request's attached scopes. -If the request includes the `profile:read user:read` scope set, then the router would execute the following filtered query: +If the request includes only the `read:others` scope, then the router would execute the following filtered query: ```graphql title="Raw query to router" query { - me { + users { name + profileImage email } - - products { - name - amount - orders { - name - } - } } ``` -```graphql title="Scopes: 'profile:read user:read'" +```graphql title="Scopes: 'read:others'" query { - me { - name - email - } - - products { + users { name + profileImage } } ``` -The response would include errors at the `/products/@/amount` and `/products/@/orders` paths. +The response would include errors at the `/users/@/email` path since that field requires the `read:emails` scope. -If the request includes the `user:read inventory:read` scope set, then the router would execute the following filtered query: +If the request includes the `read:others read:emails` scope set, then the router could successfully execute the entire query. - + -```graphql title="Raw query to router" -query { - me { - name - email - } +Take the [authorization demo](https://apollo-router-auth-demo.fly.dev/graphql) for a whirl! - products { - name - amount - orders { - name - } - } -} -``` - -```graphql title="Scopes: 'user:read inventory:read'" -query { - products { - name - amount - orders { - name - } - } -} -``` - - - -The response would include an error at the `/me` path. + ## Composition and federation Authorization directives are defined at the subgraph level and the GraphOS composes them in the supergraph schema. In other words, if subgraph fields or types include either `@authenticated` or `@requiresScopes` directives, they are set on the supergraph too. -If multiple shared subgraph fields include `@requiresScopes`, the supergraph schema merges the sets of scopes required by each subgraph. For example, imagine one subgraph requires the `profile:read` scope on the `users` query: +If multiple shared subgraph fields include `@requiresScopes`, the supergraph schema merges the sets of scopes required by each subgraph. For example, imagine one subgraph requires the `read:others` scope on the `users` query: ```graphql title="Subgraph A" type Query { - users: [User!]! @requiresScopes(scopes: ["profile:read"]) + users: [User!]! @requiresScopes(scopes: ["read:others"]) } ``` -And another subgraph requires the `user:read` scope on `users` query: +And another subgraph requires the read:profiles` scope on `users` query: ```graphql title="Subgraph B" type Query { - users: [User!]! @requiresScopes(scopes: ["user:read"]) + users: [User!]! @requiresScopes(scopes: ["read:profiles"]) } ``` @@ -332,14 +290,14 @@ Then the supergraph schema would require _both_ scopes for it. ```graphql title="Supergraph" type Query { - users: [User!]! @requiresScopes(scopes: ["profile:read", "user:read"]) + users: [User!]! @requiresScopes(scopes: ["read:others", "read:profiles"]) } ``` ### Authorization and `@key` fields The [`@key` directive](https://www.apollographql.com/docs/federation/entities/) lets you create an entity whose fields resolve across multiple subgraphs. -If you use authorization directives on fields in [`@key` directives](https://www.apollographql.com/docs/federation/entities/), Apollo still joins those fields between the subgraphs, but the client cannot query them directly. +If you use authorization directives on fields defined in [`@key` directives](https://www.apollographql.com/docs/federation/entities/), Apollo still uses those fields to compose entities between the subgraphs, but the client cannot query them directly. Consider these example subgraph schemas: @@ -396,24 +354,40 @@ This behavior is similar to what you can create with [contracts](/graphos/delive If a type [implementing an interface](https://www.apollographql.com/docs/apollo-server/schema/unions-interfaces/#interface-type) requires authorization, unauthorized requests can query the interface, but not any parts of the type that require authorization. -For example, consider this schema where the `User` interface doesn't require authentication, but the `Admin` type which implements `User` does: +For example, consider this schema where the `Post` interface doesn't require authentication, but the `PrivateBlog` type which implements `Post` does: ```graphql type Query { - users: [User!]! + post(id: ID!): Post } -interface User { - id: ID +type User { + id: ID! name: String + posts: [Post!]! } -type Admin -implements User -@authenticated { - id: ID - name: String - role: String +interface Post { + id: ID! + author: User! + title: String! + content: String! +} + +type PublicBlog implements Post { + id: ID! + author: User! + title: String! + content: String! +} + +type PrivateBlog implements Post @authenticated { + id: ID! + author: User! + title: String! + content: String! + publishAt: String + allowedViewers: [User!]! } ``` @@ -421,11 +395,12 @@ If an unauthenticated request were to make this query: ```graphql query { - users { + posts { id - name - ... on Admin { - role + author + title + ... on PrivateBlog { + allowedViewers } } } @@ -435,14 +410,21 @@ The router would filter the query as follows: ```graphql query { - users { + posts { id - name + author + title } } ``` -The response would include an `"UNAUTHORIZED_FIELD_OR_TYPE"` error at the `/users/@/role` path. +The response would include an `"UNAUTHORIZED_FIELD_OR_TYPE"` error at the `/posts/@/allowedViewers` path. + + + +Take the [authorization demo](https://apollo-router-auth-demo.fly.dev/graphql) for a whirl! + + ## Introspection From fa7ff60df0ad47b836da9831452a61bc055e8748 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Tue, 8 Aug 2023 15:17:31 -0600 Subject: [PATCH 41/82] Copy edits --- docs/source/configuration/authorization.mdx | 35 ++++++++++----------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 66e6606875..43a1b20f03 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -4,9 +4,13 @@ description: Strengthen your supergraph's security with advanced access controls minVersion: 1.27.0 --- -> ⚠️ **This is an [Enterprise feature](../enterprise-features/) of the Apollo Router.** It requires an organization with a [GraphOS Enterprise plan](https://www.apollographql.com/pricing/). -> -> If your organization _doesn't_ currently have an Enterprise plan, you can test out this functionality by signing up for a free [Enterprise trial](/graphos/org/plans/#enterprise-trials). +
+ +**This feature is available only with a [GraphOS Enterprise plan](/graphos/enterprise/).** It is currently in [preview](/resources/product-launch-stages#preview). + +If your organization _doesn't_ currently have an Enterprise plan, you can test this functionality by signing up for a free [Enterprise trial](/graphos/org/plans/#enterprise-trials). + +
APIs provide access to business-critical data. Unrestricted access can result in data breaches, monetary losses, or potential denial of service. Even for internal APIs, checks can be essential to limit data to authorized parties. @@ -23,13 +27,13 @@ The router then enforces these directives on all incoming requests. ## Prerequisites To use the router's authorization directives, you need to either configure [JWT authentication](./authn-jwt) or add a [router service coprocessor](../customizations/coprocessor) that adds **claims** to a request's context. -**Claims** are the individual details of a request's scope. They might include details like the ID of the associated user and/or any scopes assigned to that user. +Claims are the individual details of a request's authentication and scope. They might include details like the ID of the associated user and/or any scopes assigned to that user. ### JWT authentication configuration If you configure [JWT authentication](./authn-jwt), the Apollo Router automatically adds a JWT token's claims to the request's context at the `apollo_authentication::JWT::claims` key. -To-do: Recipe for JWT auth with these directives +To-do: More information on how to use JWT auth with these directives ### Coprocessors for authorization @@ -186,7 +190,8 @@ claims["scope"] = "scope1 scope2 scope3" Depending on the scopes present on the request, the router filters out unauthorized fields and types. If a field's required `scopes` array includes multiple scopes, the request must include _all_ required scopes to resolve it. -If every field on a particular subgraph query requires scopes that aren't present, this can eliminate entire subgraph requests. + +Like the efficiencies gained via the `@authenticated` directive, if every field on a particular subgraph query requires scopes that aren't present, this can eliminate entire subgraph requests. #### Prerequisites @@ -202,7 +207,6 @@ extend schema #### Example `@requiresScopes` use case Imagine your social media platform lets users view other users' information only if they have the required permissions. - Your schema may look something like this: ```graphql title="" @@ -258,11 +262,11 @@ query { The response would include errors at the `/users/@/email` path since that field requires the `read:emails` scope. -If the request includes the `read:others read:emails` scope set, then the router could successfully execute the entire query. +If the request includes the `read:others read:emails` scope set, then the router can successfully execute the entire query. -Take the [authorization demo](https://apollo-router-auth-demo.fly.dev/graphql) for a whirl! +Check out the [authorization demo](https://apollo-router-auth-demo.fly.dev/graphql)! @@ -278,7 +282,7 @@ type Query { } ``` -And another subgraph requires the read:profiles` scope on `users` query: +And another subgraph requires the `read:profiles` scope on `users` query: ```graphql title="Subgraph B" type Query { @@ -358,7 +362,7 @@ For example, consider this schema where the `Post` interface doesn't require aut ```graphql type Query { - post(id: ID!): Post + posts: [Post!]! } type User { @@ -374,13 +378,6 @@ interface Post { content: String! } -type PublicBlog implements Post { - id: ID! - author: User! - title: String! - content: String! -} - type PrivateBlog implements Post @authenticated { id: ID! author: User! @@ -422,7 +419,7 @@ The response would include an `"UNAUTHORIZED_FIELD_OR_TYPE"` error at the `/post -Take the [authorization demo](https://apollo-router-auth-demo.fly.dev/graphql) for a whirl! +Check out the [authorization demo](https://apollo-router-auth-demo.fly.dev/graphql)! From 8a3fa5892cf22cd4c997c32e6f1948c3f257f762 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Tue, 8 Aug 2023 15:53:08 -0600 Subject: [PATCH 42/82] Remove links to demo --- docs/source/configuration/authorization.mdx | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 43a1b20f03..316db5ad33 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -264,12 +264,6 @@ The response would include errors at the `/users/@/email` path since that field If the request includes the `read:others read:emails` scope set, then the router can successfully execute the entire query. - - -Check out the [authorization demo](https://apollo-router-auth-demo.fly.dev/graphql)! - - - ## Composition and federation Authorization directives are defined at the subgraph level and the GraphOS composes them in the supergraph schema. In other words, if subgraph fields or types include either `@authenticated` or `@requiresScopes` directives, they are set on the supergraph too. @@ -417,12 +411,6 @@ query { The response would include an `"UNAUTHORIZED_FIELD_OR_TYPE"` error at the `/posts/@/allowedViewers` path. - - -Check out the [authorization demo](https://apollo-router-auth-demo.fly.dev/graphql)! - - - ## Introspection Authorization directives don't affect introspection; all fields that require authorization remain visible. However, directives applied to fields _aren't_ visible. If introspection might reveal too much information about internal types, then be sure to deactivate it: From 2ab7433bda41b62f6d3cd6cbe0aae547b4f58ee9 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Wed, 9 Aug 2023 16:11:42 +0200 Subject: [PATCH 43/82] move the claim augmentation example --- docs/source/configuration/authn-jwt.mdx | 44 ------------- docs/source/configuration/authorization.mdx | 68 ++++++++++++++++++++- 2 files changed, 65 insertions(+), 47 deletions(-) diff --git a/docs/source/configuration/authn-jwt.mdx b/docs/source/configuration/authn-jwt.mdx index a01c5c40dc..ed072b93cd 100644 --- a/docs/source/configuration/authn-jwt.mdx +++ b/docs/source/configuration/authn-jwt.mdx @@ -263,50 +263,6 @@ fn subgraph_service(service, subgraph) { -### Claim augmentation via coprocessors - -Tokens can come in with limited information, that is then used to look up user specific information like roles. This can be done with [coprocessors](/customizations/coprocessor). - - - -The router level coprocessor is guaranteed to be called after the authentication plugin, so the coprocessor can receive the list of claims extracted from the token, use information like the `sub` (subject) claim to look up the user, insert its data in the claims list and return it to the router. - -If the router is configured with: - -```yaml title="router.yaml" -authentication: - jwt: - jwks: - - url: "file:///etc/router/jwks.json" - -coprocessor: - url: http://127.0.0.1:8081 - router: - request: - context: true -``` - -The coprocessor will then receive a request with this format: - -```json -{ - "version": 1, - "stage": "RouterRequest", - "control": "continue", - "id": "d0a8245df0efe8aa38a80dba1147fb2e", - "context": { - "entries": { - "apollo_authentication::JWT::claims": { - "exp": 10000000000, - "sub": "457f6bb6-789c-4e8b-8560-f3943a09e72a" - } - } - }, - "method": "POST" -} -``` - - ## Creating your own JWKS (advanced) > ⚠️ **Most third-party IdP services create and host a JWKS for you.** If you use a third-party IdP, consult its documentation to obtain the [JWKS URL](#jwks) to pass to your router. diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 316db5ad33..c57df67778 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -35,11 +35,73 @@ If you configure [JWT authentication](./authn-jwt), the Apollo Router automatica To-do: More information on how to use JWT auth with these directives -### Coprocessors for authorization -To participate in the authorization process, the coprocessor you add needs to set the key `apollo_authentication::JWT::claims` in request contexts. +### Claim augmentation via coprocessors -To-do: Coprocessors specifications +Tokens can come in with limited information, that is then used to look up user specific information like roles. This can be done with [coprocessors](/customizations/coprocessor). + + + +The router level coprocessor is guaranteed to be called after the authentication plugin, so the coprocessor can receive the list of claims extracted from the token, use information like the `sub` (subject) claim to look up the user, insert its data in the claims list and return it to the router. + +If the router is configured with: + +```yaml title="router.yaml" +authentication: + jwt: + jwks: + - url: "file:///etc/router/jwks.json" + +coprocessor: + url: http://127.0.0.1:8081 + router: + request: + context: true +``` + +The coprocessor can then receive a request with this format: + +```json +{ + "version": 1, + "stage": "RouterRequest", + "control": "continue", + "id": "d0a8245df0efe8aa38a80dba1147fb2e", + "context": { + "entries": { + "apollo_authentication::JWT::claims": { + "exp": 10000000000, + "sub": "457f6bb6-789c-4e8b-8560-f3943a09e72a" + } + } + }, + "method": "POST" +} +``` + +The coprocessor would then look up the user with identifier specified in the `sub` claim, and return a response with more claims: + + +```json +{ + // Control properties + "version": 1, + "stage": "RouterResponse", + "control": "continue", + "id": "d0a8245df0efe8aa38a80dba1147fb2e", + "context": { + "entries": { + "apollo_authentication::JWT::claims": { + "exp": 10000000000, + "sub": "457f6bb6-789c-4e8b-8560-f3943a09e72a", + "scope": "profile:read profile:write" + } + } + } +} +``` + + ## Authorization directives From 2791253d26a322215f8eb058d1a148b1c50e2a6f Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Thu, 10 Aug 2023 10:00:21 +0200 Subject: [PATCH 44/82] Update docs/source/configuration/authorization.mdx --- docs/source/configuration/authorization.mdx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index c57df67778..5db7c3cdc3 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -48,9 +48,10 @@ If the router is configured with: ```yaml title="router.yaml" authentication: - jwt: - jwks: - - url: "file:///etc/router/jwks.json" + router: + jwt: + jwks: + - url: "file:///etc/router/jwks.json" coprocessor: url: http://127.0.0.1:8081 From 19d6c5f36b31f6835339cec15712b53c849bcda5 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Fri, 11 Aug 2023 10:59:36 +0200 Subject: [PATCH 45/82] implement the policy directive (#3406) Definition: ```graphql directive @policy(policies: [String!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM ``` `@policy` is designed for usage with coprocessors: - extract the list of policies relevant to the query, store them in the context - the coprocessor (or a rhai or native plugin) goes through the list of policies and marks them as successful or not - the router then filters fields from the query according to which policies were successful the `policy` argument could be the actual authorization policy to execute, in text form, or an index into a list of policies that the coprocessor knows how to execute. This will allow router authorization to leverage existing authorization systems, with custom policy languages, or ones that call into central state like a roles database. Field filtering and null propagation happens in exactly the same way as the other authorization directives, and can be used with them in the same schema --- ...nfiguration__tests__schema_generation.snap | 2 +- .../src/plugins/authorization/mod.rs | 187 ++++- .../src/plugins/authorization/policy.rs | 643 ++++++++++++++++++ ...authorization__policy__tests__array-2.snap | 10 + ...authorization__policy__tests__array-3.snap | 17 + ...__authorization__policy__tests__array.snap | 9 + ...tion__policy__tests__extract_policies.snap | 10 + ...__policy__tests__filter_basic_query-2.snap | 10 + ...__policy__tests__filter_basic_query-3.snap | 23 + ...__policy__tests__filter_basic_query-4.snap | 11 + ...__policy__tests__filter_basic_query-5.snap | 13 + ...__policy__tests__filter_basic_query-6.snap | 14 + ...__policy__tests__filter_basic_query-7.snap | 16 + ...__policy__tests__filter_basic_query-8.snap | 14 + ...__policy__tests__filter_basic_query-9.snap | 16 + ...on__policy__tests__filter_basic_query.snap | 10 + ...__policy__tests__interface_fragment-2.snap | 13 + ...__policy__tests__interface_fragment-3.snap | 16 + ...__policy__tests__interface_fragment-4.snap | 17 + ...__policy__tests__interface_fragment-5.snap | 5 + ...on__policy__tests__interface_fragment.snap | 8 + ...y__tests__interface_inline_fragment-2.snap | 13 + ...y__tests__interface_inline_fragment-3.snap | 16 + ...icy__tests__interface_inline_fragment.snap | 8 + ...horization__policy__tests__mutation-2.snap | 5 + ...horization__policy__tests__mutation-3.snap | 13 + ...uthorization__policy__tests__mutation.snap | 9 + ...ization__policy__tests__query_field-2.snap | 10 + ...ization__policy__tests__query_field-3.snap | 13 + ...orization__policy__tests__query_field.snap | 9 + ...uthorization__policy__tests__scalar-2.snap | 10 + ...uthorization__policy__tests__scalar-3.snap | 16 + ..._authorization__policy__tests__scalar.snap | 7 + apollo-router/tests/redis_test.rs | 4 +- 34 files changed, 1162 insertions(+), 35 deletions(-) create mode 100644 apollo-router/src/plugins/authorization/policy.rs create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__array-2.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__array-3.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__array.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__extract_policies.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__filter_basic_query-2.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__filter_basic_query-3.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__filter_basic_query-4.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__filter_basic_query-5.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__filter_basic_query-6.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__filter_basic_query-7.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__filter_basic_query-8.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__filter_basic_query-9.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__filter_basic_query.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_fragment-2.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_fragment-3.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_fragment-4.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_fragment-5.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_fragment.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_inline_fragment-2.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_inline_fragment-3.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_inline_fragment.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__mutation-2.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__mutation-3.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__mutation.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__query_field-2.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__query_field-3.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__query_field.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__scalar-2.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__scalar-3.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__scalar.snap diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap index 602bec3419..831e4a2437 100644 --- a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap @@ -514,7 +514,7 @@ expression: "&schema" "type": "object", "properties": { "experimental_enable_authorization_directives": { - "description": "enables the `@authenticated` and `@hasScopes` directives", + "description": "enables the `@authenticated`, `@requiresScopes` and `@policy` directives", "default": false, "type": "boolean" }, diff --git a/apollo-router/src/plugins/authorization/mod.rs b/apollo-router/src/plugins/authorization/mod.rs index bca300fa76..32ce750356 100644 --- a/apollo-router/src/plugins/authorization/mod.rs +++ b/apollo-router/src/plugins/authorization/mod.rs @@ -1,5 +1,6 @@ //! Authorization plugin +use std::collections::HashMap; use std::collections::HashSet; use std::ops::ControlFlow; use std::sync::Arc; @@ -11,6 +12,7 @@ use router_bridge::planner::UsageReporting; use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; +use serde_json_bytes::Value; use tokio::sync::Mutex; use tower::BoxError; use tower::ServiceBuilder; @@ -18,6 +20,9 @@ use tower::ServiceExt; use self::authenticated::AuthenticatedVisitor; use self::authenticated::AUTHENTICATED_DIRECTIVE_NAME; +use self::policy::PolicyExtractionVisitor; +use self::policy::PolicyFilteringVisitor; +use self::policy::POLICY_DIRECTIVE_NAME; use self::scopes::ScopeExtractionVisitor; use self::scopes::ScopeFilteringVisitor; use self::scopes::REQUIRES_SCOPES_DIRECTIVE_NAME; @@ -37,6 +42,7 @@ use crate::register_plugin; use crate::services::supergraph; use crate::spec::query::transform; use crate::spec::query::traverse; +use crate::spec::query::QUERY_EXECUTABLE; use crate::spec::Query; use crate::spec::Schema; use crate::spec::SpecError; @@ -45,14 +51,17 @@ use crate::Configuration; use crate::Context; pub(crate) mod authenticated; +pub(crate) mod policy; pub(crate) mod scopes; const REQUIRED_SCOPES_KEY: &str = "apollo_authorization::scopes::required"; +const REQUIRED_POLICIES_KEY: &str = "apollo_authorization::policies::required"; #[derive(Clone, Debug, Default, Hash, PartialEq, Eq, Serialize)] pub(crate) struct CacheKeyMetadata { is_authenticated: bool, scopes: Vec, + policies: Vec, } /// Authorization plugin @@ -62,7 +71,7 @@ pub(crate) struct Conf { /// Reject unauthenticated requests #[serde(default)] require_authentication: bool, - /// enables the `@authenticated` and `@hasScopes` directives + /// enables the `@authenticated`, `@requiresScopes` and `@policy` directives #[serde(default)] experimental_enable_authorization_directives: bool, } @@ -94,7 +103,12 @@ impl AuthorizationPlugin { .type_system .definitions .directives - .contains_key(REQUIRES_SCOPES_DIRECTIVE_NAME); + .contains_key(REQUIRES_SCOPES_DIRECTIVE_NAME) + || schema + .type_system + .definitions + .directives + .contains_key(POLICY_DIRECTIVE_NAME); match has_config { Some(b) => Ok(b), @@ -126,6 +140,21 @@ impl AuthorizationPlugin { context.insert(REQUIRED_SCOPES_KEY, scopes).unwrap(); } + + let mut visitor = PolicyExtractionVisitor::new(&compiler, file_id); + + // if this fails, the query is invalid and will fail at the query planning phase. + // We do not return validation errors here for now because that would imply a huge + // refactoring of telemetry and tests + if traverse::document(&mut visitor, file_id).is_ok() { + let policies: HashMap> = visitor + .extracted_policies + .into_iter() + .map(|policy| (policy, None)) + .collect(); + + context.insert(REQUIRED_POLICIES_KEY, policies).unwrap(); + } } pub(crate) fn update_cache_key(context: &Context) { @@ -156,9 +185,25 @@ impl AuthorizationPlugin { }; scopes.sort(); + let mut policies = context + .get_json_value(REQUIRED_POLICIES_KEY) + .and_then(|v| { + v.as_object().map(|v| { + v.iter() + .filter_map(|(policy, result)| match result { + Value::Bool(true) => Some(policy.as_str().to_string()), + _ => None, + }) + .collect::>() + }) + }) + .unwrap_or_default(); + policies.sort(); + context.private_entries.lock().insert(CacheKeyMetadata { is_authenticated, scopes, + policies, }); } @@ -175,14 +220,16 @@ impl AuthorizationPlugin { let is_authenticated = key.metadata.is_authenticated; let scopes = &key.metadata.scopes; + let policies = &key.metadata.policies; + + let mut is_filtered = false; + let mut unauthorized_paths: Vec = vec![]; let filter_res = Self::authenticated_filter_query(&compiler, is_authenticated)?; - let filter_res = match filter_res { - None => Self::scopes_filter_query(&compiler, scopes).map(|opt| { - opt.map(|(query, paths)| (query, paths, Arc::new(Mutex::new(compiler)))) - }), - Some((query, mut paths)) => { + let compiler = match filter_res { + None => compiler, + Some((query, paths)) => { if query.is_empty() { return Err(QueryPlannerError::PlanningErrors(PlanErrors { errors: Arc::new(vec![router_bridge::planner::PlanError { @@ -196,28 +243,53 @@ impl AuthorizationPlugin { }, })); } + + is_filtered = true; + unauthorized_paths.extend(paths.into_iter()); + let mut compiler = ApolloCompiler::new(); compiler.set_type_system_hir(schema.type_system.clone()); let _id = compiler.add_executable(&query, "query"); + compiler + } + }; - match Self::scopes_filter_query(&compiler, scopes)? { - None => Ok(Some((query, paths, Arc::new(Mutex::new(compiler))))), - Some((new_query, new_paths)) => { - let mut compiler = ApolloCompiler::new(); - compiler.set_type_system_hir(schema.type_system.clone()); - let _id = compiler.add_executable(&new_query, "query"); - paths.extend(new_paths.into_iter()); - Ok(Some((new_query, paths, Arc::new(Mutex::new(compiler))))) - } + let filter_res = Self::scopes_filter_query(&compiler, scopes)?; + + let compiler = match filter_res { + None => compiler, + Some((query, paths)) => { + if query.is_empty() { + return Err(QueryPlannerError::PlanningErrors(PlanErrors { + errors: Arc::new(vec![router_bridge::planner::PlanError { + message: Some("Unauthorized query".to_string()), + extensions: None, + validation_error: false, + }]), + usage_reporting: UsageReporting { + stats_report_key: GRAPHQL_VALIDATION_FAILURE_ERROR_KEY.to_string(), + referenced_fields_by_type: Default::default(), + }, + })); } + + is_filtered = true; + unauthorized_paths.extend(paths.into_iter()); + + let mut compiler = ApolloCompiler::new(); + compiler.set_type_system_hir(schema.type_system.clone()); + let _id = compiler.add_executable(&query, "query"); + compiler } - }?; + }; - match filter_res { - None => Ok(None), - Some((filtered_query, paths, _)) => { - if filtered_query.is_empty() { - Err(QueryPlannerError::PlanningErrors(PlanErrors { + let filter_res = Self::policies_filter_query(&compiler, policies)?; + + let compiler = match filter_res { + None => compiler, + Some((query, paths)) => { + if query.is_empty() { + return Err(QueryPlannerError::PlanningErrors(PlanErrors { errors: Arc::new(vec![router_bridge::planner::PlanError { message: Some("Unauthorized query".to_string()), extensions: None, @@ -227,18 +299,37 @@ impl AuthorizationPlugin { stats_report_key: GRAPHQL_VALIDATION_FAILURE_ERROR_KEY.to_string(), referenced_fields_by_type: Default::default(), }, - })) - } else { - let mut compiler = ApolloCompiler::new(); - compiler.set_type_system_hir(schema.type_system.clone()); - let _id = compiler.add_executable(&filtered_query, "query"); - Ok(Some(( - filtered_query, - paths, - Arc::new(Mutex::new(compiler)), - ))) + })); } + + is_filtered = true; + unauthorized_paths.extend(paths.into_iter()); + + let mut compiler = ApolloCompiler::new(); + compiler.set_type_system_hir(schema.type_system.clone()); + let _id = compiler.add_executable(&query, "query"); + compiler } + }; + + if is_filtered { + let file_id = compiler + .db + .source_file(QUERY_EXECUTABLE.into()) + .ok_or_else(|| { + QueryPlannerError::SpecError(SpecError::ValidationError( + "missing input file for query".to_string(), + )) + })?; + let filtered_query = compiler.db.source_code(file_id).to_string(); + + Ok(Some(( + filtered_query, + unauthorized_paths, + Arc::new(Mutex::new(compiler)), + ))) + } else { + Ok(None) } } @@ -307,6 +398,38 @@ impl AuthorizationPlugin { Ok(None) } } + + fn policies_filter_query( + compiler: &ApolloCompiler, + policies: &[String], + ) -> Result)>, QueryPlannerError> { + let id = compiler + .db + .executable_definition_files() + .pop() + .expect("the query was added to the compiler earlier"); + + let mut visitor = + PolicyFilteringVisitor::new(compiler, id, policies.iter().cloned().collect()); + + let modified_query = transform::document(&mut visitor, id) + .map_err(|e| SpecError::ParsingError(e.to_string()))? + .to_string(); + + if visitor.query_requires_policies { + tracing::debug!("the query required policies, the requests present policies: {policies:?}, modified query:\n{modified_query}\nunauthorized paths: {:?}", + visitor + .unauthorized_paths + .iter() + .map(|path| path.to_string()) + .collect::>() + ); + Ok(Some((modified_query, visitor.unauthorized_paths))) + } else { + tracing::debug!("the query does not require policies"); + Ok(None) + } + } } #[async_trait::async_trait] diff --git a/apollo-router/src/plugins/authorization/policy.rs b/apollo-router/src/plugins/authorization/policy.rs new file mode 100644 index 0000000000..30b779709c --- /dev/null +++ b/apollo-router/src/plugins/authorization/policy.rs @@ -0,0 +1,643 @@ +//! Authorization plugin + +use std::collections::HashSet; + +use apollo_compiler::hir; +use apollo_compiler::hir::FieldDefinition; +use apollo_compiler::hir::TypeDefinition; +use apollo_compiler::hir::Value; +use apollo_compiler::ApolloCompiler; +use apollo_compiler::FileId; +use apollo_compiler::HirDatabase; +use tower::BoxError; + +use crate::json_ext::Path; +use crate::json_ext::PathElement; +use crate::spec::query::transform; +use crate::spec::query::traverse; + +pub(crate) struct PolicyExtractionVisitor<'a> { + compiler: &'a ApolloCompiler, + file_id: FileId, + pub(crate) extracted_policies: HashSet, +} + +pub(crate) const POLICY_DIRECTIVE_NAME: &str = "policy"; + +impl<'a> PolicyExtractionVisitor<'a> { + #[allow(dead_code)] + pub(crate) fn new(compiler: &'a ApolloCompiler, file_id: FileId) -> Self { + Self { + compiler, + file_id, + extracted_policies: HashSet::new(), + } + } + + fn get_policies_from_field(&mut self, field: &FieldDefinition) { + self.extracted_policies + .extend(policy_argument(field.directive_by_name(POLICY_DIRECTIVE_NAME)).cloned()); + + if let Some(ty) = field.ty().type_def(&self.compiler.db) { + self.get_policies_from_type(&ty) + } + } + + fn get_policies_from_type(&mut self, ty: &TypeDefinition) { + self.extracted_policies + .extend(policy_argument(ty.directive_by_name(POLICY_DIRECTIVE_NAME)).cloned()); + } +} + +fn policy_argument(opt_directive: Option<&hir::Directive>) -> impl Iterator { + opt_directive + .and_then(|directive| directive.argument_by_name("policies")) + .and_then(|value| match value { + Value::List { value, .. } => Some(value), + _ => None, + }) + .into_iter() + .flatten() + .filter_map(|v| match v { + Value::String { value, .. } => Some(value), + _ => None, + }) +} + +impl<'a> traverse::Visitor for PolicyExtractionVisitor<'a> { + fn compiler(&self) -> &ApolloCompiler { + self.compiler + } + + fn field(&mut self, parent_type: &str, node: &hir::Field) -> Result<(), BoxError> { + if let Some(ty) = self + .compiler + .db + .types_definitions_by_name() + .get(parent_type) + { + if let Some(field) = ty.field(&self.compiler.db, node.name()) { + self.get_policies_from_field(field); + } + } + + traverse::field(self, parent_type, node) + } + + fn fragment_definition(&mut self, node: &hir::FragmentDefinition) -> Result<(), BoxError> { + if let Some(ty) = self + .compiler + .db + .types_definitions_by_name() + .get(node.type_condition()) + { + self.get_policies_from_type(ty); + } + traverse::fragment_definition(self, node) + } + + fn fragment_spread(&mut self, node: &hir::FragmentSpread) -> Result<(), BoxError> { + let fragments = self.compiler.db.fragments(self.file_id); + let type_condition = fragments + .get(node.name()) + .ok_or("MissingFragmentDefinition")? + .type_condition(); + + if let Some(ty) = self + .compiler + .db + .types_definitions_by_name() + .get(type_condition) + { + self.get_policies_from_type(ty); + } + traverse::fragment_spread(self, node) + } + + fn inline_fragment( + &mut self, + parent_type: &str, + + node: &hir::InlineFragment, + ) -> Result<(), BoxError> { + if let Some(type_condition) = node.type_condition() { + if let Some(ty) = self + .compiler + .db + .types_definitions_by_name() + .get(type_condition) + { + self.get_policies_from_type(ty); + } + } + traverse::inline_fragment(self, parent_type, node) + } +} + +pub(crate) struct PolicyFilteringVisitor<'a> { + compiler: &'a ApolloCompiler, + file_id: FileId, + request_policies: HashSet, + pub(crate) query_requires_policies: bool, + pub(crate) unauthorized_paths: Vec, + current_path: Path, +} + +impl<'a> PolicyFilteringVisitor<'a> { + pub(crate) fn new( + compiler: &'a ApolloCompiler, + file_id: FileId, + successful_policies: HashSet, + ) -> Self { + Self { + compiler, + file_id, + request_policies: successful_policies, + query_requires_policies: false, + unauthorized_paths: vec![], + current_path: Path::default(), + } + } + + fn is_field_authorized(&mut self, field: &FieldDefinition) -> bool { + let field_policies = policy_argument(field.directive_by_name(POLICY_DIRECTIVE_NAME)) + .cloned() + .collect::>(); + if !self.request_policies.is_superset(&field_policies) { + return false; + } + + if let Some(ty) = field.ty().type_def(&self.compiler.db) { + self.is_type_authorized(&ty) + } else { + false + } + } + + fn is_type_authorized(&self, ty: &TypeDefinition) -> bool { + let type_policies = policy_argument(ty.directive_by_name(POLICY_DIRECTIVE_NAME)) + .cloned() + .collect::>(); + self.request_policies.is_superset(&type_policies) + } +} + +impl<'a> transform::Visitor for PolicyFilteringVisitor<'a> { + fn compiler(&self) -> &ApolloCompiler { + self.compiler + } + + fn field( + &mut self, + parent_type: &str, + node: &hir::Field, + ) -> Result, BoxError> { + let field_name = node.name(); + let mut is_field_list = false; + + let is_authorized = self + .compiler + .db + .types_definitions_by_name() + .get(parent_type) + .is_some_and(|def| { + if let Some(field) = def.field(&self.compiler.db, field_name) { + if field.ty().is_list() { + is_field_list = true; + } + self.is_field_authorized(field) + } else { + false + } + }); + + self.current_path.push(PathElement::Key(field_name.into())); + if is_field_list { + self.current_path.push(PathElement::Flatten); + } + + if !is_authorized { + self.unauthorized_paths.push(self.current_path.clone()); + } + + let res = if is_authorized { + transform::field(self, parent_type, node) + } else { + self.query_requires_policies = true; + Ok(None) + }; + + if is_field_list { + self.current_path.pop(); + } + self.current_path.pop(); + + res + } + + fn fragment_definition( + &mut self, + node: &hir::FragmentDefinition, + ) -> Result, BoxError> { + let fragment_is_authorized = self + .compiler + .db + .types_definitions_by_name() + .get(node.type_condition()) + .is_some_and(|ty| self.is_type_authorized(ty)); + + if !fragment_is_authorized { + Ok(None) + } else { + transform::fragment_definition(self, node) + } + } + + fn fragment_spread( + &mut self, + node: &hir::FragmentSpread, + ) -> Result, BoxError> { + let fragments = self.compiler.db.fragments(self.file_id); + let condition = fragments + .get(node.name()) + .ok_or("MissingFragmentDefinition")? + .type_condition(); + self.current_path + .push(PathElement::Fragment(condition.into())); + + let fragment_is_authorized = self + .compiler + .db + .types_definitions_by_name() + .get(condition) + .is_some_and(|ty| self.is_type_authorized(ty)); + + let res = if !fragment_is_authorized { + self.query_requires_policies = true; + self.unauthorized_paths.push(self.current_path.clone()); + + Ok(None) + } else { + transform::fragment_spread(self, node) + }; + + self.current_path.pop(); + res + } + + fn inline_fragment( + &mut self, + parent_type: &str, + + node: &hir::InlineFragment, + ) -> Result, BoxError> { + match node.type_condition() { + None => { + self.current_path.push(PathElement::Fragment(String::new())); + let res = transform::inline_fragment(self, parent_type, node); + self.current_path.pop(); + res + } + Some(name) => { + self.current_path.push(PathElement::Fragment(name.into())); + + let fragment_is_authorized = self + .compiler + .db + .types_definitions_by_name() + .get(name) + .is_some_and(|ty| self.is_type_authorized(ty)); + + let res = if !fragment_is_authorized { + self.query_requires_policies = true; + self.unauthorized_paths.push(self.current_path.clone()); + Ok(None) + } else { + transform::inline_fragment(self, parent_type, node) + }; + + self.current_path.pop(); + + res + } + } + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeSet; + use std::collections::HashSet; + + use apollo_compiler::ApolloCompiler; + use apollo_encoder::Document; + + use crate::json_ext::Path; + use crate::plugins::authorization::policy::PolicyExtractionVisitor; + use crate::plugins::authorization::policy::PolicyFilteringVisitor; + use crate::spec::query::transform; + use crate::spec::query::traverse; + + static BASIC_SCHEMA: &str = r#" + directive @policy(policies: [String]) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM + + type Query { + topProducts: Product + customer: User + me: User @policy(policies: ["profile"]) + itf: I + } + + type Mutation { + ping: User @policy(policies: ["ping"]) + } + + interface I { + id: ID + } + + type Product { + type: String + price(setPrice: Int): Int + reviews: [Review] + internal: Internal + publicReviews: [Review] + } + + scalar Internal @policy(policies: ["internal"]) @specifiedBy(url: "http///example.com/test") + + type Review @policy(policies: ["review"]) { + body: String + author: User + } + + type User implements I @policy(policies: ["read user"]) { + id: ID + name: String @policy(policies: ["read username"]) + } + "#; + + fn extract(query: &str) -> BTreeSet { + let mut compiler = ApolloCompiler::new(); + + let _schema_id = compiler.add_type_system(BASIC_SCHEMA, "schema.graphql"); + let id = compiler.add_executable(query, "query.graphql"); + + let diagnostics = compiler.validate(); + for diagnostic in &diagnostics { + println!("{diagnostic}"); + } + assert!(diagnostics.is_empty()); + + let mut visitor = PolicyExtractionVisitor::new(&compiler, id); + traverse::document(&mut visitor, id).unwrap(); + + visitor.extracted_policies.into_iter().collect() + } + + #[test] + fn extract_policies() { + static QUERY: &str = r#" + { + topProducts { + type + internal + } + + me { + name + } + } + "#; + + let doc = extract(QUERY); + insta::assert_debug_snapshot!(doc); + } + + fn filter(query: &str, policies: HashSet) -> (Document, Vec) { + let mut compiler = ApolloCompiler::new(); + + let _schema_id = compiler.add_type_system(BASIC_SCHEMA, "schema.graphql"); + let file_id = compiler.add_executable(query, "query.graphql"); + + let diagnostics = compiler.validate(); + for diagnostic in &diagnostics { + println!("{diagnostic}"); + } + assert!(diagnostics.is_empty()); + + let mut visitor = PolicyFilteringVisitor::new(&compiler, file_id, policies); + ( + transform::document(&mut visitor, file_id).unwrap(), + visitor.unauthorized_paths, + ) + } + + #[test] + fn filter_basic_query() { + static QUERY: &str = r#" + { + topProducts { + type + internal + } + + me { + id + name + } + } + "#; + + let doc = extract(QUERY); + insta::assert_debug_snapshot!(doc); + + let (doc, paths) = filter(QUERY, HashSet::new()); + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + + let (doc, paths) = filter( + QUERY, + ["profile".to_string(), "internal".to_string()] + .into_iter() + .collect(), + ); + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + + let (doc, paths) = filter( + QUERY, + [ + "profile".to_string(), + "read user".to_string(), + "internal".to_string(), + ] + .into_iter() + .collect(), + ); + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + + let (doc, paths) = filter( + QUERY, + [ + "profile".to_string(), + "read user".to_string(), + "read username".to_string(), + ] + .into_iter() + .collect(), + ); + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + } + + #[test] + fn mutation() { + static QUERY: &str = r#" + mutation { + ping { + name + } + } + "#; + + let doc = extract(QUERY); + insta::assert_debug_snapshot!(doc); + + let (doc, paths) = filter(QUERY, HashSet::new()); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + } + + #[test] + fn query_field() { + static QUERY: &str = r#" + query { + topProducts { + type + } + + me { + name + } + } + "#; + + let doc = extract(QUERY); + insta::assert_debug_snapshot!(doc); + + let (doc, paths) = filter(QUERY, HashSet::new()); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + } + + #[test] + fn scalar() { + static QUERY: &str = r#" + query { + topProducts { + type + internal + } + } + "#; + + let doc = extract(QUERY); + insta::assert_debug_snapshot!(doc); + + let (doc, paths) = filter(QUERY, HashSet::new()); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + } + + #[test] + fn array() { + static QUERY: &str = r#" + query { + topProducts { + type + publicReviews { + body + author { + name + } + } + } + } + "#; + + let doc = extract(QUERY); + insta::assert_debug_snapshot!(doc); + + let (doc, paths) = filter(QUERY, HashSet::new()); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + } + + #[test] + fn interface_inline_fragment() { + static QUERY: &str = r#" + query { + topProducts { + type + } + itf { + id + ... on User { + name + } + } + } + "#; + + let doc = extract(QUERY); + insta::assert_debug_snapshot!(doc); + + let (doc, paths) = filter(QUERY, HashSet::new()); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + } + + #[test] + fn interface_fragment() { + static QUERY: &str = r#" + query { + topProducts { + type + } + itf { + id + ...F + } + } + + fragment F on User { + name + } + "#; + + let doc = extract(QUERY); + insta::assert_debug_snapshot!(doc); + + let (doc, paths) = filter(QUERY, HashSet::new()); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + + let (doc, paths) = filter( + QUERY, + ["read user".to_string(), "read username".to_string()] + .into_iter() + .collect(), + ); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + } +} diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__array-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__array-2.snap new file mode 100644 index 0000000000..34750f1eca --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__array-2.snap @@ -0,0 +1,10 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: doc +--- +query { + topProducts { + type + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__array-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__array-3.snap new file mode 100644 index 0000000000..ffb5abfd56 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__array-3.snap @@ -0,0 +1,17 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: paths +--- +[ + Path( + [ + Key( + "topProducts", + ), + Key( + "publicReviews", + ), + Flatten, + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__array.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__array.snap new file mode 100644 index 0000000000..b7ea0cf0e1 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__array.snap @@ -0,0 +1,9 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: doc +--- +{ + "read user", + "read username", + "review", +} diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__extract_policies.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__extract_policies.snap new file mode 100644 index 0000000000..23e3733041 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__extract_policies.snap @@ -0,0 +1,10 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: doc +--- +{ + "internal", + "profile", + "read user", + "read username", +} diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__filter_basic_query-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__filter_basic_query-2.snap new file mode 100644 index 0000000000..34750f1eca --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__filter_basic_query-2.snap @@ -0,0 +1,10 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: doc +--- +query { + topProducts { + type + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__filter_basic_query-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__filter_basic_query-3.snap new file mode 100644 index 0000000000..d563a70ee8 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__filter_basic_query-3.snap @@ -0,0 +1,23 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: paths +--- +[ + Path( + [ + Key( + "topProducts", + ), + Key( + "internal", + ), + ], + ), + Path( + [ + Key( + "me", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__filter_basic_query-4.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__filter_basic_query-4.snap new file mode 100644 index 0000000000..6753fadea3 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__filter_basic_query-4.snap @@ -0,0 +1,11 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: doc +--- +query { + topProducts { + type + internal + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__filter_basic_query-5.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__filter_basic_query-5.snap new file mode 100644 index 0000000000..9bdc7c6438 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__filter_basic_query-5.snap @@ -0,0 +1,13 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: paths +--- +[ + Path( + [ + Key( + "me", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__filter_basic_query-6.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__filter_basic_query-6.snap new file mode 100644 index 0000000000..a9beb34c39 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__filter_basic_query-6.snap @@ -0,0 +1,14 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: doc +--- +query { + topProducts { + type + internal + } + me { + id + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__filter_basic_query-7.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__filter_basic_query-7.snap new file mode 100644 index 0000000000..ec99f5f6fb --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__filter_basic_query-7.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: paths +--- +[ + Path( + [ + Key( + "me", + ), + Key( + "name", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__filter_basic_query-8.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__filter_basic_query-8.snap new file mode 100644 index 0000000000..1f729768c1 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__filter_basic_query-8.snap @@ -0,0 +1,14 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: doc +--- +query { + topProducts { + type + } + me { + id + name + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__filter_basic_query-9.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__filter_basic_query-9.snap new file mode 100644 index 0000000000..6936b66867 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__filter_basic_query-9.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: paths +--- +[ + Path( + [ + Key( + "topProducts", + ), + Key( + "internal", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__filter_basic_query.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__filter_basic_query.snap new file mode 100644 index 0000000000..23e3733041 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__filter_basic_query.snap @@ -0,0 +1,10 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: doc +--- +{ + "internal", + "profile", + "read user", + "read username", +} diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_fragment-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_fragment-2.snap new file mode 100644 index 0000000000..aac80f5c14 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_fragment-2.snap @@ -0,0 +1,13 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: doc +--- +query { + topProducts { + type + } + itf { + id + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_fragment-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_fragment-3.snap new file mode 100644 index 0000000000..9f89e24746 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_fragment-3.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: paths +--- +[ + Path( + [ + Key( + "itf", + ), + Fragment( + "User", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_fragment-4.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_fragment-4.snap new file mode 100644 index 0000000000..9633b5a7dd --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_fragment-4.snap @@ -0,0 +1,17 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: doc +--- +query { + topProducts { + type + } + itf { + id + ...F + } +} +fragment F on User { + name +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_fragment-5.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_fragment-5.snap new file mode 100644 index 0000000000..8dd6c4970f --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_fragment-5.snap @@ -0,0 +1,5 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: paths +--- +[] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_fragment.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_fragment.snap new file mode 100644 index 0000000000..0ec97e6ebb --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_fragment.snap @@ -0,0 +1,8 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: doc +--- +{ + "read user", + "read username", +} diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_inline_fragment-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_inline_fragment-2.snap new file mode 100644 index 0000000000..aac80f5c14 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_inline_fragment-2.snap @@ -0,0 +1,13 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: doc +--- +query { + topProducts { + type + } + itf { + id + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_inline_fragment-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_inline_fragment-3.snap new file mode 100644 index 0000000000..9f89e24746 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_inline_fragment-3.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: paths +--- +[ + Path( + [ + Key( + "itf", + ), + Fragment( + "User", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_inline_fragment.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_inline_fragment.snap new file mode 100644 index 0000000000..0ec97e6ebb --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_inline_fragment.snap @@ -0,0 +1,8 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: doc +--- +{ + "read user", + "read username", +} diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__mutation-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__mutation-2.snap new file mode 100644 index 0000000000..00f37bb653 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__mutation-2.snap @@ -0,0 +1,5 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: doc +--- + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__mutation-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__mutation-3.snap new file mode 100644 index 0000000000..48fb7f54ed --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__mutation-3.snap @@ -0,0 +1,13 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: paths +--- +[ + Path( + [ + Key( + "ping", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__mutation.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__mutation.snap new file mode 100644 index 0000000000..173e426b01 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__mutation.snap @@ -0,0 +1,9 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: doc +--- +{ + "ping", + "read user", + "read username", +} diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__query_field-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__query_field-2.snap new file mode 100644 index 0000000000..34750f1eca --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__query_field-2.snap @@ -0,0 +1,10 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: doc +--- +query { + topProducts { + type + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__query_field-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__query_field-3.snap new file mode 100644 index 0000000000..9bdc7c6438 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__query_field-3.snap @@ -0,0 +1,13 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: paths +--- +[ + Path( + [ + Key( + "me", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__query_field.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__query_field.snap new file mode 100644 index 0000000000..27ede2615b --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__query_field.snap @@ -0,0 +1,9 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: doc +--- +{ + "profile", + "read user", + "read username", +} diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__scalar-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__scalar-2.snap new file mode 100644 index 0000000000..34750f1eca --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__scalar-2.snap @@ -0,0 +1,10 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: doc +--- +query { + topProducts { + type + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__scalar-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__scalar-3.snap new file mode 100644 index 0000000000..6936b66867 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__scalar-3.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: paths +--- +[ + Path( + [ + Key( + "topProducts", + ), + Key( + "internal", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__scalar.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__scalar.snap new file mode 100644 index 0000000000..49112329fe --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__scalar.snap @@ -0,0 +1,7 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: doc +--- +{ + "internal", +} diff --git a/apollo-router/tests/redis_test.rs b/apollo-router/tests/redis_test.rs index cf075e37db..0802d35d29 100644 --- a/apollo-router/tests/redis_test.rs +++ b/apollo-router/tests/redis_test.rs @@ -22,7 +22,7 @@ mod test { .expect("got redis connection"); connection - .del::<&'static str, ()>("plan.5abb5fecf7df056396fb90fdf38d430b8c1fec55ec132fde878161608af18b76.4c45433039407593557f8a982dafd316a66ec03f0e1ed5fa1b7ef8060d76e8ec.3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112.0368f4b0f505ba3991b3131f834d07baad8bf5c4085585d8520db2c51fd4be9f").await.unwrap(); + .del::<&'static str, ()>("plan.5abb5fecf7df056396fb90fdf38d430b8c1fec55ec132fde878161608af18b76.4c45433039407593557f8a982dafd316a66ec03f0e1ed5fa1b7ef8060d76e8ec.3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112.4f918cb09d5956bea87fe8addb4db3bd16de2cdf935e899cf252cac5528090e4").await.unwrap(); let supergraph = apollo_router::TestHarness::builder() .with_subgraph_network_requests() @@ -52,7 +52,7 @@ mod test { let _ = supergraph.oneshot(request).await?.next_response().await; let s:String = connection - .get("plan.5abb5fecf7df056396fb90fdf38d430b8c1fec55ec132fde878161608af18b76.4c45433039407593557f8a982dafd316a66ec03f0e1ed5fa1b7ef8060d76e8ec.3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112.0368f4b0f505ba3991b3131f834d07baad8bf5c4085585d8520db2c51fd4be9f") + .get("plan.5abb5fecf7df056396fb90fdf38d430b8c1fec55ec132fde878161608af18b76.4c45433039407593557f8a982dafd316a66ec03f0e1ed5fa1b7ef8060d76e8ec.3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112.4f918cb09d5956bea87fe8addb4db3bd16de2cdf935e899cf252cac5528090e4") .await .unwrap(); let query_plan_res: serde_json::Value = serde_json::from_str(&s).unwrap(); From 141b84cd3ef73c2af0d0b5051f400c21e1774f83 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Fri, 11 Aug 2023 12:05:47 +0200 Subject: [PATCH 46/82] add documentation for --- docs/source/configuration/authorization.mdx | 163 +++++++++++++++++++- 1 file changed, 161 insertions(+), 2 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 5db7c3cdc3..6d56287be2 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -16,10 +16,11 @@ APIs provide access to business-critical data. Unrestricted access can result in Enforcing authorization before processing requests is more efficient because it allows for early request termination. It also enhances security by creating an initial checkpoint that can be reinforced in other service layers. -The Apollo Router provides fine-grained access control at your graph's edge. Using the `@authenticated` and `@requiresScopes` directives, you can define access to specific fields and types across your supergraph: +The Apollo Router provides fine-grained access control at your graph's edge. Using the `@authenticated`, `@requiresScopes` and `@policy` directives, you can define access to specific fields and types across your supergraph: - The `@authenticated` directive works in a binary way: authenticated requests can access the specified field or type and unauthenticated requests can't. - The `@requiresScopes` directive allows granular access control through scopes you define. +- The `@policy` directive offloads the authorization policy execution to Rhai or a coprocessor, and integrates the result with Router authorization You define and use these directives on subgraph schemas, and GraphOS [composes](#composition-and-federation) them on to the supergraph schema. The router then enforces these directives on all incoming requests. @@ -87,7 +88,7 @@ The coprocessor would then look up the user with identifier specified in the `su { // Control properties "version": 1, - "stage": "RouterResponse", + "stage": "RouterRequest", "control": "continue", "id": "d0a8245df0efe8aa38a80dba1147fb2e", "context": { @@ -327,6 +328,164 @@ The response would include errors at the `/users/@/email` path since that field If the request includes the `read:others read:emails` scope set, then the router can successfully execute the entire query. +### `@policy` + +The `@policy` directive marks fields and types as restricted based on authorization policies evaluated in Rhaiscript or in coprocessors. +The directive should include a `policies` argument that defines an array of the required policies. + +```graphql +@policy(policies: ["claims[`roles`].contains(`support`)"]) +``` + +The Apollo Router extract from the schema the list of policies relevant to the query, and stores them in the request's context, under the key `apollo_authorization::policies::required` as a map `policy -> null|true|false`. This is done at the [Router service level](../customizations/overview#the-request-lifecycle). A Rhai script or a coprocessor at the Supergraph service level goes through this map, setting the value to `true` if they are successful and `false` if not. After that, the router will filter types and fields for which the policies failed or were not executed. +If the `policies` array contains multiple elements, one of them has to be successful to keep the field. + +Like the efficiencies gained via the `@authenticated` directive, if every field on a particular subgraph query requires policies that fail, this can eliminate entire subgraph requests. + +#### Prerequisites + +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.5", + import: [..., "@policy"]) +``` + +It requires a Supergraph plugin to evaluate the authorization policies. This can be done with Rhaiscript, a coprocessor or a native plugin. + +##### Usage with Rhaiscript + + + +The `policies` argument contains a list of strings, with no format constraints, so we can use them to store Rhai code. As an example, with the following schema, we define policies as boolean expressions that will be evaluated in Rhai: + +```graphql +type Query { + me: User @join__field(graph: ACCOUNTS) @policy(policies: ["claims[`kind`] == `user`"]) +} + +type User + @join__owner(graph: ACCOUNTS) + @join__type(graph: ACCOUNTS, key: "id") { + id: ID! @join__field(graph: ACCOUNTS) + name: String @join__field(graph: ACCOUNTS) + username: String @join__field(graph: ACCOUNTS) @policy(policies: ["claims[`roles`].contains(`support`)"]) +} +``` + +We can then load the following Rhai file: + +``` +fn supergraph_service(service) { + let request_callback = |request| { + let claims = request.context["apollo_authentication::JWT::claims"]; + + let policies = request.context["apollo_authorization::policies::required"]; + + for key in policies.keys() { + let result = eval(key); + policies[key] = result; + } + + request.context["apollo_authorization::policies::required"] = policies; + }; + service.map_request(request_callback); +} +``` + +For each policy, it will evaluate it and store the result in the `apollo_authorization::policies::required` map. + + + +##### Usage with a coprocessor + +A [coprocessor](../customizations/coprocessor) called at the Supergraph request stage can receive the list of policies and execute them. It is useful to bridge the Router authorization with an existing authorization stack, or link policy execution with lookups in a database. + + + +If the router is configured with: + +```yaml title="router.yaml" +coprocessor: + url: http://127.0.0.1:8081 + supergraph: + request: + context: true +``` + +And a schema like this: + +```graphql +type Query { + me: User @join__field(graph: ACCOUNTS) @policy(policies: ["read_profile"]) +} + +type User + @join__owner(graph: ACCOUNTS) + @join__type(graph: ACCOUNTS, key: "id") { + id: ID! @join__field(graph: ACCOUNTS) + name: String @join__field(graph: ACCOUNTS) + credit_card: String @join__field(graph: ACCOUNTS) @policy(policies: ["read_credit_card"]) +} +``` + +The 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_address": 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 +{ + // Control properties + "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_address": false + } + } + } +} +``` + + + +#### Example `@policy` use case + +TODO + ## Composition and federation Authorization directives are defined at the subgraph level and the GraphOS composes them in the supergraph schema. In other words, if subgraph fields or types include either `@authenticated` or `@requiresScopes` directives, they are set on the supergraph too. From c36fca41d5d9e0a7e5a164aab0be03caa9d4328c Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Fri, 11 Aug 2023 15:34:34 +0200 Subject: [PATCH 47/82] return a proper GraphQL response with authorization errors If the query is empty after filtering, instead of returning a query planning error, we return an empty GraphQL response with errors pointing at the removed paths --- apollo-router/src/error.rs | 3 ++ .../src/plugins/authorization/mod.rs | 45 ++++--------------- .../src/query_planner/bridge_query_planner.rs | 25 ++++++++++- 3 files changed, 36 insertions(+), 37 deletions(-) diff --git a/apollo-router/src/error.rs b/apollo-router/src/error.rs index c3521a3561..fe844c04ee 100644 --- a/apollo-router/src/error.rs +++ b/apollo-router/src/error.rs @@ -293,6 +293,9 @@ pub(crate) enum QueryPlannerError { /// complexity limit exceeded LimitExceeded(OperationLimits), + + /// Unauthorized field or type + Unauthorized(Vec), } impl IntoGraphQLErrors for QueryPlannerError { diff --git a/apollo-router/src/plugins/authorization/mod.rs b/apollo-router/src/plugins/authorization/mod.rs index 32ce750356..22952d7fec 100644 --- a/apollo-router/src/plugins/authorization/mod.rs +++ b/apollo-router/src/plugins/authorization/mod.rs @@ -230,22 +230,13 @@ impl AuthorizationPlugin { let compiler = match filter_res { None => compiler, Some((query, paths)) => { + unauthorized_paths.extend(paths.into_iter()); + if query.is_empty() { - return Err(QueryPlannerError::PlanningErrors(PlanErrors { - errors: Arc::new(vec![router_bridge::planner::PlanError { - message: Some("Unauthorized query".to_string()), - extensions: None, - validation_error: false, - }]), - usage_reporting: UsageReporting { - stats_report_key: GRAPHQL_VALIDATION_FAILURE_ERROR_KEY.to_string(), - referenced_fields_by_type: Default::default(), - }, - })); + return Err(QueryPlannerError::Unauthorized(unauthorized_paths)); } is_filtered = true; - unauthorized_paths.extend(paths.into_iter()); let mut compiler = ApolloCompiler::new(); compiler.set_type_system_hir(schema.type_system.clone()); @@ -259,22 +250,13 @@ impl AuthorizationPlugin { let compiler = match filter_res { None => compiler, Some((query, paths)) => { + unauthorized_paths.extend(paths.into_iter()); + if query.is_empty() { - return Err(QueryPlannerError::PlanningErrors(PlanErrors { - errors: Arc::new(vec![router_bridge::planner::PlanError { - message: Some("Unauthorized query".to_string()), - extensions: None, - validation_error: false, - }]), - usage_reporting: UsageReporting { - stats_report_key: GRAPHQL_VALIDATION_FAILURE_ERROR_KEY.to_string(), - referenced_fields_by_type: Default::default(), - }, - })); + return Err(QueryPlannerError::Unauthorized(unauthorized_paths)); } is_filtered = true; - unauthorized_paths.extend(paths.into_iter()); let mut compiler = ApolloCompiler::new(); compiler.set_type_system_hir(schema.type_system.clone()); @@ -288,22 +270,13 @@ impl AuthorizationPlugin { let compiler = match filter_res { None => compiler, Some((query, paths)) => { + unauthorized_paths.extend(paths.into_iter()); + if query.is_empty() { - return Err(QueryPlannerError::PlanningErrors(PlanErrors { - errors: Arc::new(vec![router_bridge::planner::PlanError { - message: Some("Unauthorized query".to_string()), - extensions: None, - validation_error: false, - }]), - usage_reporting: UsageReporting { - stats_report_key: GRAPHQL_VALIDATION_FAILURE_ERROR_KEY.to_string(), - referenced_fields_by_type: Default::default(), - }, - })); + return Err(QueryPlannerError::Unauthorized(unauthorized_paths)); } is_filtered = true; - unauthorized_paths.extend(paths.into_iter()); let mut compiler = ApolloCompiler::new(); compiler.set_type_system_hir(schema.type_system.clone()); diff --git a/apollo-router/src/query_planner/bridge_query_planner.rs b/apollo-router/src/query_planner/bridge_query_planner.rs index bcb997a374..e98c540463 100644 --- a/apollo-router/src/query_planner/bridge_query_planner.rs +++ b/apollo-router/src/query_planner/bridge_query_planner.rs @@ -26,6 +26,7 @@ use crate::error::QueryPlannerError; use crate::error::ServiceBuildError; use crate::graphql; use crate::introspection::Introspection; +use crate::json_ext::Object; use crate::json_ext::Path; use crate::plugins::authorization::AuthorizationPlugin; use crate::plugins::authorization::CacheKeyMetadata; @@ -460,7 +461,29 @@ impl BridgeQueryPlanner { mut compiler: Arc>, ) -> Result { let filter_res = if self.enable_authorization_directives { - AuthorizationPlugin::filter_query(&key, &self.schema)? + match AuthorizationPlugin::filter_query(&key, &self.schema) { + Err(QueryPlannerError::Unauthorized(unauthorized_paths)) => { + let response = graphql::Response::builder() + .data(Object::new()) + .errors( + unauthorized_paths + .into_iter() + .map(|path| { + graphql::Error::builder() + .message("Unauthorized field or type") + .path(path.clone()) + .extension_code("UNAUTHORIZED_FIELD_OR_TYPE") + .build() + }) + .collect(), + ) + .build(); + return Ok(QueryPlannerContent::Introspection { + response: Box::new(response), + }); + } + other => other?, + } } else { None }; From 1ccb412995e84e4f8b01a1c75550f94894e6e879 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Fri, 11 Aug 2023 15:46:49 +0200 Subject: [PATCH 48/82] lint --- apollo-router/src/plugins/authorization/mod.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/apollo-router/src/plugins/authorization/mod.rs b/apollo-router/src/plugins/authorization/mod.rs index 22952d7fec..ab2f802520 100644 --- a/apollo-router/src/plugins/authorization/mod.rs +++ b/apollo-router/src/plugins/authorization/mod.rs @@ -8,7 +8,6 @@ use std::sync::Arc; use apollo_compiler::ApolloCompiler; use apollo_compiler::InputDatabase; use http::StatusCode; -use router_bridge::planner::UsageReporting; use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; @@ -26,7 +25,6 @@ use self::policy::POLICY_DIRECTIVE_NAME; use self::scopes::ScopeExtractionVisitor; use self::scopes::ScopeFilteringVisitor; use self::scopes::REQUIRES_SCOPES_DIRECTIVE_NAME; -use crate::error::PlanErrors; use crate::error::QueryPlannerError; use crate::error::SchemaError; use crate::error::ServiceBuildError; @@ -46,7 +44,6 @@ use crate::spec::query::QUERY_EXECUTABLE; use crate::spec::Query; use crate::spec::Schema; use crate::spec::SpecError; -use crate::spec::GRAPHQL_VALIDATION_FAILURE_ERROR_KEY; use crate::Configuration; use crate::Context; From 542fbfbe982c0b0224c674827878d4b773569080 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Fri, 11 Aug 2023 16:05:51 +0200 Subject: [PATCH 49/82] lint --- apollo-router/src/query_planner/bridge_query_planner.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apollo-router/src/query_planner/bridge_query_planner.rs b/apollo-router/src/query_planner/bridge_query_planner.rs index e98c540463..3809cf06d4 100644 --- a/apollo-router/src/query_planner/bridge_query_planner.rs +++ b/apollo-router/src/query_planner/bridge_query_planner.rs @@ -471,7 +471,7 @@ impl BridgeQueryPlanner { .map(|path| { graphql::Error::builder() .message("Unauthorized field or type") - .path(path.clone()) + .path(path) .extension_code("UNAUTHORIZED_FIELD_OR_TYPE") .build() }) From 2a452907d3851977469c9e5a245394dc7f5e6504 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Fri, 11 Aug 2023 16:32:17 +0200 Subject: [PATCH 50/82] implement OR for the policy directive the policies argument is an array of policy strings. If any of them succeeds, then the field is authorized --- .../src/plugins/authorization/policy.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/apollo-router/src/plugins/authorization/policy.rs b/apollo-router/src/plugins/authorization/policy.rs index 30b779709c..d85df9c390 100644 --- a/apollo-router/src/plugins/authorization/policy.rs +++ b/apollo-router/src/plugins/authorization/policy.rs @@ -163,7 +163,15 @@ impl<'a> PolicyFilteringVisitor<'a> { let field_policies = policy_argument(field.directive_by_name(POLICY_DIRECTIVE_NAME)) .cloned() .collect::>(); - if !self.request_policies.is_superset(&field_policies) { + + // The field is authorized if any of the policies succeeds + if !field_policies.is_empty() + && self + .request_policies + .intersection(&field_policies) + .next() + .is_none() + { return false; } @@ -178,7 +186,13 @@ impl<'a> PolicyFilteringVisitor<'a> { let type_policies = policy_argument(ty.directive_by_name(POLICY_DIRECTIVE_NAME)) .cloned() .collect::>(); - self.request_policies.is_superset(&type_policies) + // The field is authorized if any of the policies succeeds + type_policies.is_empty() + || self + .request_policies + .intersection(&type_policies) + .next() + .is_some() } } From ed1b8d4d4baee9add4616c4e35b28bf7c26e077f Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Fri, 11 Aug 2023 17:12:57 +0200 Subject: [PATCH 51/82] implement OR for the requiresScopes directive `@requiresScopes` now takes as argument an array of arrays: ``` @requiresScopes(scopes: [ ["X", "Y"], ["Z"] ]) ``` This indicates that to authorize the fields, the request must either: - contain both the `X` and `Y` scopes - contain the `Z` scope --- .../src/plugins/authorization/scopes.rs | 98 +++++++++++++++---- ...horization__tests__scopes_directive-4.snap | 29 ++++++ .../src/plugins/authorization/tests.rs | 37 ++++++- 3 files changed, 142 insertions(+), 22 deletions(-) create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__tests__scopes_directive-4.snap diff --git a/apollo-router/src/plugins/authorization/scopes.rs b/apollo-router/src/plugins/authorization/scopes.rs index 29f0afc317..d34d6dd95f 100644 --- a/apollo-router/src/plugins/authorization/scopes.rs +++ b/apollo-router/src/plugins/authorization/scopes.rs @@ -1,5 +1,10 @@ //! Authorization plugin - +//! +//! Implementation of the `@policy` directive: +//! +//! ```graphql +//! directive @requiresScopes(scopes: [[String!]!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM +//! ``` use std::collections::HashSet; use apollo_compiler::hir; @@ -53,12 +58,20 @@ impl<'a> ScopeExtractionVisitor<'a> { fn scopes_argument(opt_directive: Option<&hir::Directive>) -> impl Iterator { opt_directive .and_then(|directive| directive.argument_by_name("scopes")) + // outer array .and_then(|value| match value { Value::List { value, .. } => Some(value), _ => None, }) .into_iter() .flatten() + // inner array + .filter_map(|value| match value { + Value::List { value, .. } => Some(value), + _ => None, + }) + .into_iter() + .flatten() .filter_map(|v| match v { Value::String { value, .. } => Some(value), _ => None, @@ -135,6 +148,32 @@ impl<'a> traverse::Visitor for ScopeExtractionVisitor<'a> { } } +fn scopes_sets_argument(directive: &hir::Directive) -> impl Iterator> + '_ { + directive + .argument_by_name("scopes") + // outer array + .and_then(|value| match value { + Value::List { value, .. } => Some(value), + _ => None, + }) + .into_iter() + .flatten() + // inner array + .filter_map(|value| match value { + Value::List { value, .. } => Some( + value + .into_iter() + .filter_map(|v| match v { + Value::String { value, .. } => Some(value), + _ => None, + }) + .cloned() + .collect(), + ), + _ => None, + }) +} + pub(crate) struct ScopeFilteringVisitor<'a> { compiler: &'a ApolloCompiler, file_id: FileId, @@ -161,12 +200,21 @@ impl<'a> ScopeFilteringVisitor<'a> { } fn is_field_authorized(&mut self, field: &FieldDefinition) -> bool { - let field_scopes = scopes_argument(field.directive_by_name(REQUIRES_SCOPES_DIRECTIVE_NAME)) - .cloned() - .collect::>(); - - if !self.request_scopes.is_superset(&field_scopes) { - return false; + if let Some(directive) = field.directive_by_name(REQUIRES_SCOPES_DIRECTIVE_NAME) { + let mut field_scopes_sets = scopes_sets_argument(directive); + + // The outer array acts like a logical OR: if any of the inner arrays of scopes matches, the field + // is authorized. + // On an empty set, all returns true, so we must check that case separately + let mut empty = true; + if field_scopes_sets.all(|scopes_set| { + empty = false; + !self.request_scopes.is_superset(&scopes_set) + }) { + if !empty { + return false; + } + } } if let Some(ty) = field.ty().type_def(&self.compiler.db) { @@ -177,11 +225,23 @@ impl<'a> ScopeFilteringVisitor<'a> { } fn is_type_authorized(&self, ty: &TypeDefinition) -> bool { - let type_scopes = scopes_argument(ty.directive_by_name(REQUIRES_SCOPES_DIRECTIVE_NAME)) - .cloned() - .collect::>(); - - self.request_scopes.is_superset(&type_scopes) + match ty.directive_by_name(REQUIRES_SCOPES_DIRECTIVE_NAME) { + None => true, + Some(directive) => { + let mut type_scopes_sets = scopes_sets_argument(directive); + + // The outer array acts like a logical OR: if any of the inner arrays of scopes matches, the field + // is authorized. + // On an empty set, any returns false, so we must check that case separately + let mut empty = true; + let res = type_scopes_sets.any(|scopes_set| { + empty = false; + self.request_scopes.is_superset(&scopes_set) + }); + + empty || res + } + } } } @@ -347,17 +407,17 @@ mod tests { use crate::spec::query::traverse; static BASIC_SCHEMA: &str = r#" - directive @requiresScopes(scopes: [String]) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM + directive @requiresScopes(scopes: [[String!]!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM type Query { topProducts: Product customer: User - me: User @requiresScopes(scopes: ["profile"]) + me: User @requiresScopes(scopes: [["profile"]]) itf: I } type Mutation { - ping: User @requiresScopes(scopes: ["ping"]) + ping: User @requiresScopes(scopes: [["ping"]]) } interface I { @@ -372,16 +432,16 @@ mod tests { publicReviews: [Review] } - scalar Internal @requiresScopes(scopes: ["internal", "test"]) @specifiedBy(url: "http///example.com/test") + scalar Internal @requiresScopes(scopes: [["internal", "test"]]) @specifiedBy(url: "http///example.com/test") - type Review @requiresScopes(scopes: ["review"]) { + type Review @requiresScopes(scopes: [["review"]]) { body: String author: User } - type User implements I @requiresScopes(scopes: ["read:user"]) { + type User implements I @requiresScopes(scopes: [["read:user"]]) { id: ID - name: String @requiresScopes(scopes: ["read:username"]) + name: String @requiresScopes(scopes: [["read:username"]]) } "#; diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__tests__scopes_directive-4.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__tests__scopes_directive-4.snap new file mode 100644 index 0000000000..0b4b8966eb --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__tests__scopes_directive-4.snap @@ -0,0 +1,29 @@ +--- +source: apollo-router/src/plugins/authorization/tests.rs +expression: response +--- +{ + "data": { + "orga": { + "id": 1, + "creatorUser": { + "id": 0, + "name": "Ada", + "phone": null + } + } + }, + "errors": [ + { + "message": "Unauthorized field or type", + "path": [ + "orga", + "creatorUser", + "phone" + ], + "extensions": { + "code": "UNAUTHORIZED_FIELD_OR_TYPE" + } + } + ] +} diff --git a/apollo-router/src/plugins/authorization/tests.rs b/apollo-router/src/plugins/authorization/tests.rs index ce3d5bc01a..161203202c 100644 --- a/apollo-router/src/plugins/authorization/tests.rs +++ b/apollo-router/src/plugins/authorization/tests.rs @@ -360,7 +360,7 @@ directive @join__owner(graph: join__Graph!) on OBJECT | INTERFACE directive @join__graph(name: String!, url: String!) on ENUM_VALUE directive @inaccessible on OBJECT | FIELD_DEFINITION | INTERFACE | UNION -directive @requiresScopes(scopes: [String]) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM +directive @requiresScopes(scopes: [[String!]!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM scalar join__FieldSet enum join__Graph { @@ -375,10 +375,10 @@ type User @join__owner(graph: USER) @join__type(graph: ORGA, key: "id") @join__type(graph: USER, key: "id") -@requiresScopes(scopes: ["user:read"]) { +@requiresScopes(scopes: [["user:read"], ["admin"]]) { id: ID! name: String - phone: String @requiresScopes(scopes: ["pii"]) + phone: String @requiresScopes(scopes: [["pii"]]) activeOrganization: Organization } type Organization @@ -508,6 +508,37 @@ async fn scopes_directive() { .unwrap(), }; + let response = service + .clone() + .oneshot(request) + .await + .unwrap() + .into_graphql_response_stream() + .await + .next() + .await + .unwrap() + .unwrap(); + + insta::assert_json_snapshot!(response); + + let context = Context::new(); + context + .insert( + "apollo_authentication::JWT::claims", + json! {{ "scope": "admin" }}, + ) + .unwrap(); + let request = router::Request { + context, + router_request: http::Request::builder() + .method("POST") + .header(CONTENT_TYPE, "application/json") + .header(ACCEPT, "application/json") + .body(serde_json::to_vec(&req).unwrap().into()) + .unwrap(), + }; + let response = service .oneshot(request) .await From 9a5f0de387eb6b81c84065715af3314bd9e120b5 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Fri, 11 Aug 2023 17:29:06 +0200 Subject: [PATCH 52/82] WiP: Rhai script to edit the claims this cannot work yet because router service level scripts don't work yet --- docs/source/configuration/authorization.mdx | 29 +++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 6d56287be2..af921214db 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -328,6 +328,35 @@ The response would include errors at the `/users/@/email` path since that field If the request includes the `read:others read:emails` scope set, then the router can successfully execute the entire query. +#### Scopes claims in other formats + +If the token holds scopes in another format (ex: an array of strings) or in another claim, it is possible to edit the claims with a Rhai script while the request is going through the router: + +``` +fn router_service(service) { + let request_callback = |request| { + let claims = request.context["apollo_authentication::JWT::claims"]; + let roles = claims["roles"]; + + let scope = ""; + if roles.len() > 1 { + scope = roles[0]; + } + + if roles.len() > 2 { + for role in roles[1..] { + scope += ' '; + scope += role; + } + } + + claims["scope"] = scope; + request.context["apollo_authentication::JWT::claims"] = claims; + }; + service.map_request(request_callback); +} +``` + ### `@policy` The `@policy` directive marks fields and types as restricted based on authorization policies evaluated in Rhaiscript or in coprocessors. From 77f536816ad490d148b16a357d281de443283ab3 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 11 Aug 2023 13:21:26 -0600 Subject: [PATCH 53/82] Use content components --- docs/source/configuration/authorization.mdx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index af921214db..7ebfb24f0e 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -3,18 +3,13 @@ title: Authorization in the Apollo Router description: Strengthen your supergraph's security with advanced access controls minVersion: 1.27.0 --- + -
- -**This feature is available only with a [GraphOS Enterprise plan](/graphos/enterprise/).** It is currently in [preview](/resources/product-launch-stages#preview). - -If your organization _doesn't_ currently have an Enterprise plan, you can test this functionality by signing up for a free [Enterprise trial](/graphos/org/plans/#enterprise-trials). - -
+ APIs provide access to business-critical data. Unrestricted access can result in data breaches, monetary losses, or potential denial of service. Even for internal APIs, checks can be essential to limit data to authorized parties. -Enforcing authorization before processing requests is more efficient because it allows for early request termination. It also enhances security by creating an initial checkpoint that can be reinforced in other service layers. +Enforcing authorization before processing requests is efficient because it allows for early request termination. It also enhances security by creating an initial checkpoint that can be reinforced in other service layers. The Apollo Router provides fine-grained access control at your graph's edge. Using the `@authenticated`, `@requiresScopes` and `@policy` directives, you can define access to specific fields and types across your supergraph: From 3459afb479161c123b5d1fe29f5cb4cc74936c59 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 11 Aug 2023 14:29:36 -0600 Subject: [PATCH 54/82] Copy edits --- docs/source/configuration/authorization.mdx | 174 ++++++++++---------- 1 file changed, 85 insertions(+), 89 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 7ebfb24f0e..0656c84d37 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -15,7 +15,7 @@ The Apollo Router provides fine-grained access control at your graph's edge. Usi - The `@authenticated` directive works in a binary way: authenticated requests can access the specified field or type and unauthenticated requests can't. - The `@requiresScopes` directive allows granular access control through scopes you define. -- The `@policy` directive offloads the authorization policy execution to Rhai or a coprocessor, and integrates the result with Router authorization +- The `@policy` directive offloads authorization policy execution to a [Rhai script](../customizations/rhai/) or a [coprocessor](../customizations/coprocessor) and integrates the result in the router. You define and use these directives on subgraph schemas, and GraphOS [composes](#composition-and-federation) them on to the supergraph schema. The router then enforces these directives on all incoming requests. @@ -31,13 +31,10 @@ If you configure [JWT authentication](./authn-jwt), the Apollo Router automatica To-do: More information on how to use JWT auth with these directives - ### Claim augmentation via coprocessors Tokens can come in with limited information, that is then used to look up user specific information like roles. This can be done with [coprocessors](/customizations/coprocessor). - - The router level coprocessor is guaranteed to be called after the authentication plugin, so the coprocessor can receive the list of claims extracted from the token, use information like the `sub` (subject) claim to look up the user, insert its data in the claims list and return it to the router. If the router is configured with: @@ -98,8 +95,6 @@ The coprocessor would then look up the user with identifier specified in the `su } ``` - - ## Authorization directives ### `@authenticated` @@ -108,10 +103,10 @@ The `@authenticated` directive marks specific fields and types as requiring auth It works by checking for the `apollo_authentication::JWT::claims` key in a request's context. If the request is authenticated, the router executes the query in its entirety. -For unauthenticated requests, the router removes `@authenticated` fields before planning the query and only executes the parts of the query that don't require authentication. -If every field in a particular subgraph's query is marked as requiring authentication, this can eliminate entire subgraph requests. +If the request is unauthenticated, the router removes `@authenticated` fields before planning the query and only executes the parts of the query that don't require authentication. +If every field in a particular subgraph's query is marked as requiring authentication, this can eliminate entire subgraph requests, thereby increasing router efficiency. -#### Prerequisites +#### Usage To use the `@authenticated` directive in a subgraph you can [import it from the `@link` directive](/federation/federated-types/federated-directives/#importing-directives) like so: @@ -124,9 +119,9 @@ extend schema #### Example `@authenticated` use case -Suppose you are building a social media platform. Unauthenticated users can view all other parts of a public post—its title, author, etc. -However, you only want authenticated users to be able to see a post's number of views. -You also want to be able to query for an authenticated user's information. +Suppose you're building a social media platform. Unauthenticated users can view a public post's title, author, and content. +However, you only want authenticated users to be able to see the number of views a post has received. +You also need to be able to query for an authenticated user's information. Your schema may look something like this: @@ -138,7 +133,7 @@ type Query { type User { id: ID! - name: String + username: String posts: [Post!]! } @@ -157,7 +152,7 @@ Consider the following query: ```graphql title="Sample query" query { me { - name + username } post(id: "1234") { title @@ -166,7 +161,7 @@ query { } ``` -An authenticated request would execute the entire query. +The router would executed the entire request if it's authenticated. For an unauthenticated request, the router would remove the `@authenticated` fields and execute the filtered query. @@ -174,7 +169,7 @@ For an unauthenticated request, the router would remove the `@authenticated` fie ```graphql title="Query executed for an authenticated request" query { me { - name + username } post(id: "1234") { title @@ -233,26 +228,59 @@ If _every_ requested field requires authentication and a request is unauthentica ### `@requiresScopes` The `@requiresScopes` directive marks fields and types as restricted based on required scopes. +Depending on the scopes present on the request, the router filters out unauthorized fields and types. The directive should include a `scopes` argument that defines an array of the required scopes. ```graphql @requiresScopes(scopes: ["scope1", "scope2", "scope3"]) ``` +> If a field's required `scopes` array includes multiple scopes, the request must include _all_ required scopes to resolve it. + The directive validates the required scopes by loading the claims object at the `apollo_authentication::JWT::claims` key in a request's context. The claims object's `scope` key's value should be a space separated string of scopes in the format defined by the [OAuth2 RFC for access token scopes](https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). -``` +```rhai claims = context["apollo_authentication::JWT::claims"] claims["scope"] = "scope1 scope2 scope3" ``` -Depending on the scopes present on the request, the router filters out unauthorized fields and types. -If a field's required `scopes` array includes multiple scopes, the request must include _all_ required scopes to resolve it. + + +If the `apollo_authentication::JWT::claims` token holds scopes in another format, for example, an array of strings, or in another claim, you can edit the claims with a [custom Rhai script](../customizations/rhai). + +The example below extracts an array of scopes from the `roles` claim and reformats them as a space separated string. + +```Rhai +fn router_service(service) { + let request_callback = |request| { + let claims = request.context["apollo_authentication::JWT::claims"]; + let roles = claims["roles"]; + + let scope = ""; + if roles.len() > 1 { + scope = roles[0]; + } + + if roles.len() > 2 { + for role in roles[1..] { + scope += ' '; + scope += role; + } + } + + claims["scope"] = scope; + request.context["apollo_authentication::JWT::claims"] = claims; + }; + service.map_request(request_callback); +} +``` + + Like the efficiencies gained via the `@authenticated` directive, if every field on a particular subgraph query requires scopes that aren't present, this can eliminate entire subgraph requests. -#### Prerequisites +#### Usage To use the `@requiresScopes` directive in a subgraph you can [import it from the `@link` directive](/federation/federated-types/federated-directives/#importing-directives) like so: @@ -265,7 +293,7 @@ extend schema #### Example `@requiresScopes` use case -Imagine your social media platform lets users view other users' information only if they have the required permissions. +Imagine the social media platform you're building lets users view other users' information only if they have the required permissions. Your schema may look something like this: ```graphql title="" @@ -273,11 +301,12 @@ type Query { me: User @authenticated user(id: ID!): User @requiresScopes(scopes: ["read:others"]) users: [User!]! @requiresScopes(scopes: ["read:others"]) + post(id: ID!): Post } type User { id: ID! - name: String + username: String email: String @requiresScopes(scopes: ["read:email"]) profileImage: String posts: [Post!]! @@ -301,7 +330,7 @@ If the request includes only the `read:others` scope, then the router would exec ```graphql title="Raw query to router" query { users { - name + username profileImage email } @@ -311,7 +340,7 @@ query { ```graphql title="Scopes: 'read:others'" query { users { - name + username profileImage } } @@ -319,54 +348,25 @@ query { -The response would include errors at the `/users/@/email` path since that field requires the `read:emails` scope. +The response would include an errors at the `/users/@/email` path since that field requires the `read:emails` scope. If the request includes the `read:others read:emails` scope set, then the router can successfully execute the entire query. -#### Scopes claims in other formats - -If the token holds scopes in another format (ex: an array of strings) or in another claim, it is possible to edit the claims with a Rhai script while the request is going through the router: - -``` -fn router_service(service) { - let request_callback = |request| { - let claims = request.context["apollo_authentication::JWT::claims"]; - let roles = claims["roles"]; - - let scope = ""; - if roles.len() > 1 { - scope = roles[0]; - } - - if roles.len() > 2 { - for role in roles[1..] { - scope += ' '; - scope += role; - } - } - - claims["scope"] = scope; - request.context["apollo_authentication::JWT::claims"] = claims; - }; - service.map_request(request_callback); -} -``` - ### `@policy` -The `@policy` directive marks fields and types as restricted based on authorization policies evaluated in Rhaiscript or in coprocessors. -The directive should include a `policies` argument that defines an array of the required policies. +The `@policy` directive marks fields and types as restricted based on authorization policies evaluated in a [Rhai script](../customizations/rhai/) or[coprocessor](../customizations/coprocessor). +The directive should include a `policies` argument that defines an array of the required policies. The example below includes one policy that requires all roles from the claims object to include the string `"support"`. ```graphql @policy(policies: ["claims[`roles`].contains(`support`)"]) ``` -The Apollo Router extract from the schema the list of policies relevant to the query, and stores them in the request's context, under the key `apollo_authorization::policies::required` as a map `policy -> null|true|false`. This is done at the [Router service level](../customizations/overview#the-request-lifecycle). A Rhai script or a coprocessor at the Supergraph service level goes through this map, setting the value to `true` if they are successful and `false` if not. After that, the router will filter types and fields for which the policies failed or were not executed. -If the `policies` array contains multiple elements, one of them has to be successful to keep the field. +The Apollo Router extracts the list of policies relevant to a request from the schema. It then stores them in the request's context, under the key `apollo_authorization::policies::required` as a map `policy -> null|true|false`. This happens at the [Router service level](../customizations/overview#the-request-lifecycle). A Rhai script or a coprocessor at the Supergraph service level goes through this map, setting the value to `true` if they are successful and `false` if not. After that, the router filters the requests' types and fields to only those where the policy is `true`. +If the `policies` array contains multiple elements, only one of them has to be successful for the policy to be `true`. -Like the efficiencies gained via the `@authenticated` directive, if every field on a particular subgraph query requires policies that fail, this can eliminate entire subgraph requests. +Like the efficiencies gained via the authorization directives, if every field on a particular subgraph query is unauthorized because none of its policies pass, this can eliminate entire subgraph requests. -#### Prerequisites +#### 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: @@ -377,13 +377,13 @@ extend schema import: [..., "@policy"]) ``` -It requires a Supergraph plugin to evaluate the authorization policies. This can be done with Rhaiscript, a coprocessor or a native plugin. +Using the `@policy` directive requires a [Supergraph plugin](../customizations/overview) to evaluate the authorization policies. You can do this with a a [Rhai script](../customizations/rhai/), [coprocessor](../customizations/coprocessor), or [native plugin](/customizations/native). Refer to the following Rhai script and coprocessor examples for more information. -##### Usage with Rhaiscript +##### Usage with a Rhai script -The `policies` argument contains a list of strings, with no format constraints, so we can use them to store Rhai code. As an example, with the following schema, we define policies as boolean expressions that will be evaluated in Rhai: +The `policies` argument contains a list of strings, with no formatting constraints, so you can use them to store Rhai code. As an example,the following schema defines policies as boolean expressions that will be evaluated in Rhai: ```graphql type Query { @@ -394,12 +394,12 @@ type User @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") { id: ID! @join__field(graph: ACCOUNTS) - name: String @join__field(graph: ACCOUNTS) + username: String @join__field(graph: ACCOUNTS) username: String @join__field(graph: ACCOUNTS) @policy(policies: ["claims[`roles`].contains(`support`)"]) } ``` -We can then load the following Rhai file: +You can then use the following Rhai script to evaluate the policies: ``` fn supergraph_service(service) { @@ -419,27 +419,17 @@ fn supergraph_service(service) { } ``` -For each policy, it will evaluate it and store the result in the `apollo_authorization::policies::required` map. +The script uses the [`eval` function](https://rhai.rs/book/ref/eval.html) to evaluate each policy and store the result in the `apollo_authorization::policies::required` map. ##### Usage with a coprocessor -A [coprocessor](../customizations/coprocessor) called at the Supergraph request stage can receive the list of policies and execute them. It is useful to bridge the Router authorization with an existing authorization stack, or link policy execution with lookups in a database. +You can use a [coprocessor](../customizations/coprocessor) called at the Supergraph request stage to receive the list of policies and execute them. This is useful to bridge the router authorization with an existing authorization stack, or link policy execution with lookups in a database. -If the router is configured with: - -```yaml title="router.yaml" -coprocessor: - url: http://127.0.0.1:8081 - supergraph: - request: - context: true -``` - -And a schema like this: +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 { @@ -450,12 +440,22 @@ type User @join__owner(graph: ACCOUNTS) @join__type(graph: ACCOUNTS, key: "id") { id: ID! @join__field(graph: ACCOUNTS) - name: String @join__field(graph: ACCOUNTS) + username: String @join__field(graph: ACCOUNTS) credit_card: String @join__field(graph: ACCOUNTS) @policy(policies: ["read_credit_card"]) } ``` -The coprocessor can then receive a request with this format: +If you configure your router like this: + +```yaml title="router.yaml" +coprocessor: + url: http://127.0.0.1:8081 + supergraph: + request: + context: true +``` + +then a coprocessor can then receive a request with this format: ```json { @@ -506,13 +506,9 @@ A user can read their own profile, so `read_profile` will succeed. But only the -#### Example `@policy` use case - -TODO - ## Composition and federation -Authorization directives are defined at the subgraph level and the GraphOS composes them in the supergraph schema. In other words, if subgraph fields or types include either `@authenticated` or `@requiresScopes` directives, they are set on the supergraph too. +Authorization directives are defined at the subgraph level and the GraphOS composes them in the supergraph schema. In other words, if subgraph fields or types include `@authenticated`, `@requiresScopes`, or `@policy` directives, they are set on the supergraph too. If multiple shared subgraph fields include `@requiresScopes`, the supergraph schema merges the sets of scopes required by each subgraph. For example, imagine one subgraph requires the `read:others` scope on the `users` query: @@ -552,7 +548,7 @@ type Query { type Product @key(fields: "id") { id: ID! @authenticated - name: String! + username: String! price: Int @authenticated } ``` @@ -573,7 +569,7 @@ An unauthenticated request would successfully execute this query: ```graphql query { product { - name + username inStock } } @@ -587,7 +583,7 @@ For the following query, an unauthenticated request would resolve `null` for `id query { product { id - name + username } } ``` @@ -607,7 +603,7 @@ type Query { type User { id: ID! - name: String + username: String posts: [Post!]! } From cb2b46ff3665c42e2a90867bda15dbefd102ebfd Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 11 Aug 2023 16:11:06 -0600 Subject: [PATCH 55/82] Copy edit --- docs/source/configuration/authorization.mdx | 93 ++++++++++++--------- 1 file changed, 52 insertions(+), 41 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 0656c84d37..6ff5e7f908 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -3,41 +3,45 @@ title: Authorization in the Apollo Router description: Strengthen your supergraph's security with advanced access controls minVersion: 1.27.0 --- + APIs provide access to business-critical data. Unrestricted access can result in data breaches, monetary losses, or potential denial of service. Even for internal APIs, checks can be essential to limit data to authorized parties. -Enforcing authorization before processing requests is efficient because it allows for early request termination. It also enhances security by creating an initial checkpoint that can be reinforced in other service layers. +Enforcing authorization _before_ processing requests is efficient because it allows for early request termination. It also enhances security by creating an initial checkpoint that can be reinforced in other service layers. -The Apollo Router provides fine-grained access control at your graph's edge. Using the `@authenticated`, `@requiresScopes` and `@policy` directives, you can define access to specific fields and types across your supergraph: +The Apollo Router provides fine-grained access controls at your graph's edge. Using the `@authenticated`, `@requiresScopes`, and `@policy` directives, you can define access to specific fields and types across your supergraph: -- The `@authenticated` directive works in a binary way: authenticated requests can access the specified field or type and unauthenticated requests can't. -- The `@requiresScopes` directive allows granular access control through scopes you define. -- The `@policy` directive offloads authorization policy execution to a [Rhai script](../customizations/rhai/) or a [coprocessor](../customizations/coprocessor) and integrates the result in the router. +- The [`@authenticated`](#authenticated) directive works in a binary way: authenticated requests can access the specified field or type, and unauthenticated requests can't. +- The [`@requiresScopes`](#requiresscopes) directive allows granular access control through the scopes you define. +- 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. -You define and use these directives on subgraph schemas, and GraphOS [composes](#composition-and-federation) them on to the supergraph schema. +You define and use these directives on subgraph schemas, and GraphOS [composes](#composition-and-federation) them onto the supergraph schema. The router then enforces these directives on all incoming requests. ## Prerequisites -To use the router's authorization directives, you need to either configure [JWT authentication](./authn-jwt) or add a [router service coprocessor](../customizations/coprocessor) that adds **claims** to a request's context. -Claims are the individual details of a request's authentication and scope. They might include details like the ID of the associated user and/or any scopes assigned to that user. - -### JWT authentication configuration +The authorization directives use a request's **claims** to evaluate which fields and types are authorized. Claims are the individual details of a request's authentication and scope. They might include details like the ID of the associated user and any authorization scopes assigned to that user. -If you configure [JWT authentication](./authn-jwt), the Apollo Router automatically adds a JWT token's claims to the request's context at the `apollo_authentication::JWT::claims` key. +To provide the router with the claims it needs to evaluate authorization directives, you must either configure [JSON Web Token (JWT) authentication](./authn-jwt) or add an [external coprocessor](../customizations/coprocessor) that adds claims to a request's context. In [some cases](#claim-augmentation-via-coprocessors) (explained below), you may require both. -To-do: More information on how to use JWT auth with these directives +### JWT authentication configuration +If you configure [JWT authentication](./authn-jwt), the Apollo Router [automatically adds a JWT token's claims](./authn-jwt#working-with-jwt-claims) to the request's context at the `apollo_authentication::JWT::claims` key. These claims are then accessible to the authorization directives to evaluate which fields and types are authorized. + ### Claim augmentation via coprocessors -Tokens can come in with limited information, that is then used to look up user specific information like roles. This can be done with [coprocessors](/customizations/coprocessor). +Your authorization policies may require information beyond what your JWT tokens' claims provide. For example, the claims may include a user ID, which you then use to look up a user's role. For situations like this, you can augment claims with [coprocessors](/customizations/coprocessor). -The router level coprocessor is guaranteed to be called after the authentication plugin, so the coprocessor can receive the list of claims extracted from the token, use information like the `sub` (subject) claim to look up the user, insert its data in the claims list and return it to the router. +A [`RouterService` coprocessor](/customizations/coprocessor#how-it-works) is called after the authentication plugin, which suits it to this use case. You can use this type of coprocessor to: +- receive the list of claims extracted from the token +- use information like the `sub` (subject) claim to look up the user in an external database or service +- insert additional data in the claims list +- return the claims list to the router -If the router is configured with: +For example, if you use this [router configuration](/configuration/overview#yaml-config-file): ```yaml title="router.yaml" authentication: @@ -73,7 +77,7 @@ The coprocessor can then receive a request with this format: } ``` -The coprocessor would then look up the user with identifier specified in the `sub` claim, and return a response with more claims: +The coprocessor can then look up the user with the identifier specified in the `sub` claim and return a response with more claims: ```json @@ -108,7 +112,7 @@ If every field in a particular subgraph's query is marked as requiring authentic #### Usage -To use the `@authenticated` directive in a subgraph you can [import it from the `@link` directive](/federation/federated-types/federated-directives/#importing-directives) like so: +To use the `@authenticated` directive in a subgraph, you can [import it from the `@link` directive](/federation/federated-types/federated-directives/#importing-directives) like so: ```graphql extend schema @@ -120,7 +124,7 @@ extend schema #### Example `@authenticated` use case Suppose you're building a social media platform. Unauthenticated users can view a public post's title, author, and content. -However, you only want authenticated users to be able to see the number of views a post has received. +However, you only want authenticated users to see the number of views a post has received. You also need to be able to query for an authenticated user's information. Your schema may look something like this: @@ -161,7 +165,7 @@ query { } ``` -The router would executed the entire request if it's authenticated. +The router would execute the entire query in an authenticated request. For an unauthenticated request, the router would remove the `@authenticated` fields and execute the filtered query. @@ -228,17 +232,18 @@ If _every_ requested field requires authentication and a request is unauthentica ### `@requiresScopes` The `@requiresScopes` directive marks fields and types as restricted based on required scopes. -Depending on the scopes present on the request, the router filters out unauthorized fields and types. -The directive should include a `scopes` argument that defines an array of the required scopes. +To declare which scopes are required, the directive should include a `scopes` argument with an array of the required scopes. ```graphql @requiresScopes(scopes: ["scope1", "scope2", "scope3"]) ``` +Depending on the scopes present on the request, the router filters out unauthorized fields and types. + > If a field's required `scopes` array includes multiple scopes, the request must include _all_ required scopes to resolve it. The directive validates the required scopes by loading the claims object at the `apollo_authentication::JWT::claims` key in a request's context. -The claims object's `scope` key's value should be a space separated string of scopes in the format defined by the [OAuth2 RFC for access token scopes](https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). +The claims object's `scope` key's value should be a space-separated string of scopes in the format defined by the [OAuth2 RFC for access token scopes](https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). ```rhai claims = context["apollo_authentication::JWT::claims"] @@ -247,9 +252,9 @@ claims["scope"] = "scope1 scope2 scope3" -If the `apollo_authentication::JWT::claims` token holds scopes in another format, for example, an array of strings, or in another claim, you can edit the claims with a [custom Rhai script](../customizations/rhai). +If the `apollo_authentication::JWT::claims` object holds scopes in another format, for example, an array of strings, or at a key other than `"scope"`, you can edit the claims with a [Rhai script](../customizations/rhai). -The example below extracts an array of scopes from the `roles` claim and reformats them as a space separated string. +The example below extracts an array of scopes from the `"roles"` claim and reformats them as a space-separated string. ```Rhai fn router_service(service) { @@ -282,7 +287,7 @@ Like the efficiencies gained via the `@authenticated` directive, if every field #### Usage -To use the `@requiresScopes` directive in a subgraph you can [import it from the `@link` directive](/federation/federated-types/federated-directives/#importing-directives) like so: +To use the `@requiresScopes` directive in a subgraph, you can [import it from the `@link` directive](/federation/federated-types/federated-directives/#importing-directives) like so: ```graphql extend schema @@ -321,9 +326,8 @@ type Post { } ``` -The router executes the following query differently, depending on the request's attached scopes. - -If the request includes only the `read:others` scope, then the router would execute the following filtered query: +Depending on a request's attached scopes, the router executes the following query differently. +If the request includes only the `read:others` scope, then the router will execute the following filtered query: @@ -348,27 +352,32 @@ query { -The response would include an errors at the `/users/@/email` path since that field requires the `read:emails` scope. +The response would include an error at the `/users/@/email` path since that field requires the `read:emails` scope. -If the request includes the `read:others read:emails` scope set, then the router can successfully execute the entire query. +If the request includes the `read:others read:emails` scope set, the router can execute the entire query successfully. ### `@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). -The directive should include a `policies` argument that defines an array of the required policies. The example below includes one policy that requires all roles from the claims object to include the string `"support"`. +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. +The directive should include a `policies` argument that defines an array of the required policies. ```graphql @policy(policies: ["claims[`roles`].contains(`support`)"]) ``` -The Apollo Router extracts the list of policies relevant to a request from the schema. It then stores them in the request's context, under the key `apollo_authorization::policies::required` as a map `policy -> null|true|false`. This happens at the [Router service level](../customizations/overview#the-request-lifecycle). A Rhai script or a coprocessor at the Supergraph service level goes through this map, setting the value to `true` if they are successful and `false` if not. After that, the router filters the requests' types and fields to only those where the policy is `true`. -If the `policies` array contains multiple elements, only one of them has to be successful for the policy to be `true`. +The preceding example includes one policy that requires all roles from the claims object to include the string `"support"`. + +The Apollo Router extracts the list of policies relevant to a request from the schema. It then stores them in the request's context in `apollo_authorization::policies::required` as a map `policy -> null|true|false`. This process happens at the [`RouterService` level](../customizations/overview#the-request-lifecycle). -Like the efficiencies gained via the authorization directives, if every field on a particular subgraph query is unauthorized because none of its policies pass, this can eliminate entire subgraph requests. +You need to provide a Rhai script or a coprocessor at the `SupergraphService` level that evaluates the map, setting the value to `true` if the policy is validated and `false` if not. After that, the router filters the requests' types and fields to only those where the policy is `true`. + +> If the `policies` array contains multiple elements, _only one must be successful for the policy to be `true`_. + +Like the efficiencies gained via the other authorization directives, if every field on a particular subgraph query is unauthorized because none of its policies pass, this can eliminate entire subgraph requests. #### 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: +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 @@ -377,13 +386,15 @@ extend schema import: [..., "@policy"]) ``` -Using the `@policy` directive requires a [Supergraph plugin](../customizations/overview) to evaluate the authorization policies. You can do this with a a [Rhai script](../customizations/rhai/), [coprocessor](../customizations/coprocessor), or [native plugin](/customizations/native). Refer to the following Rhai script and coprocessor examples for more information. +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/), [coprocessor](../customizations/coprocessor), or [native plugin](/customizations/native). Refer to the following Rhai script and coprocessor examples for more information. ##### Usage with a Rhai script +The `policies` argument contains a list of strings with no formatting constraints. That means you can use them to store Rhai code. + -The `policies` argument contains a list of strings, with no formatting constraints, so you can use them to store Rhai code. As an example,the following schema defines policies as boolean expressions that will be evaluated in Rhai: +As an example, the following schema defines policies as boolean expressions that can be evaluated in Rhai: ```graphql type Query { @@ -425,7 +436,7 @@ The script uses the [`eval` function](https://rhai.rs/book/ref/eval.html) to eva ##### Usage with a coprocessor -You can use a [coprocessor](../customizations/coprocessor) called at the Supergraph request stage to receive the list of policies and execute them. This is useful to bridge the router authorization with an existing authorization stack, or link policy execution with lookups in a database. +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. @@ -508,7 +519,7 @@ A user can read their own profile, so `read_profile` will succeed. But only the ## Composition and federation -Authorization directives are defined at the subgraph level and the GraphOS composes them in the supergraph schema. In other words, if subgraph fields or types include `@authenticated`, `@requiresScopes`, or `@policy` directives, they are set on the supergraph too. +Authorization directives are defined at the subgraph level, and GraphOS composes them in the supergraph schema. In other words, if subgraph fields or types include `@authenticated`, `@requiresScopes`, or `@policy` directives, they are set on the supergraph too. If multiple shared subgraph fields include `@requiresScopes`, the supergraph schema merges the sets of scopes required by each subgraph. For example, imagine one subgraph requires the `read:others` scope on the `users` query: @@ -594,7 +605,7 @@ This behavior is similar to what you can create with [contracts](/graphos/delive If a type [implementing an interface](https://www.apollographql.com/docs/apollo-server/schema/unions-interfaces/#interface-type) requires authorization, unauthorized requests can query the interface, but not any parts of the type that require authorization. -For example, consider this schema where the `Post` interface doesn't require authentication, but the `PrivateBlog` type which implements `Post` does: +For example, consider this schema where the `Post` interface doesn't require authentication, but the `PrivateBlog` type, which implements `Post` does: ```graphql type Query { From 84d1054c1e2693414bec015ae54ea60acd964bd0 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 11 Aug 2023 16:29:52 -0600 Subject: [PATCH 56/82] Update intro --- docs/source/configuration/authorization.mdx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 6ff5e7f908..b8b874c93d 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -10,7 +10,9 @@ minVersion: 1.27.0 APIs provide access to business-critical data. Unrestricted access can result in data breaches, monetary losses, or potential denial of service. Even for internal APIs, checks can be essential to limit data to authorized parties. -Enforcing authorization _before_ processing requests is efficient because it allows for early request termination. It also enhances security by creating an initial checkpoint that can be reinforced in other service layers. +Enforcing authorization in the Apollo Router is valuable for a few reasons: +- Validating authorization _before_ processing requests allows for early request termination. +- It supports a defense-in-depth strategy by creating an initial checkpoint that other service layers can reinforce. The Apollo Router provides fine-grained access controls at your graph's edge. Using the `@authenticated`, `@requiresScopes`, and `@policy` directives, you can define access to specific fields and types across your supergraph: @@ -599,7 +601,7 @@ query { } ``` -This behavior is similar to what you can create with [contracts](/graphos/delivery/contracts/) and the [`@inaccessible` directive](https://www.apollographql.com/docs/federation/federated-types/federated-directives/#inaccessible). +This behavior resembles what you can create with [contracts](/graphos/delivery/contracts/) and the [`@inaccessible` directive](https://www.apollographql.com/docs/federation/federated-types/federated-directives/#inaccessible). ### Authorization and interfaces From 62f66e39987cc4069e3827f17a83087293240dc3 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Fri, 11 Aug 2023 16:36:09 -0600 Subject: [PATCH 57/82] Fix relative links --- docs/source/configuration/authorization.mdx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index b8b874c93d..1421e2daf2 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -35,15 +35,18 @@ If you configure [JWT authentication](./authn-jwt), the Apollo Router [automatic ### Claim augmentation via coprocessors -Your authorization policies may require information beyond what your JWT tokens' claims provide. For example, the claims may include a user ID, which you then use to look up a user's role. For situations like this, you can augment claims with [coprocessors](/customizations/coprocessor). +Your authorization policies may require information beyond what your JWT tokens' claims provide. For example, the claims may include a user ID, which you then use to look up a user's role. For situations like this, you can augment claims with [coprocessors](../customizations/coprocessor). -A [`RouterService` coprocessor](/customizations/coprocessor#how-it-works) is called after the authentication plugin, which suits it to this use case. You can use this type of coprocessor to: + + + +A [`RouterService` coprocessor](../customizations/coprocessor#how-it-works) is called after the authentication plugin, which suits it to this use case. You can use this type of coprocessor to: - receive the list of claims extracted from the token - use information like the `sub` (subject) claim to look up the user in an external database or service - insert additional data in the claims list - return the claims list to the router -For example, if you use this [router configuration](/configuration/overview#yaml-config-file): +For example, if you use this [router configuration](../configuration/overview#yaml-config-file): ```yaml title="router.yaml" authentication: @@ -101,6 +104,8 @@ The coprocessor can then look up the user with the identifier specified in the ` } ``` + + ## Authorization directives ### `@authenticated` @@ -388,7 +393,7 @@ extend schema import: [..., "@policy"]) ``` -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/), [coprocessor](../customizations/coprocessor), or [native plugin](/customizations/native). Refer to the following Rhai script and coprocessor examples for more information. +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/), [coprocessor](../customizations/coprocessor), or [native plugin](../customizations/native). Refer to the following Rhai script and coprocessor examples for more information. ##### Usage with a Rhai script @@ -679,4 +684,4 @@ You can also hide fields using [contracts](/graphos/delivery/contracts/). ## Query deduplication -When [query deduplication](/configuration/traffic-shaping/#query-deduplication) is activated for subgraphs, the authorization status is accounted for: the router groups unauthenticated queries together, and authenticated queries are grouped by scope set. \ No newline at end of file +When [query deduplication](../configuration/traffic-shaping/#query-deduplication) is activated for subgraphs, the authorization status is accounted for: the router groups unauthenticated queries together, and authenticated queries are grouped by scope set. \ No newline at end of file From 556330b778e21b5127c1530587b6c043da7a680c Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Mon, 14 Aug 2023 14:06:25 +0200 Subject: [PATCH 58/82] lint --- apollo-router/src/plugins/authorization/scopes.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/apollo-router/src/plugins/authorization/scopes.rs b/apollo-router/src/plugins/authorization/scopes.rs index d34d6dd95f..a5c0eac532 100644 --- a/apollo-router/src/plugins/authorization/scopes.rs +++ b/apollo-router/src/plugins/authorization/scopes.rs @@ -70,7 +70,6 @@ fn scopes_argument(opt_directive: Option<&hir::Directive>) -> impl Iterator Some(value), _ => None, }) - .into_iter() .flatten() .filter_map(|v| match v { Value::String { value, .. } => Some(value), @@ -162,7 +161,7 @@ fn scopes_sets_argument(directive: &hir::Directive) -> impl Iterator Some( value - .into_iter() + .iter() .filter_map(|v| match v { Value::String { value, .. } => Some(value), _ => None, @@ -210,10 +209,9 @@ impl<'a> ScopeFilteringVisitor<'a> { if field_scopes_sets.all(|scopes_set| { empty = false; !self.request_scopes.is_superset(&scopes_set) - }) { - if !empty { - return false; - } + }) && !empty + { + return false; } } From 204a70f78c107ceae799fcbe02a3e7c8f86630c8 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Mon, 14 Aug 2023 13:49:42 -0600 Subject: [PATCH 59/82] Remove @policy docs --- docs/source/configuration/authorization.mdx | 169 +------------------- 1 file changed, 4 insertions(+), 165 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 1421e2daf2..b29a30382d 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -14,11 +14,12 @@ Enforcing authorization in the Apollo Router is valuable for a few reasons: - Validating authorization _before_ processing requests allows for early request termination. - It supports a defense-in-depth strategy by creating an initial checkpoint that other service layers can reinforce. -The Apollo Router provides fine-grained access controls at your graph's edge. Using the `@authenticated`, `@requiresScopes`, and `@policy` directives, you can define access to specific fields and types across your supergraph: +## How it works + +The Apollo Router provides fine-grained access controls at your graph's edge. Using the `@authenticated` and `@requiresScopes` directives, you can define access to specific fields and types across your supergraph: - The [`@authenticated`](#authenticated) directive works in a binary way: authenticated requests can access the specified field or type, and unauthenticated requests can't. - The [`@requiresScopes`](#requiresscopes) directive allows granular access control through the scopes you define. -- 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. You define and use these directives on subgraph schemas, and GraphOS [composes](#composition-and-federation) them onto the supergraph schema. The router then enforces these directives on all incoming requests. @@ -37,7 +38,6 @@ If you configure [JWT authentication](./authn-jwt), the Apollo Router [automatic Your authorization policies may require information beyond what your JWT tokens' claims provide. For example, the claims may include a user ID, which you then use to look up a user's role. For situations like this, you can augment claims with [coprocessors](../customizations/coprocessor). - A [`RouterService` coprocessor](../customizations/coprocessor#how-it-works) is called after the authentication plugin, which suits it to this use case. You can use this type of coprocessor to: @@ -363,170 +363,9 @@ The response would include an error at the `/users/@/email` path since that fiel If the request includes the `read:others read:emails` scope set, the router can execute the entire query successfully. -### `@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. -The directive should include a `policies` argument that defines an array of the required policies. - -```graphql -@policy(policies: ["claims[`roles`].contains(`support`)"]) -``` - -The preceding example includes one policy that requires all roles from the claims object to include the string `"support"`. - -The Apollo Router extracts the list of policies relevant to a request from the schema. It then stores them in the request's context in `apollo_authorization::policies::required` as a map `policy -> null|true|false`. This process happens at the [`RouterService` level](../customizations/overview#the-request-lifecycle). - -You need to provide a Rhai script or a coprocessor at the `SupergraphService` level that evaluates the map, setting the value to `true` if the policy is validated and `false` if not. After that, the router filters the requests' types and fields to only those where the policy is `true`. - -> If the `policies` array contains multiple elements, _only one must be successful for the policy to be `true`_. - -Like the efficiencies gained via the other authorization directives, if every field on a particular subgraph query is unauthorized because none of its policies pass, this can eliminate entire subgraph requests. - -#### 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.5", - import: [..., "@policy"]) -``` - -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/), [coprocessor](../customizations/coprocessor), or [native plugin](../customizations/native). Refer to the following Rhai script and coprocessor examples for more information. - -##### Usage with a Rhai script - -The `policies` argument contains a list of strings with no formatting constraints. That means 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 @join__field(graph: ACCOUNTS) @policy(policies: ["claims[`kind`] == `user`"]) -} - -type User - @join__owner(graph: ACCOUNTS) - @join__type(graph: ACCOUNTS, key: "id") { - id: ID! @join__field(graph: ACCOUNTS) - username: String @join__field(graph: ACCOUNTS) - username: String @join__field(graph: ACCOUNTS) @policy(policies: ["claims[`roles`].contains(`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"]; - - for key in policies.keys() { - let result = eval(key); - policies[key] = result; - } - - request.context["apollo_authorization::policies::required"] = policies; - }; - service.map_request(request_callback); -} -``` - -The script uses the [`eval` function](https://rhai.rs/book/ref/eval.html) to evaluate each policy and store the result in the `apollo_authorization::policies::required` map. - - - -##### 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 @join__field(graph: ACCOUNTS) @policy(policies: ["read_profile"]) -} - -type User - @join__owner(graph: ACCOUNTS) - @join__type(graph: ACCOUNTS, key: "id") { - id: ID! @join__field(graph: ACCOUNTS) - username: String @join__field(graph: ACCOUNTS) - credit_card: String @join__field(graph: ACCOUNTS) @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 -``` - -then 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_address": 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 -{ - // Control properties - "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_address": false - } - } - } -} -``` - - - ## Composition and federation -Authorization directives are defined at the subgraph level, and GraphOS composes them in the supergraph schema. In other words, if subgraph fields or types include `@authenticated`, `@requiresScopes`, or `@policy` directives, they are set on the supergraph too. +Authorization directives are defined at the subgraph level, and GraphOS composes them in the supergraph schema. In other words, if subgraph fields or types include `@authenticated` or `@requiresScopes` directives, they are set on the supergraph too. If multiple shared subgraph fields include `@requiresScopes`, the supergraph schema merges the sets of scopes required by each subgraph. For example, imagine one subgraph requires the `read:others` scope on the `users` query: From 94bb17b1fbec4663ef22090b4bec04438161ce22 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Mon, 14 Aug 2023 14:59:33 -0600 Subject: [PATCH 60/82] Add mermaid diagrams to intro --- docs/source/configuration/authorization.mdx | 32 +++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index b29a30382d..8c74e506df 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -10,9 +10,37 @@ minVersion: 1.27.0 APIs provide access to business-critical data. Unrestricted access can result in data breaches, monetary losses, or potential denial of service. Even for internal APIs, checks can be essential to limit data to authorized parties. -Enforcing authorization in the Apollo Router is valuable for a few reasons: +Enforcing authorization in the **Apollo Router** is valuable for a few reasons: + - It supports a defense-in-depth strategy by creating an initial checkpoint that other service layers can reinforce. + +```mermaid +flowchart LR; + clients(Clients); + subgraph Level1["
🔐 Router Layer                                                   "] + router(["Apollo Router"]); + subgraph Level2["       🔐 Services Layer    "] + serviceB[Products
API]; + serviceC[Reviews
API]; + end + end + + router -->|"Sub-query"| serviceB & serviceC; + clients -->|"Request"| router; +``` + - Validating authorization _before_ processing requests allows for early request termination. -- It supports a defense-in-depth strategy by creating an initial checkpoint that other service layers can reinforce. + +```mermaid +flowchart LR; + clients(Client); + subgraph Router[" "] + router(["Apollo Router"]); + serviceB[Products
API]; + serviceC[Reviews
API]; + end + router -.->|"❌ Sub-query"| serviceB & serviceC; + clients -->|"⚠️Unauthorized
request"| router; +``` ## How it works From 6dd06d6c571d493be2c8983a75872c4f26dbf9d4 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Mon, 14 Aug 2023 17:25:26 -0600 Subject: [PATCH 61/82] Add coprocessor section --- docs/source/configuration/authorization.mdx | 133 +++++++++++++++++--- 1 file changed, 119 insertions(+), 14 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 8c74e506df..a0ef5526fa 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -10,15 +10,16 @@ minVersion: 1.27.0 APIs provide access to business-critical data. Unrestricted access can result in data breaches, monetary losses, or potential denial of service. Even for internal APIs, checks can be essential to limit data to authorized parties. -Enforcing authorization in the **Apollo Router** is valuable for a few reasons: +Enforcing authorization _in the Apollo Router_ is valuable for a few reasons: - It supports a defense-in-depth strategy by creating an initial checkpoint that other service layers can reinforce. ```mermaid flowchart LR; clients(Clients); - subgraph Level1["
🔐 Router Layer                                                   "] + Level2:::padding + subgraph Level1["
🔐 Router layer                                                   "] router(["Apollo Router"]); - subgraph Level2["       🔐 Services Layer    "] + subgraph Level2["🔐 Service layer"] serviceB[Products
API]; serviceC[Reviews
API]; end @@ -26,6 +27,8 @@ flowchart LR; router -->|"Sub-query"| serviceB & serviceC; clients -->|"Request"| router; + +classDef padding padding-left:1em, padding-right:1em ``` - Validating authorization _before_ processing requests allows for early request termination. @@ -44,32 +47,133 @@ flowchart LR; ## How it works -The Apollo Router provides fine-grained access controls at your graph's edge. Using the `@authenticated` and `@requiresScopes` directives, you can define access to specific fields and types across your supergraph: +The Apollo Router provides fine-grained access controls at your graph's edge via **authorization directives**. Using the `@authenticated` and `@requiresScopes` directives, you can define access to specific fields and types across your supergraph: - The [`@authenticated`](#authenticated) directive works in a binary way: authenticated requests can access the specified field or type, and unauthenticated requests can't. - The [`@requiresScopes`](#requiresscopes) directive allows granular access control through the scopes you define. +The authorization directives use a request's **claims** to evaluate which fields and types are authorized. Claims are the individual details of a request's authentication and scope. They might include details like the ID of the associated user and any authorization scopes assigned to that user. + You define and use these directives on subgraph schemas, and GraphOS [composes](#composition-and-federation) them onto the supergraph schema. The router then enforces these directives on all incoming requests. ## Prerequisites -The authorization directives use a request's **claims** to evaluate which fields and types are authorized. Claims are the individual details of a request's authentication and scope. They might include details like the ID of the associated user and any authorization scopes assigned to that user. - -To provide the router with the claims it needs to evaluate authorization directives, you must either configure [JSON Web Token (JWT) authentication](./authn-jwt) or add an [external coprocessor](../customizations/coprocessor) that adds claims to a request's context. In [some cases](#claim-augmentation-via-coprocessors) (explained below), you may require both. +To provide the router with the claims it needs to evaluate authorization directives, you must either configure [JSON Web Token (JWT) authentication](./authn-jwt) or add an [external coprocessor](../customizations/coprocessor) that adds claims to a request's context. In [some cases](#augmenting-claims-with-coprocessors) (explained below), you may require both. ### JWT authentication configuration If you configure [JWT authentication](./authn-jwt), the Apollo Router [automatically adds a JWT token's claims](./authn-jwt#working-with-jwt-claims) to the request's context at the `apollo_authentication::JWT::claims` key. These claims are then accessible to the authorization directives to evaluate which fields and types are authorized. - -### Claim augmentation via coprocessors -Your authorization policies may require information beyond what your JWT tokens' claims provide. For example, the claims may include a user ID, which you then use to look up a user's role. For situations like this, you can augment claims with [coprocessors](../customizations/coprocessor). +### Adding claims with coprocessor + +If you can't use JWT tokens to add claims to your request, you can do so via a [`RouterService` coprocessor](../customizations/coprocessor#how-it-works). +Coprocessors let you hook into the [router's request-handling lifecycle](../customizations/overview#the-request-lifecycle) using standalone code. A [`RouterService` coprocessor](../customizations/coprocessor#how-it-works) hooks into the request life cycle directly after the router has received a client request. + +```mermaid +flowchart TB; + client(Client); + router:::alignLeft + subgraph router["Apollo Router"] + direction LR + httpServer("HTTP server") + subgraph routerService["RouterService"] + routerPlugins[[Router plugins]]; + end + subgraph supergraphService["SupergraphService"] + supergraphPlugins[[Supergraph plugins]]; + end + subgraph executionService["ExecutionService"] + executionPlugins[[Execution plugins]]; + end + + subgraph subgraphService["SubgraphServices"] + subgraph service1["Subgraph Service A"] + subgraphPlugins1[[Subgraph plugins]]; + end + subgraph service2["Subgraph Service B"] + subgraphPlugins2[[Subgraph plugins]]; + end + end + end; +subgraphA[Subgraph A]; +subgraphB[Subgraph B]; + +coprocessor[Coprocessor] +routerService <--> coprocessor + +client --> httpServer; +httpServer --> routerService; +routerService --> supergraphService +supergraphService --> executionService; +executionService --> service1; +executionService --> service2; +service1 --> subgraphA; +service2 --> subgraphB; + +classDef alignLeft padding-right:25em; +class coprocessor secondary; +``` + +You configure external coprocessing in your router's [YAML config file](../configuration/overview/#yaml-config-file) under the `coprocessor` key. To use the authorization directives, you need to add claims to a `RouterRequest`'s [`context`](/customizations/coprocessor/#context). +The router configuration needs to include at least these settings: + +```yaml title="router.yaml" +coprocessor: + url: http://127.0.0.1:8081 # Required. Replace with the URL of your coprocessor's HTTP endpoint. + router: # By including this key, a coprocessor can hook into the `RouterService` + request: # By including this key, the `RouterService` sends a coprocessor request whenever it first receives a client request. + headers: false # These boolean properties indicate which request data to include in the coprocessor request. All are optional and false by default. + context: true # The authorization directives works with claims stored in the request's context +``` + +This configuration prompts the router to send an HTTP POST request to your coprocessor whenever it receives a client request. For example, your coprocessor may receive a request with this format: + +```json +{ + "version": 1, + "stage": "RouterRequest", + "control": "continue", + "id": "d0a8245df0efe8aa38a80dba1147fb2e", + "context": { + "entries": { + "accepts-json": true + } + } +} +``` + +When your coprocessor receives this request from the router, it should add claims to the request's [`context`](/customizations/coprocessor/#context) and return them in the response to the router. + +Specifically, the coprocessor should add an entry with a claims object. The key must be `apollo_authentication::JWT::claims`, and the value should be the claims required by the authorization directives you intend to use. For example, if you want to use [`@requireScopes`](#requirescopes), the response may look something like this: + +```json +{ + "version": 1, + "stage": "RouterRequest", + "control": "continue", + "id": "d0a8245df0efe8aa38a80dba1147fb2e", + "context": { + "entries": { + "accepts-json": true, + "apollo_authentication::JWT::claims": { + "scope": "profile:read profile:write" + } + } + } +} +``` + +Refer to [the coprocessor documentation](../customizations/coprocessor) for more information on [coprocessor request format](../customizations/coprocessor/#coprocessor-request-format) and [responding to coprocessor requests](../customizations/coprocessor/#responding-to-coprocessor-requests). + +#### Augmenting claims with coprocessors + +Your authorization policies may require information beyond what your JWT tokens' claims provide. For example, the claims may include user IDs, which you then use to look up the user roles. For situations like this, you can augment the claims from your JWT tokens with [coprocessors](../customizations/coprocessor). -A [`RouterService` coprocessor](../customizations/coprocessor#how-it-works) is called after the authentication plugin, which suits it to this use case. You can use this type of coprocessor to: -- receive the list of claims extracted from the token +As with [adding claims](#adding-claims-with-coprocessor), a [`RouterService` coprocessor](../customizations/coprocessor#how-it-works) is appropriate for augmenting claims since the router calls it directly after receiving a client request. Specifically, the router calls it after the JWT authentication plugin, so you can use a `RouterService` coprocessor to: +- receive the list of claims extracted from the JWT - use information like the `sub` (subject) claim to look up the user in an external database or service - insert additional data in the claims list - return the claims list to the router @@ -90,7 +194,7 @@ coprocessor: context: true ``` -The coprocessor can then receive a request with this format: +The router sends requests to the coprocessor with this format: ```json { @@ -115,7 +219,6 @@ The coprocessor can then look up the user with the identifier specified in the ` ```json { - // Control properties "version": 1, "stage": "RouterRequest", "control": "continue", @@ -132,6 +235,8 @@ The coprocessor can then look up the user with the identifier specified in the ` } ``` +For more information, refer to [the coprocessor documentation](../customizations/coprocessor/). + ## Authorization directives From 9e0d867d1a005a7ef5c6fa1d85ec38619d54eeb4 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Mon, 14 Aug 2023 17:25:40 -0600 Subject: [PATCH 62/82] Copy edit --- docs/source/configuration/authorization.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index a0ef5526fa..2024e6628e 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -172,7 +172,7 @@ Your authorization policies may require information beyond what your JWT tokens' -As with [adding claims](#adding-claims-with-coprocessor), a [`RouterService` coprocessor](../customizations/coprocessor#how-it-works) is appropriate for augmenting claims since the router calls it directly after receiving a client request. Specifically, the router calls it after the JWT authentication plugin, so you can use a `RouterService` coprocessor to: +As with [adding claims](#adding-claims-with-coprocessor), a [`RouterService` coprocessor](../customizations/coprocessor#how-it-works) is appropriate for augmenting claims since the router calls it directly after receiving a client request. The router calls it after the JWT authentication plugin, so you can use a `RouterService` coprocessor to: - receive the list of claims extracted from the JWT - use information like the `sub` (subject) claim to look up the user in an external database or service - insert additional data in the claims list From 1dbdc780ee1476a9594729450854f2b7fc5030a2 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Mon, 14 Aug 2023 18:13:54 -0600 Subject: [PATCH 63/82] Copy edit --- docs/source/configuration/authorization.mdx | 26 ++++++++++----------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 2024e6628e..4c661eb75e 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -54,20 +54,20 @@ The Apollo Router provides fine-grained access controls at your graph's edge via The authorization directives use a request's **claims** to evaluate which fields and types are authorized. Claims are the individual details of a request's authentication and scope. They might include details like the ID of the associated user and any authorization scopes assigned to that user. -You define and use these directives on subgraph schemas, and GraphOS [composes](#composition-and-federation) them onto the supergraph schema. +You define and use authorization directives on subgraph schemas, and GraphOS [composes](#composition-and-federation) them onto the supergraph schema. The router then enforces these directives on all incoming requests. ## Prerequisites -To provide the router with the claims it needs to evaluate authorization directives, you must either configure [JSON Web Token (JWT) authentication](./authn-jwt) or add an [external coprocessor](../customizations/coprocessor) that adds claims to a request's context. In [some cases](#augmenting-claims-with-coprocessors) (explained below), you may require both. +To provide the router with the claims it needs to evaluate authorization directives, you must either configure [JSON Web Token (JWT) authentication](./authn-jwt) or add an [external coprocessor](../customizations/coprocessor) that adds claims to a request's context. In [some cases](#augmenting-jwt-claims-via-coprocessor) (explained below), you may require both. ### JWT authentication configuration If you configure [JWT authentication](./authn-jwt), the Apollo Router [automatically adds a JWT token's claims](./authn-jwt#working-with-jwt-claims) to the request's context at the `apollo_authentication::JWT::claims` key. These claims are then accessible to the authorization directives to evaluate which fields and types are authorized. -### Adding claims with coprocessor +### Adding claims via coprocessor -If you can't use JWT tokens to add claims to your request, you can do so via a [`RouterService` coprocessor](../customizations/coprocessor#how-it-works). +If you can't use JWT authentiation, you can add claims via a [`RouterService` coprocessor](../customizations/coprocessor#how-it-works). Coprocessors let you hook into the [router's request-handling lifecycle](../customizations/overview#the-request-lifecycle) using standalone code. A [`RouterService` coprocessor](../customizations/coprocessor#how-it-works) hooks into the request life cycle directly after the router has received a client request. ```mermaid @@ -143,9 +143,7 @@ This configuration prompts the router to send an HTTP POST request to your copro } ``` -When your coprocessor receives this request from the router, it should add claims to the request's [`context`](/customizations/coprocessor/#context) and return them in the response to the router. - -Specifically, the coprocessor should add an entry with a claims object. The key must be `apollo_authentication::JWT::claims`, and the value should be the claims required by the authorization directives you intend to use. For example, if you want to use [`@requireScopes`](#requirescopes), the response may look something like this: +When your coprocessor receives this request from the router, it should add claims to the request's [`context`](/customizations/coprocessor/#context) and return them in the response to the router. Specifically, the coprocessor should add an entry with a claims object. The key must be `apollo_authentication::JWT::claims`, and the value should be the claims required by the authorization directives you intend to use. For example, if you want to use [`@requireScopes`](#requirescopes), the response may look something like this: ```json { @@ -166,13 +164,13 @@ Specifically, the coprocessor should add an entry with a claims object. The key Refer to [the coprocessor documentation](../customizations/coprocessor) for more information on [coprocessor request format](../customizations/coprocessor/#coprocessor-request-format) and [responding to coprocessor requests](../customizations/coprocessor/#responding-to-coprocessor-requests). -#### Augmenting claims with coprocessors +#### Augmenting JWT claims via coprocessor -Your authorization policies may require information beyond what your JWT tokens' claims provide. For example, the claims may include user IDs, which you then use to look up the user roles. For situations like this, you can augment the claims from your JWT tokens with [coprocessors](../customizations/coprocessor). +Your authorization policies may require information beyond what your JSON web tokens' claims provide. For example, the token's claims may include user IDs, which you then use to look up user roles. For situations like this, you can augment the claims from your JSON web tokens with [coprocessors](../customizations/coprocessor). -As with [adding claims](#adding-claims-with-coprocessor), a [`RouterService` coprocessor](../customizations/coprocessor#how-it-works) is appropriate for augmenting claims since the router calls it directly after receiving a client request. The router calls it after the JWT authentication plugin, so you can use a `RouterService` coprocessor to: +As with [adding claims](#adding-claims-via-coprocessor), a [`RouterService` coprocessor](../customizations/coprocessor#how-it-works) is appropriate for augmenting claims since the router calls it directly after receiving a client request. The router calls it after the JWT authentication plugin, so you can use a `RouterService` coprocessor to: - receive the list of claims extracted from the JWT - use information like the `sub` (subject) claim to look up the user in an external database or service - insert additional data in the claims list @@ -245,7 +243,7 @@ For more information, refer to [the coprocessor documentation](../customizations 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. -If the request is authenticated, the router executes the query in its entirety. +If the key exists, it means the request is authenticated and the router executes the query in its entirety. If the request is unauthenticated, the router removes `@authenticated` fields before planning the query and only executes the parts of the query that don't require authentication. If every field in a particular subgraph's query is marked as requiring authentication, this can eliminate entire subgraph requests, thereby increasing router efficiency. @@ -467,7 +465,7 @@ type Post { ``` Depending on a request's attached scopes, the router executes the following query differently. -If the request includes only the `read:others` scope, then the router will execute the following filtered query: +If the request includes only the `read:others` scope, then the router executes the following filtered query: @@ -538,7 +536,7 @@ type Query { type Product @key(fields: "id") { id: ID! @authenticated - username: String! + name: String! price: Int @authenticated } ``` @@ -559,7 +557,7 @@ An unauthenticated request would successfully execute this query: ```graphql query { product { - username + name inStock } } From b1dccdf4ef367e0351da971ce094ff41af4b9297 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Tue, 15 Aug 2023 07:43:42 -0600 Subject: [PATCH 64/82] Nest coprocessor sections in Expansion blocks --- docs/source/configuration/authorization.mdx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 4c661eb75e..f8a35ecef9 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -59,7 +59,7 @@ The router then enforces these directives on all incoming requests. ## Prerequisites -To provide the router with the claims it needs to evaluate authorization directives, you must either configure [JSON Web Token (JWT) authentication](./authn-jwt) or add an [external coprocessor](../customizations/coprocessor) that adds claims to a request's context. In [some cases](#augmenting-jwt-claims-via-coprocessor) (explained below), you may require both. +To provide the router with the claims it needs to evaluate authorization directives, you must either configure [JSON Web Token (JWT) authentication](#jwt-authentication-configuration) or add an [external coprocessor](#adding-claims-via-coprocessor) that adds claims to a request's context. In [some cases](#augmenting-jwt-claims-via-coprocessor) (explained below), you may require both. ### JWT authentication configuration @@ -68,6 +68,9 @@ If you configure [JWT authentication](./authn-jwt), the Apollo Router [automatic ### Adding claims via coprocessor If you can't use JWT authentiation, you can add claims via a [`RouterService` coprocessor](../customizations/coprocessor#how-it-works). + + + Coprocessors let you hook into the [router's request-handling lifecycle](../customizations/overview#the-request-lifecycle) using standalone code. A [`RouterService` coprocessor](../customizations/coprocessor#how-it-works) hooks into the request life cycle directly after the router has received a client request. ```mermaid @@ -164,11 +167,13 @@ When your coprocessor receives this request from the router, it should add claim Refer to [the coprocessor documentation](../customizations/coprocessor) for more information on [coprocessor request format](../customizations/coprocessor/#coprocessor-request-format) and [responding to coprocessor requests](../customizations/coprocessor/#responding-to-coprocessor-requests). + + #### Augmenting JWT claims via coprocessor Your authorization policies may require information beyond what your JSON web tokens' claims provide. For example, the token's claims may include user IDs, which you then use to look up user roles. For situations like this, you can augment the claims from your JSON web tokens with [coprocessors](../customizations/coprocessor). - + As with [adding claims](#adding-claims-via-coprocessor), a [`RouterService` coprocessor](../customizations/coprocessor#how-it-works) is appropriate for augmenting claims since the router calls it directly after receiving a client request. The router calls it after the JWT authentication plugin, so you can use a `RouterService` coprocessor to: - receive the list of claims extracted from the JWT From 7ade43864a90333e03bed82caf5fd83b9d58a722 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Tue, 15 Aug 2023 16:50:07 -0600 Subject: [PATCH 65/82] Apply feedback from docs review --- docs/source/configuration/authn-jwt.mdx | 73 +++++++ docs/source/configuration/authorization.mdx | 218 ++------------------ docs/source/customizations/coprocessor.mdx | 95 +++++++++ 3 files changed, 190 insertions(+), 196 deletions(-) diff --git a/docs/source/configuration/authn-jwt.mdx b/docs/source/configuration/authn-jwt.mdx index fdfe303c04..149be31017 100644 --- a/docs/source/configuration/authn-jwt.mdx +++ b/docs/source/configuration/authn-jwt.mdx @@ -264,6 +264,79 @@ fn subgraph_service(service, subgraph) { +### Claim augmentation via coprocessors + +You may require information beyond what your JSON web tokens provide. For example, a token's claims may include user IDs, which you then use to look up user roles. For situations like this, you can augment the claims from your JSON web tokens with [coprocessors](../customizations/coprocessor#how-it-works). + + + +A [`RouterService` coprocessor](../customizations/coprocessor#how-it-works) is appropriate for augmenting claims since the router calls it directly after receiving a client request. The router calls it after the JWT authentication plugin, so you can use a `RouterService` coprocessor to: +- receive the list of claims extracted from the JWT +- use information like the `sub` (subject) claim to look up the user in an external database or service +- insert additional data in the claims list +- return the claims list to the router + +For example, if you use this [router configuration](./overview#yaml-config-file): + +```yaml title="router.yaml" +authentication: + router: + jwt: + jwks: + - url: "file:///etc/router/jwks.json" + +coprocessor: + url: http://127.0.0.1:8081 + router: + request: + context: true +``` + +The router sends requests to the coprocessor with this format: + +```json +{ + "version": 1, + "stage": "RouterRequest", + "control": "continue", + "id": "d0a8245df0efe8aa38a80dba1147fb2e", + "context": { + "entries": { + "apollo_authentication::JWT::claims": { + "exp": 10000000000, + "sub": "457f6bb6-789c-4e8b-8560-f3943a09e72a" + } + } + }, + "method": "POST" +} +``` + +The coprocessor can then look up the user with the identifier specified in the `sub` claim and return a response with more claims: + + +```json +{ + "version": 1, + "stage": "RouterRequest", + "control": "continue", + "id": "d0a8245df0efe8aa38a80dba1147fb2e", + "context": { + "entries": { + "apollo_authentication::JWT::claims": { + "exp": 10000000000, + "sub": "457f6bb6-789c-4e8b-8560-f3943a09e72a", + "scope": "profile:read profile:write" + } + } + } +} +``` + +For more information, refer to [the coprocessor documentation](../customizations/coprocessor/). + + + ## Creating your own JWKS (advanced) > ⚠️ **Most third-party IdP services create and host a JWKS for you.** If you use a third-party IdP, consult its documentation to obtain the [JWKS URL](#jwks) to pass to your router. diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index f8a35ecef9..e02e0c75f6 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -1,6 +1,6 @@ --- title: Authorization in the Apollo Router -description: Strengthen your supergraph's security with advanced access controls +description: Enhance your services' security with an overlying governance layer minVersion: 1.27.0 --- @@ -11,7 +11,22 @@ minVersion: 1.27.0 APIs provide access to business-critical data. Unrestricted access can result in data breaches, monetary losses, or potential denial of service. Even for internal APIs, checks can be essential to limit data to authorized parties. Enforcing authorization _in the Apollo Router_ is valuable for a few reasons: - - It supports a defense-in-depth strategy by creating an initial checkpoint that other service layers can reinforce. + +- **Optimized query execution**: Validating authorization _before_ processing requests allows for early termination. Terminating unauthorized requests at the edge of your graph reduces the load on your services and improves performance. + +```mermaid +flowchart LR; + clients(Client); + subgraph Router[" "] + router(["Apollo Router"]); + serviceB[Products
API]; + serviceC[Reviews
API]; + end + router -.->|"❌ Sub-query"| serviceB & serviceC; + clients -->|"⚠️Unauthorized
request"| router; +``` + +- **Principled architecture**: Authorization at the router level supports a defense-in-depth strategy by creating an initial checkpoint that other service layers can reinforce. ```mermaid flowchart LR; @@ -31,19 +46,7 @@ flowchart LR; classDef padding padding-left:1em, padding-right:1em ``` -- Validating authorization _before_ processing requests allows for early request termination. - -```mermaid -flowchart LR; - clients(Client); - subgraph Router[" "] - router(["Apollo Router"]); - serviceB[Products
API]; - serviceC[Reviews
API]; - end - router -.->|"❌ Sub-query"| serviceB & serviceC; - clients -->|"⚠️Unauthorized
request"| router; -``` +- **Declarative access rules**: You define access controls at the field level and GraphOS [composes](#composition-and-federation) them _across_ all your services. Composition creates graph-native governance without you having to create an additional orchestration layer. ## How it works @@ -59,188 +62,11 @@ The router then enforces these directives on all incoming requests. ## Prerequisites -To provide the router with the claims it needs to evaluate authorization directives, you must either configure [JSON Web Token (JWT) authentication](#jwt-authentication-configuration) or add an [external coprocessor](#adding-claims-via-coprocessor) that adds claims to a request's context. In [some cases](#augmenting-jwt-claims-via-coprocessor) (explained below), you may require both. - -### JWT authentication configuration - -If you configure [JWT authentication](./authn-jwt), the Apollo Router [automatically adds a JWT token's claims](./authn-jwt#working-with-jwt-claims) to the request's context at the `apollo_authentication::JWT::claims` key. These claims are then accessible to the authorization directives to evaluate which fields and types are authorized. - -### Adding claims via coprocessor - -If you can't use JWT authentiation, you can add claims via a [`RouterService` coprocessor](../customizations/coprocessor#how-it-works). - - +To provide the router with the claims it needs to evaluate authorization directives, you must either configure JSON Web Token (JWT) authentication or add an external coprocessor that adds claims to a request's context. In some cases (explained below), you may require both. -Coprocessors let you hook into the [router's request-handling lifecycle](../customizations/overview#the-request-lifecycle) using standalone code. A [`RouterService` coprocessor](../customizations/coprocessor#how-it-works) hooks into the request life cycle directly after the router has received a client request. - -```mermaid -flowchart TB; - client(Client); - router:::alignLeft - subgraph router["Apollo Router"] - direction LR - httpServer("HTTP server") - subgraph routerService["RouterService"] - routerPlugins[[Router plugins]]; - end - subgraph supergraphService["SupergraphService"] - supergraphPlugins[[Supergraph plugins]]; - end - subgraph executionService["ExecutionService"] - executionPlugins[[Execution plugins]]; - end - - subgraph subgraphService["SubgraphServices"] - subgraph service1["Subgraph Service A"] - subgraphPlugins1[[Subgraph plugins]]; - end - subgraph service2["Subgraph Service B"] - subgraphPlugins2[[Subgraph plugins]]; - end - end - end; -subgraphA[Subgraph A]; -subgraphB[Subgraph B]; - -coprocessor[Coprocessor] -routerService <--> coprocessor - -client --> httpServer; -httpServer --> routerService; -routerService --> supergraphService -supergraphService --> executionService; -executionService --> service1; -executionService --> service2; -service1 --> subgraphA; -service2 --> subgraphB; - -classDef alignLeft padding-right:25em; -class coprocessor secondary; -``` - -You configure external coprocessing in your router's [YAML config file](../configuration/overview/#yaml-config-file) under the `coprocessor` key. To use the authorization directives, you need to add claims to a `RouterRequest`'s [`context`](/customizations/coprocessor/#context). -The router configuration needs to include at least these settings: - -```yaml title="router.yaml" -coprocessor: - url: http://127.0.0.1:8081 # Required. Replace with the URL of your coprocessor's HTTP endpoint. - router: # By including this key, a coprocessor can hook into the `RouterService` - request: # By including this key, the `RouterService` sends a coprocessor request whenever it first receives a client request. - headers: false # These boolean properties indicate which request data to include in the coprocessor request. All are optional and false by default. - context: true # The authorization directives works with claims stored in the request's context -``` - -This configuration prompts the router to send an HTTP POST request to your coprocessor whenever it receives a client request. For example, your coprocessor may receive a request with this format: - -```json -{ - "version": 1, - "stage": "RouterRequest", - "control": "continue", - "id": "d0a8245df0efe8aa38a80dba1147fb2e", - "context": { - "entries": { - "accepts-json": true - } - } -} -``` - -When your coprocessor receives this request from the router, it should add claims to the request's [`context`](/customizations/coprocessor/#context) and return them in the response to the router. Specifically, the coprocessor should add an entry with a claims object. The key must be `apollo_authentication::JWT::claims`, and the value should be the claims required by the authorization directives you intend to use. For example, if you want to use [`@requireScopes`](#requirescopes), the response may look something like this: - -```json -{ - "version": 1, - "stage": "RouterRequest", - "control": "continue", - "id": "d0a8245df0efe8aa38a80dba1147fb2e", - "context": { - "entries": { - "accepts-json": true, - "apollo_authentication::JWT::claims": { - "scope": "profile:read profile:write" - } - } - } -} -``` - -Refer to [the coprocessor documentation](../customizations/coprocessor) for more information on [coprocessor request format](../customizations/coprocessor/#coprocessor-request-format) and [responding to coprocessor requests](../customizations/coprocessor/#responding-to-coprocessor-requests). - - - -#### Augmenting JWT claims via coprocessor - -Your authorization policies may require information beyond what your JSON web tokens' claims provide. For example, the token's claims may include user IDs, which you then use to look up user roles. For situations like this, you can augment the claims from your JSON web tokens with [coprocessors](../customizations/coprocessor). - - - -As with [adding claims](#adding-claims-via-coprocessor), a [`RouterService` coprocessor](../customizations/coprocessor#how-it-works) is appropriate for augmenting claims since the router calls it directly after receiving a client request. The router calls it after the JWT authentication plugin, so you can use a `RouterService` coprocessor to: -- receive the list of claims extracted from the JWT -- use information like the `sub` (subject) claim to look up the user in an external database or service -- insert additional data in the claims list -- return the claims list to the router - -For example, if you use this [router configuration](../configuration/overview#yaml-config-file): - -```yaml title="router.yaml" -authentication: - router: - jwt: - jwks: - - url: "file:///etc/router/jwks.json" - -coprocessor: - url: http://127.0.0.1:8081 - router: - request: - context: true -``` - -The router sends requests to the coprocessor with this format: - -```json -{ - "version": 1, - "stage": "RouterRequest", - "control": "continue", - "id": "d0a8245df0efe8aa38a80dba1147fb2e", - "context": { - "entries": { - "apollo_authentication::JWT::claims": { - "exp": 10000000000, - "sub": "457f6bb6-789c-4e8b-8560-f3943a09e72a" - } - } - }, - "method": "POST" -} -``` - -The coprocessor can then look up the user with the identifier specified in the `sub` claim and return a response with more claims: - - -```json -{ - "version": 1, - "stage": "RouterRequest", - "control": "continue", - "id": "d0a8245df0efe8aa38a80dba1147fb2e", - "context": { - "entries": { - "apollo_authentication::JWT::claims": { - "exp": 10000000000, - "sub": "457f6bb6-789c-4e8b-8560-f3943a09e72a", - "scope": "profile:read profile:write" - } - } - } -} -``` - -For more information, refer to [the coprocessor documentation](../customizations/coprocessor/). - - +- **JWT authentication configuration**: If you configure [JWT authentication](./authn-jwt), the Apollo Router [automatically adds a JWT token's claims](./authn-jwt#working-with-jwt-claims) to the request's context at the `apollo_authentication::JWT::claims` key. +- **Adding claims via coprocessor**: If you can't use JWT authentiation, you can [add claims with a coprocessor](/customizations/coprocessor#adding-authorization-claims-via-coprocessor). Coprocessors let you hook into the Apollo Router's request-handling lifecycle with custom code. +- **Augmenting JWT claims via coprocessor**: Your authorization policies may require information beyond what your JSON web tokens provide. For example, a token's claims may include user IDs, which you then use to look up user roles. For situations like this, you can [augment the claims](./authn-jwt#claim-augmentation-via-coprocessors) from your JSON web tokens with coprocessors. ## Authorization directives diff --git a/docs/source/customizations/coprocessor.mdx b/docs/source/customizations/coprocessor.mdx index de0a1e4ad8..1f4346aeac 100644 --- a/docs/source/customizations/coprocessor.mdx +++ b/docs/source/customizations/coprocessor.mdx @@ -903,3 +903,98 @@ Subsequent response chunks omit the `headers` and `statusCode` fields: } } ``` + +## Adding authorization claims via coprocessor + +An alternative to JWT authentiation is adding claims via a [`RouterService` coprocessor](#how-it-works). since it hooks into the request life cycle directly after the router has received a client request. + +```mermaid +flowchart TB; + client(Client); + router:::alignLeft + subgraph router["Apollo Router"] + direction LR + httpServer("HTTP server") + subgraph routerService["RouterService"] + routerPlugins[[Router plugins]]; + end + subgraph supergraphService["SupergraphService"] + supergraphPlugins[[Supergraph plugins]]; + end + subgraph executionService["ExecutionService"] + executionPlugins[[Execution plugins]]; + end + + subgraph subgraphService["SubgraphServices"] + subgraph service1["Subgraph Service A"] + subgraphPlugins1[[Subgraph plugins]]; + end + subgraph service2["Subgraph Service B"] + subgraphPlugins2[[Subgraph plugins]]; + end + end + end; +subgraphA[Subgraph A]; +subgraphB[Subgraph B]; + +coprocessor[Coprocessor] +routerService <--> coprocessor + +client --> httpServer; +httpServer --> routerService; +routerService --> supergraphService +supergraphService --> executionService; +executionService --> service1; +executionService --> service2; +service1 --> subgraphA; +service2 --> subgraphB; + +classDef alignLeft padding-right:25em; +class coprocessor secondary; +``` +To use the [authorization directives](../configuration/authorization#authorization-directives), you need to add claims to a `RouterRequest`'s [`context`](#context). +The router configuration needs to include at least these settings: + +```yaml title="router.yaml" +coprocessor: + url: http://127.0.0.1:8081 # Required. Replace with the URL of your coprocessor's HTTP endpoint. + router: # By including this key, a coprocessor can hook into the `RouterService` + request: # By including this key, the `RouterService` sends a coprocessor request whenever it first receives a client request. + headers: false # These boolean properties indicate which request data to include in the coprocessor request. All are optional and false by default. + context: true # The authorization directives works with claims stored in the request's context +``` + +This configuration prompts the router to send an HTTP POST request to your coprocessor whenever it receives a client request. For example, your coprocessor may receive a request with this format: + +```json +{ + "version": 1, + "stage": "RouterRequest", + "control": "continue", + "id": "d0a8245df0efe8aa38a80dba1147fb2e", + "context": { + "entries": { + "accepts-json": true + } + } +} +``` + +When your coprocessor receives this request from the router, it should add claims to the request's [`context`](#context) and return them in the response to the router. Specifically, the coprocessor should add an entry with a claims object. The key must be `apollo_authentication::JWT::claims`, and the value should be the claims required by the authorization directives you intend to use. For example, if you want to use [`@requireScopes`](../configuration/authorization#requiresscopes), the response may look something like this: + +```json +{ + "version": 1, + "stage": "RouterRequest", + "control": "continue", + "id": "d0a8245df0efe8aa38a80dba1147fb2e", + "context": { + "entries": { + "accepts-json": true, + "apollo_authentication::JWT::claims": { + "scope": "profile:read profile:write" + } + } + } +} +``` \ No newline at end of file From 8b44636ba04d250a41af4161d18085d2a3279328 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Tue, 15 Aug 2023 17:09:39 -0600 Subject: [PATCH 66/82] Copy edit --- docs/source/configuration/authorization.mdx | 4 +- docs/source/customizations/coprocessor.mdx | 51 ++------------------- 2 files changed, 5 insertions(+), 50 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index e02e0c75f6..a081d65cc7 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -53,11 +53,11 @@ classDef padding padding-left:1em, padding-right:1em The Apollo Router provides fine-grained access controls at your graph's edge via **authorization directives**. Using the `@authenticated` and `@requiresScopes` directives, you can define access to specific fields and types across your supergraph: - The [`@authenticated`](#authenticated) directive works in a binary way: authenticated requests can access the specified field or type, and unauthenticated requests can't. -- The [`@requiresScopes`](#requiresscopes) directive allows granular access control through the scopes you define. +- The [`@requiresScopes`](#requiresscopes) directive allows granular access control through the scopes you define. For example, you may declare that querying a user's information requires a `read:user` scope, while mutating a user's information requires an `update:user` scope. You can require scopes on individual fields. The authorization directives use a request's **claims** to evaluate which fields and types are authorized. Claims are the individual details of a request's authentication and scope. They might include details like the ID of the associated user and any authorization scopes assigned to that user. -You define and use authorization directives on subgraph schemas, and GraphOS [composes](#composition-and-federation) them onto the supergraph schema. +You define and use authorization directives on fields and types in subgraph schemas, and GraphOS [composes](#composition-and-federation) them onto the supergraph schema. The router then enforces these directives on all incoming requests. ## Prerequisites diff --git a/docs/source/customizations/coprocessor.mdx b/docs/source/customizations/coprocessor.mdx index 1f4346aeac..fa2bac638f 100644 --- a/docs/source/customizations/coprocessor.mdx +++ b/docs/source/customizations/coprocessor.mdx @@ -35,11 +35,11 @@ flowchart TB; end; subgraphs[[Subgraphs]]; client --"1. Sends request"--> routerService; - routerService -."2. Can send request
details to coprocessor
and receive modifications".-> coprocessing; + routerService <-."2. Can send request
details to coprocessor
and receive modifications".-> coprocessing; routerService --"3"--> supergraphService; supergraphService --"4"--> executionService; executionService --"5"--> subgraphService; - subgraphService -."6. Can send request
details to coprocessor
and receive modifications".-> coprocessing; + subgraphService <-."6. Can send request
details to coprocessor
and receive modifications".-> coprocessing; subgraphService -- "7"--> subgraphs; class client,subgraphs,coprocessing secondary; @@ -906,53 +906,8 @@ Subsequent response chunks omit the `headers` and `statusCode` fields: ## Adding authorization claims via coprocessor -An alternative to JWT authentiation is adding claims via a [`RouterService` coprocessor](#how-it-works). since it hooks into the request life cycle directly after the router has received a client request. +To use the [authorization directives](../configuration/authorization#authorization-directives), a request needs to include **claims**—the details of its authentication and scope. The most straightforward way to add claims is with [JWT authentication](../configuration/./authn-jwt). You can also add claims with a [`RouterService` coprocessor](#how-it-works) since it hooks into the request lifecycle directly after the router has received a client request. -```mermaid -flowchart TB; - client(Client); - router:::alignLeft - subgraph router["Apollo Router"] - direction LR - httpServer("HTTP server") - subgraph routerService["RouterService"] - routerPlugins[[Router plugins]]; - end - subgraph supergraphService["SupergraphService"] - supergraphPlugins[[Supergraph plugins]]; - end - subgraph executionService["ExecutionService"] - executionPlugins[[Execution plugins]]; - end - - subgraph subgraphService["SubgraphServices"] - subgraph service1["Subgraph Service A"] - subgraphPlugins1[[Subgraph plugins]]; - end - subgraph service2["Subgraph Service B"] - subgraphPlugins2[[Subgraph plugins]]; - end - end - end; -subgraphA[Subgraph A]; -subgraphB[Subgraph B]; - -coprocessor[Coprocessor] -routerService <--> coprocessor - -client --> httpServer; -httpServer --> routerService; -routerService --> supergraphService -supergraphService --> executionService; -executionService --> service1; -executionService --> service2; -service1 --> subgraphA; -service2 --> subgraphB; - -classDef alignLeft padding-right:25em; -class coprocessor secondary; -``` -To use the [authorization directives](../configuration/authorization#authorization-directives), you need to add claims to a `RouterRequest`'s [`context`](#context). The router configuration needs to include at least these settings: ```yaml title="router.yaml" From ae586bd067c13951be300a1213c31746a5c97b9f Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Wed, 16 Aug 2023 17:35:18 -0600 Subject: [PATCH 67/82] More docs edits --- docs/source/configuration/authorization.mdx | 77 +++++++++++---------- 1 file changed, 39 insertions(+), 38 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index a081d65cc7..8162eb6969 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -1,6 +1,6 @@ --- title: Authorization in the Apollo Router -description: Enhance your services' security with an overlying governance layer +description: Strengthen service security with a centralized governance layer minVersion: 1.27.0 --- @@ -12,48 +12,47 @@ APIs provide access to business-critical data. Unrestricted access can result in Enforcing authorization _in the Apollo Router_ is valuable for a few reasons: -- **Optimized query execution**: Validating authorization _before_ processing requests allows for early termination. Terminating unauthorized requests at the edge of your graph reduces the load on your services and improves performance. +- **Optimal query execution**: Validating authorization _before_ processing requests allows for early request termination. Stopping unauthorized requests at the edge of your graph reduces the load on your services and enhances performance. ```mermaid flowchart LR; clients(Client); subgraph Router[" "] router(["Apollo Router"]); - serviceB[Products
API]; - serviceC[Reviews
API]; + serviceB[Users
API]; + serviceC[Posts
API]; end - router -.->|"❌ Sub-query"| serviceB & serviceC; + router -.->|"❌ Subquery"| serviceB & serviceC; clients -->|"⚠️Unauthorized
request"| router; ``` -- **Principled architecture**: Authorization at the router level supports a defense-in-depth strategy by creating an initial checkpoint that other service layers can reinforce. + - If every field in a particular subquery requires authorization, the router's [query planner](../customizations/overview#request-path) can _eliminate entire subgraph requests_ for unauthorized requests. For example, a request may have permission to view a particular user's posts on a social media platform but not have permission to view any of that user's PII. ```mermaid flowchart LR; - clients(Clients); - Level2:::padding - subgraph Level1["
🔐 Router layer                                                   "] + clients(Client); + subgraph Router[" "] router(["Apollo Router"]); - subgraph Level2["🔐 Service layer"] - serviceB[Products
API]; - serviceC[Reviews
API]; - end + serviceB[Users
API]; + serviceC[Posts
API]; end - - router -->|"Sub-query"| serviceB & serviceC; - clients -->|"Request"| router; - -classDef padding padding-left:1em, padding-right:1em + router -->|"✅ Authorized
subquery"| serviceC; + router -.->|"❌ Unauthorized
subquery"| serviceB; + clients -->|"⚠️ Partially authorized
request"| router; ``` -- **Declarative access rules**: You define access controls at the field level and GraphOS [composes](#composition-and-federation) them _across_ all your services. Composition creates graph-native governance without you having to create an additional orchestration layer. +- **Declarative access rules**: You define access controls at the field level, and GraphOS [composes](#composition-and-federation) them across your services. These rules create graph-native governance without the need for an extra orchestration layer. + +- **Principled architecture**: Through composition, the router centralizes authorization logic while allowing for auditing at the service level. [Query deduplication](./traffic-shaping/#query-deduplication) also accounts for authorization by grouping requested fields based on their required authorization. ## How it works -The Apollo Router provides fine-grained access controls at your graph's edge via **authorization directives**. Using the `@authenticated` and `@requiresScopes` directives, you can define access to specific fields and types across your supergraph: +The Apollo Router provides access controls via **authorization directives**. Using the `@authenticated` and `@requiresScopes` directives, you can define access to specific fields and types across your supergraph: - The [`@authenticated`](#authenticated) directive works in a binary way: authenticated requests can access the specified field or type, and unauthenticated requests can't. -- The [`@requiresScopes`](#requiresscopes) directive allows granular access control through the scopes you define. For example, you may declare that querying a user's information requires a `read:user` scope, while mutating a user's information requires an `update:user` scope. You can require scopes on individual fields. +- The [`@requiresScopes`](#requiresscopes) directive allows granular access control through the scopes you define. + +For example, imagine you're building a social media platform that includes a `Users` service. You can use the [`@requiresScopes`](#requiresscopes) directive to declare that viewing other users' information requires the `read:user` scope. You can use the [`@authenticated`](#authenticated) directive to declare that users must be logged in to update their own information. And you can use both directives—together or separately—at the field level to fine-tune these access controls. The authorization directives use a request's **claims** to evaluate which fields and types are authorized. Claims are the individual details of a request's authentication and scope. They might include details like the ID of the associated user and any authorization scopes assigned to that user. @@ -65,7 +64,7 @@ The router then enforces these directives on all incoming requests. To provide the router with the claims it needs to evaluate authorization directives, you must either configure JSON Web Token (JWT) authentication or add an external coprocessor that adds claims to a request's context. In some cases (explained below), you may require both. - **JWT authentication configuration**: If you configure [JWT authentication](./authn-jwt), the Apollo Router [automatically adds a JWT token's claims](./authn-jwt#working-with-jwt-claims) to the request's context at the `apollo_authentication::JWT::claims` key. -- **Adding claims via coprocessor**: If you can't use JWT authentiation, you can [add claims with a coprocessor](/customizations/coprocessor#adding-authorization-claims-via-coprocessor). Coprocessors let you hook into the Apollo Router's request-handling lifecycle with custom code. +- **Adding claims via coprocessor**: If you can't use JWT authentication, you can [add claims with a coprocessor](/customizations/coprocessor#adding-authorization-claims-via-coprocessor). Coprocessors let you hook into the Apollo Router's request-handling lifecycle with custom code. - **Augmenting JWT claims via coprocessor**: Your authorization policies may require information beyond what your JSON web tokens provide. For example, a token's claims may include user IDs, which you then use to look up user roles. For situations like this, you can [augment the claims](./authn-jwt#claim-augmentation-via-coprocessors) from your JSON web tokens with coprocessors. ## Authorization directives @@ -74,10 +73,8 @@ To provide the router with the claims it needs to evaluate authorization directi 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. -If the key exists, it means the request is authenticated and the router executes the query in its entirety. - +If the key exists, it means the request is authenticated, and the router executes the query in its entirety. If the request is unauthenticated, the router removes `@authenticated` fields before planning the query and only executes the parts of the query that don't require authentication. -If every field in a particular subgraph's query is marked as requiring authentication, this can eliminate entire subgraph requests, thereby increasing router efficiency. #### Usage @@ -92,7 +89,7 @@ extend schema #### Example `@authenticated` use case -Suppose you're building a social media platform. Unauthenticated users can view a public post's title, author, and content. +Diving deeper into the [social media example](#how-it-works): let's say unauthenticated users can view a post's title, author, and content. However, you only want authenticated users to see the number of views a post has received. You also need to be able to query for an authenticated user's information. @@ -134,7 +131,7 @@ query { } ``` -The router would execute the entire query in an authenticated request. +The router would execute the entire query for an authenticated request. For an unauthenticated request, the router would remove the `@authenticated` fields and execute the filtered query. @@ -164,7 +161,7 @@ query { For an unauthenticated request, the router doesn't attempt to resolve the top-level `me` query, nor the views for the post with `id: "1234"`. The response retains the initial request's shape but returns `null` for unauthorized fields and applies the [standard GraphQL null propagation rules](https://www.apollographql.com/blog/graphql/basics/using-nullability-in-graphql/#what-happens-if-you-try-to-return-null-for-a-non-null-field). -```json title="Unauthenticated request's response" +```json title="Unauthenticated request response" { "data": { "me": null, @@ -252,8 +249,6 @@ fn router_service(service) {
-Like the efficiencies gained via the `@authenticated` directive, if every field on a particular subgraph query requires scopes that aren't present, this can eliminate entire subgraph requests. - #### Usage To use the `@requiresScopes` directive in a subgraph, you can [import it from the `@link` directive](/federation/federated-types/federated-directives/#importing-directives) like so: @@ -323,11 +318,11 @@ query { The response would include an error at the `/users/@/email` path since that field requires the `read:emails` scope. -If the request includes the `read:others read:emails` scope set, the router can execute the entire query successfully. +The router can execute the entire query successfully if the request includes the `read:others read:emails` scope set. ## Composition and federation -Authorization directives are defined at the subgraph level, and GraphOS composes them in the supergraph schema. In other words, if subgraph fields or types include `@authenticated` or `@requiresScopes` 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 `@authenticated` or `@requiresScopes` directives, they are set on the supergraph too. If multiple shared subgraph fields include `@requiresScopes`, the supergraph schema merges the sets of scopes required by each subgraph. For example, imagine one subgraph requires the `read:others` scope on the `users` query: @@ -353,6 +348,8 @@ type Query { } ``` +This accumulative approach ensures that each subgraph's restrictions are respected, even if they apply to the same type or field. + ### Authorization and `@key` fields The [`@key` directive](https://www.apollographql.com/docs/federation/entities/) lets you create an entity whose fields resolve across multiple subgraphs. @@ -413,7 +410,7 @@ This behavior resembles what you can create with [contracts](/graphos/delivery/c If a type [implementing an interface](https://www.apollographql.com/docs/apollo-server/schema/unions-interfaces/#interface-type) requires authorization, unauthorized requests can query the interface, but not any parts of the type that require authorization. -For example, consider this schema where the `Post` interface doesn't require authentication, but the `PrivateBlog` type, which implements `Post` does: +For example, consider this schema where the `Post` interface doesn't require authentication, but the `PrivateBlog` type, which implements `Post`, does: ```graphql type Query { @@ -472,17 +469,21 @@ query { The response would include an `"UNAUTHORIZED_FIELD_OR_TYPE"` error at the `/posts/@/allowedViewers` path. +## Query deduplication + +You can enable [query deduplication](../configuration/traffic-shaping/#query-deduplication) in the router to reduce redundant requests to a subgraph. The router does this by buffering similar queries and reusing the result. + +**Query deduplication takes authorization into account.** First, the router groups unauthenticated queries together. Then it groups authenticated queries by their required scope set. It uses these groups to execute queries as efficiently as possible when fulfilling requests. + ## Introspection -Authorization directives don't affect introspection; all fields that require authorization remain visible. However, directives applied to fields _aren't_ visible. If introspection might reveal too much information about internal types, then be sure to deactivate it: +The Apollo Router enforces authorization directives on queries at runtime. **Authorization directives don't affect introspection**; all fields that require authorization remain visible. However, directives applied to fields _aren't_ visible. If introspection might reveal too much information about internal types, then be sure it's deactivated in your [router configuration](./overview/#introspection): ```yaml supergraph: introspection: false ``` -You can also hide fields using [contracts](/graphos/delivery/contracts/). - -## Query deduplication +> Introspection is turned off in the router by default. -When [query deduplication](../configuration/traffic-shaping/#query-deduplication) is activated for subgraphs, the authorization status is accounted for: the router groups unauthenticated queries together, and authenticated queries are grouped by scope set. \ No newline at end of file +With introspection turned off ([as is best practice in production](https://www.apollographql.com/blog/graphql/security/why-you-should-disable-graphql-introspection-in-production/)), you can use GraphOS's [schema registry](/graphos/delivery/) to explore your supergraph schema and empower your teammates to do the same. From 2573b130afd591b2e982f1c3d6b5db3cca5347df Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Mon, 21 Aug 2023 15:39:53 -0600 Subject: [PATCH 68/82] More docs feedback applied --- docs/source/configuration/authorization.mdx | 361 +++++++++++--------- 1 file changed, 193 insertions(+), 168 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 8162eb6969..cea5141e14 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -8,192 +8,100 @@ minVersion: 1.27.0 -APIs provide access to business-critical data. Unrestricted access can result in data breaches, monetary losses, or potential denial of service. Even for internal APIs, checks can be essential to limit data to authorized parties. +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. -Enforcing authorization _in the Apollo Router_ is valuable for a few reasons: +Services may have their own access controls, but enforcing authorization _in the Apollo Router_ is valuable for a few reasons: - **Optimal query execution**: Validating authorization _before_ processing requests allows for early request termination. Stopping unauthorized requests at the edge of your graph reduces the load on your services and enhances performance. -```mermaid -flowchart LR; - clients(Client); - subgraph Router[" "] - router(["Apollo Router"]); - serviceB[Users
API]; - serviceC[Posts
API]; - end - router -.->|"❌ Subquery"| serviceB & serviceC; - clients -->|"⚠️Unauthorized
request"| router; -``` + ```mermaid + flowchart LR; + clients(Client); + subgraph Router[" "] + router(["Apollo Router"]); + serviceB[Users
API]; + serviceC[Posts
API]; + end + router -.->|"❌ Subquery"| serviceB & serviceC; + clients -->|"⚠️Unauthorized
request"| router; + ``` - If every field in a particular subquery requires authorization, the router's [query planner](../customizations/overview#request-path) can _eliminate entire subgraph requests_ for unauthorized requests. For example, a request may have permission to view a particular user's posts on a social media platform but not have permission to view any of that user's PII. -```mermaid -flowchart LR; - clients(Client); - subgraph Router[" "] - router(["Apollo Router"]); - serviceB[Users
API]; - serviceC[Posts
API]; - end - router -->|"✅ Authorized
subquery"| serviceC; - router -.->|"❌ Unauthorized
subquery"| serviceB; - clients -->|"⚠️ Partially authorized
request"| router; -``` + ```mermaid + flowchart LR; + clients(Client); + subgraph Router[" "] + router(["Apollo Router"]); + serviceB[Users
API]; + serviceC[Posts
API]; + end + router -->|"✅ Authorized
subquery"| serviceC; + router -.->|"❌ Unauthorized
subquery"| serviceB; + clients -->|"⚠️ Partially authorized
request"| router; + ``` + - [Query deduplication](./traffic-shaping/#query-deduplication) also accounts for authorization by grouping requested fields based on their required authorization. Entire groups can be eliminated from the query plan if they don't have the correct authorization. - **Declarative access rules**: You define access controls at the field level, and GraphOS [composes](#composition-and-federation) them across your services. These rules create graph-native governance without the need for an extra orchestration layer. -- **Principled architecture**: Through composition, the router centralizes authorization logic while allowing for auditing at the service level. [Query deduplication](./traffic-shaping/#query-deduplication) also accounts for authorization by grouping requested fields based on their required authorization. - -## How it works - -The Apollo Router provides access controls via **authorization directives**. Using the `@authenticated` and `@requiresScopes` directives, you can define access to specific fields and types across your supergraph: - -- The [`@authenticated`](#authenticated) directive works in a binary way: authenticated requests can access the specified field or type, and unauthenticated requests can't. -- The [`@requiresScopes`](#requiresscopes) directive allows granular access control through the scopes you define. - -For example, imagine you're building a social media platform that includes a `Users` service. You can use the [`@requiresScopes`](#requiresscopes) directive to declare that viewing other users' information requires the `read:user` scope. You can use the [`@authenticated`](#authenticated) directive to declare that users must be logged in to update their own information. And you can use both directives—together or separately—at the field level to fine-tune these access controls. - -The authorization directives use a request's **claims** to evaluate which fields and types are authorized. Claims are the individual details of a request's authentication and scope. They might include details like the ID of the associated user and any authorization scopes assigned to that user. - -You define and use authorization directives on fields and types in subgraph schemas, and GraphOS [composes](#composition-and-federation) them onto the supergraph schema. -The router then enforces these directives on all incoming requests. - -## Prerequisites - -To provide the router with the claims it needs to evaluate authorization directives, you must either configure JSON Web Token (JWT) authentication or add an external coprocessor that adds claims to a request's context. In some cases (explained below), you may require both. - -- **JWT authentication configuration**: If you configure [JWT authentication](./authn-jwt), the Apollo Router [automatically adds a JWT token's claims](./authn-jwt#working-with-jwt-claims) to the request's context at the `apollo_authentication::JWT::claims` key. -- **Adding claims via coprocessor**: If you can't use JWT authentication, you can [add claims with a coprocessor](/customizations/coprocessor#adding-authorization-claims-via-coprocessor). Coprocessors let you hook into the Apollo Router's request-handling lifecycle with custom code. -- **Augmenting JWT claims via coprocessor**: Your authorization policies may require information beyond what your JSON web tokens provide. For example, a token's claims may include user IDs, which you then use to look up user roles. For situations like this, you can [augment the claims](./authn-jwt#claim-augmentation-via-coprocessors) from your JSON web tokens with coprocessors. +- **Principled architecture**: Through composition, the router centralizes authorization logic while allowing for auditing at the service level. This centralized authorization is an initial checkpoint that other service layers can reinforce. -## Authorization directives + ```mermaid + flowchart LR; + clients(Clients); + Level2:::padding + subgraph Level1["
🔐 Router layer                                                   "] + router(["Apollo Router"]); + subgraph Level2["🔐 Service layer"] + serviceB[Users
API]; + serviceC[Posts
API]; + end + end -### `@authenticated` + router -->|"Subquery"| serviceB & serviceC; + clients -->|"Request"| router; -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. -If the key exists, it means the request is authenticated, and the router executes the query in its entirety. -If the request is unauthenticated, the router removes `@authenticated` fields before planning the query and only executes the parts of the query that don't require authentication. + classDef padding padding-left:1em, padding-right:1em + ``` -#### Usage - -To use the `@authenticated` 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.5", - import: [..., "@authenticated"]) -``` +## How it works -#### Example `@authenticated` use case +The Apollo Router provides access controls via **authorization directives**. Using the `@requiresScopes` and `@authenticated` directives, you can define access to specific fields and types across your supergraph: -Diving deeper into the [social media example](#how-it-works): let's say unauthenticated users can view a post's title, author, and content. -However, you only want authenticated users to see the number of views a post has received. -You also need to be able to query for an authenticated user's information. +- The [`@requiresScopes`](#requiresscopes) directive allows granular access control through the scopes you define. +- The [`@authenticated`](#authenticated) directive works in a binary way: authenticated requests can access the specified field or type, and unauthenticated requests can't. -Your schema may look something like this: +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: ```graphql type Query { - me: User @authenticated - post(id: ID!): Post -} - -type User { - id: ID! - username: String - posts: [Post!]! + users: [User!]! @requiresScopes(scopes: ["read:users"]) } - -type Post { - id: ID! - author: User! - title: String! - content: String! - views: Int @authenticated -} - ``` -Consider the following query: +You can use the [`@authenticated`](#authenticated) directive to declare that users must be logged in to update their own information: -```graphql title="Sample query" -query { - me { - username - } - post(id: "1234") { - title - views - } +```graphql +type Mutation { + updateUser(input: UpdateUserInput!): User! @authenticated } ``` -The router would execute the entire query for an authenticated request. -For an unauthenticated request, the router would remove the `@authenticated` fields and execute the filtered query. - - - -```graphql title="Query executed for an authenticated request" -query { - me { - username - } - post(id: "1234") { - title - views - } -} -``` +You can define both directives—together or separately—at the field level to fine-tune your access controls. +GraphOS [composes](#composition-and-federation) restrictions into the supergraph schema so that each subgraph's restrictions are respected. +The router then enforces these directives on all incoming requests. -```graphql title="Query executed for an unauthenticated request" -query { - post(id: "1234") { - title - } -} -``` +## Prerequisites - +The authorization directives use a request's **claims** to evaluate which fields and types are authorized. Claims are the individual details of a request's authentication and scope. They might include details like the ID of the user making the request and any authorization scopes assigned to that user. -For an unauthenticated request, the router doesn't attempt to resolve the top-level `me` query, nor the views for the post with `id: "1234"`. -The response retains the initial request's shape but returns `null` for unauthorized fields and applies the [standard GraphQL null propagation rules](https://www.apollographql.com/blog/graphql/basics/using-nullability-in-graphql/#what-happens-if-you-try-to-return-null-for-a-non-null-field). +To provide the router with the claims it needs to evaluate authorization directives, you must either configure JSON Web Token (JWT) authentication or add an external coprocessor that adds claims to a request's context. In some cases (explained below), you may require both. -```json title="Unauthenticated request response" -{ - "data": { - "me": null, - "post": { - "title": "Securing supergraphs", - } - }, - "errors": [ - { - "message": "Unauthorized field or type", - "path": [ - "me" - ], - "extensions": { - "code": "UNAUTHORIZED_FIELD_OR_TYPE" - } - }, - { - "message": "Unauthorized field or type", - "path": [ - "post", - "views" - ], - "extensions": { - "code": "UNAUTHORIZED_FIELD_OR_TYPE" - } - } - ] -} -``` +- **JWT authentication configuration**: If you configure [JWT authentication](./authn-jwt), the Apollo Router [automatically adds a JWT token's claims](./authn-jwt#working-with-jwt-claims) to the request's context at the `apollo_authentication::JWT::claims` key. +- **Adding claims via coprocessor**: If you can't use JWT authentication, you can [add claims with a coprocessor](/customizations/coprocessor#adding-authorization-claims-via-coprocessor). Coprocessors let you hook into the Apollo Router's request-handling lifecycle with custom code. +- **Augmenting JWT claims via coprocessor**: Your authorization policies may require information beyond what your JSON web tokens provide. For example, a token's claims may include user IDs, which you then use to look up user roles. For situations like this, you can [augment the claims](./authn-jwt#claim-augmentation-via-coprocessors) from your JSON web tokens with coprocessors. -If _every_ requested field requires authentication and a request is unauthenticated, the router generates an error indicating that the query is unauthorized. +## Authorization directives ### `@requiresScopes` @@ -265,9 +173,8 @@ extend schema Imagine the social media platform you're building lets users view other users' information only if they have the required permissions. Your schema may look something like this: -```graphql title="" +```graphql type Query { - me: User @authenticated user(id: ID!): User @requiresScopes(scopes: ["read:others"]) users: [User!]! @requiresScopes(scopes: ["read:others"]) post(id: ID!): Post @@ -286,7 +193,6 @@ type Post { author: User! title: String! content: String! - views: Int @authenticated } ``` @@ -317,9 +223,135 @@ 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. +If the key exists, it means the request is authenticated, and the router executes the query in its entirety. +If the request is unauthenticated, the router removes `@authenticated` fields before planning the query and only executes the parts of the query that don't require authentication. + +#### Usage + +To use the `@authenticated` 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.5", + import: [..., "@authenticated"]) +``` + +#### Example `@authenticated` use case + +Diving deeper into the social media example: let's say unauthenticated users can view a post's title, author, and content. +However, you only want authenticated users to see the number of views a post has received. +You also need to be able to query for an authenticated user's information. + +The relevant part of your schema may look something like this: + +```graphql +type Query { + me: User @authenticated + post(id: ID!): Post +} + +type User { + id: ID! + username: String + email: String @requiresScopes(scopes: ["read:email"]) + posts: [Post!]! +} + +type Post { + id: ID! + author: User! + title: String! + content: String! + views: Int @authenticated +} + +``` + +Consider the following query: + +```graphql title="Sample query" +query { + me { + username + } + post(id: "1234") { + title + views + } +} +``` + +The router would execute the entire query for an authenticated request. +For an unauthenticated request, the router would remove the `@authenticated` fields and execute the filtered query. + + + +```graphql title="Query executed for an authenticated request" +query { + me { + username + } + post(id: "1234") { + title + views + } +} +``` + +```graphql title="Query executed for an unauthenticated request" +query { + post(id: "1234") { + title + } +} +``` + + + +For an unauthenticated request, the router doesn't attempt to resolve the top-level `me` query, nor the views for the post with `id: "1234"`. +The response retains the initial request's shape but returns `null` for unauthorized fields and applies the [standard GraphQL null propagation rules](https://www.apollographql.com/blog/graphql/basics/using-nullability-in-graphql/#what-happens-if-you-try-to-return-null-for-a-non-null-field). + +```json title="Unauthenticated request response" +{ + "data": { + "me": null, + "post": { + "title": "Securing supergraphs", + } + }, + "errors": [ + { + "message": "Unauthorized field or type", + "path": [ + "me" + ], + "extensions": { + "code": "UNAUTHORIZED_FIELD_OR_TYPE" + } + }, + { + "message": "Unauthorized field or type", + "path": [ + "post", + "views" + ], + "extensions": { + "code": "UNAUTHORIZED_FIELD_OR_TYPE" + } + } + ] +} +``` + +If _every_ requested field requires authentication and a request is unauthenticated, the router generates an error indicating that the query is unauthorized. + ## 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 `@authenticated` or `@requiresScopes` directives, they are set on the supergraph too. @@ -473,17 +505,10 @@ The response would include an `"UNAUTHORIZED_FIELD_OR_TYPE"` error at the `/post You can enable [query deduplication](../configuration/traffic-shaping/#query-deduplication) in the router to reduce redundant requests to a subgraph. The router does this by buffering similar queries and reusing the result. -**Query deduplication takes authorization into account.** First, the router groups unauthenticated queries together. Then it groups authenticated queries by their required scope set. It uses these groups to execute queries as efficiently as possible when fulfilling requests. +**Query deduplication takes authorization into account.** First, the router groups unauthenticated queries together. Then it groups authenticated queries by their required scope set. It uses these groups to execute queries efficiently when fulfilling requests. ## Introspection -The Apollo Router enforces authorization directives on queries at runtime. **Authorization directives don't affect introspection**; all fields that require authorization remain visible. However, directives applied to fields _aren't_ visible. If introspection might reveal too much information about internal types, then be sure it's deactivated in your [router configuration](./overview/#introspection): - -```yaml -supergraph: - introspection: false -``` - -> Introspection is turned off in the router by default. +Introspection is turned off in the router by default, [as is best production practice](https://www.apollographql.com/blog/graphql/security/why-you-should-disable-graphql-introspection-in-production/). If you've chosen to [enable it](./overview/#introspection), keep in mind that **authorization directives don't affect introspection**. All fields that require authorization remain visible. However, directives applied to fields _aren't_ visible. If introspection might reveal too much information about internal types, then be sure it hasn't been enabled in your router configuration. -With introspection turned off ([as is best practice in production](https://www.apollographql.com/blog/graphql/security/why-you-should-disable-graphql-introspection-in-production/)), you can use GraphOS's [schema registry](/graphos/delivery/) to explore your supergraph schema and empower your teammates to do the same. +With introspection turned off, you can use GraphOS's [schema registry](/graphos/delivery/) to explore your supergraph schema and empower your teammates to do the same. If you want to completely remove fields from a graph rather than just preventing access (even with introspection on), consider building a [contract graph](/graphos/delivery/contracts/). From 24fcb3d685f7099f7bff12c6769d7559eb346eeb Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Tue, 22 Aug 2023 09:25:12 +0200 Subject: [PATCH 69/82] check the authorization status of implementors of an interface (#3588) 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 } } } ``` --- .../plugins/authorization/authenticated.rs | 317 +++++++++++++- .../src/plugins/authorization/policy.rs | 373 ++++++++++++++++- .../src/plugins/authorization/scopes.rs | 394 +++++++++++++++++- ...thenticated__tests__interface_field-2.snap | 16 + ...thenticated__tests__interface_field-3.snap | 17 + ...thenticated__tests__interface_field-4.snap | 19 + ...authenticated__tests__interface_field.snap | 11 + ...uthenticated__tests__interface_type-2.snap | 13 + ...uthenticated__tests__interface_type-3.snap | 13 + ...uthenticated__tests__interface_type-4.snap | 16 + ..._authenticated__tests__interface_type.snap | 8 + ...enticated__tests__query_field_alias-2.snap | 13 + ...thenticated__tests__query_field_alias.snap | 10 + ...zation__authenticated__tests__union-2.snap | 16 + ...rization__authenticated__tests__union.snap | 13 + ...ion__policy__tests__interface_field-2.snap | 16 + ...ion__policy__tests__interface_field-3.snap | 16 + ...ion__policy__tests__interface_field-4.snap | 32 ++ ...ation__policy__tests__interface_field.snap | 11 + ...ion__policy__tests__interface_type-10.snap | 16 + ...tion__policy__tests__interface_type-2.snap | 13 + ...tion__policy__tests__interface_type-3.snap | 8 + ...tion__policy__tests__interface_type-4.snap | 13 + ...tion__policy__tests__interface_type-5.snap | 8 + ...tion__policy__tests__interface_type-6.snap | 13 + ...tion__policy__tests__interface_type-7.snap | 8 + ...tion__policy__tests__interface_type-8.snap | 26 ++ ...tion__policy__tests__interface_type-9.snap | 13 + ...zation__policy__tests__interface_type.snap | 8 + ...n__policy__tests__query_field_alias-2.snap | 10 + ...n__policy__tests__query_field_alias-3.snap | 13 + ...ion__policy__tests__query_field_alias.snap | 9 + ...authorization__policy__tests__union-2.snap | 16 + ...__authorization__policy__tests__union.snap | 13 + ...ion__scopes__tests__interface_field-2.snap | 16 + ...ion__scopes__tests__interface_field-3.snap | 16 + ...ion__scopes__tests__interface_field-4.snap | 32 ++ ...ation__scopes__tests__interface_field.snap | 11 + ...ion__scopes__tests__interface_type-10.snap | 16 + ...tion__scopes__tests__interface_type-2.snap | 13 + ...tion__scopes__tests__interface_type-3.snap | 8 + ...tion__scopes__tests__interface_type-4.snap | 13 + ...tion__scopes__tests__interface_type-5.snap | 8 + ...tion__scopes__tests__interface_type-6.snap | 13 + ...tion__scopes__tests__interface_type-7.snap | 8 + ...tion__scopes__tests__interface_type-8.snap | 26 ++ ...tion__scopes__tests__interface_type-9.snap | 13 + ...zation__scopes__tests__interface_type.snap | 8 + ...n__scopes__tests__query_field_alias-2.snap | 10 + ...n__scopes__tests__query_field_alias-3.snap | 13 + ...ion__scopes__tests__query_field_alias.snap | 9 + ...authorization__scopes__tests__union-2.snap | 16 + ...__authorization__scopes__tests__union.snap | 13 + 53 files changed, 1730 insertions(+), 43 deletions(-) create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_field-2.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_field-3.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_field-4.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_field.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_type-2.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_type-3.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_type-4.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_type.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__query_field_alias-2.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__query_field_alias.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__union-2.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__union.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_field-2.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_field-3.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_field-4.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_field.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-10.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-2.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-3.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-4.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-5.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-6.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-7.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-8.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-9.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__query_field_alias-2.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__query_field_alias-3.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__query_field_alias.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__union-2.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__union.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_field-2.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_field-3.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_field-4.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_field.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-10.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-2.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-3.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-4.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-5.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-6.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-7.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-8.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-9.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field_alias-2.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field_alias-3.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field_alias.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__union-2.snap create mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__union.snap diff --git a/apollo-router/src/plugins/authorization/authenticated.rs b/apollo-router/src/plugins/authorization/authenticated.rs index e82dc0f227..981940d0fc 100644 --- a/apollo-router/src/plugins/authorization/authenticated.rs +++ b/apollo-router/src/plugins/authorization/authenticated.rs @@ -11,6 +11,7 @@ use tower::BoxError; use crate::json_ext::Path; use crate::json_ext::PathElement; use crate::spec::query::transform; +use crate::spec::query::transform::get_field_type; pub(crate) const AUTHENTICATED_DIRECTIVE_NAME: &str = "authenticated"; @@ -47,6 +48,100 @@ impl<'a> AuthenticatedVisitor<'a> { fn is_type_authenticated(&self, t: &TypeDefinition) -> bool { t.directive_by_name(AUTHENTICATED_DIRECTIVE_NAME).is_some() } + + fn implementors_with_different_requirements( + &self, + parent_type: &str, + node: &hir::Field, + ) -> bool { + // if all selections under the interface field are fragments with type conditions + // then we don't need to check that they have the same authorization requirements + if node.selection_set().fields().is_empty() { + return false; + } + + if let Some(type_definition) = get_field_type(self, parent_type, node.name()) + .and_then(|ty| self.compiler.db.find_type_definition_by_name(ty)) + { + if self.implementors_with_different_type_requirements(&type_definition) { + return true; + } + } + false + } + + fn implementors_with_different_type_requirements(&self, t: &TypeDefinition) -> bool { + if t.is_interface_type_definition() { + let mut is_authenticated: Option = None; + + for ty in self + .compiler + .db + .subtype_map() + .get(t.name()) + .into_iter() + .flatten() + .cloned() + .filter_map(|ty| self.compiler.db.find_type_definition_by_name(ty)) + { + let ty_is_authenticated = + ty.directive_by_name(AUTHENTICATED_DIRECTIVE_NAME).is_some(); + match is_authenticated { + None => is_authenticated = Some(ty_is_authenticated), + Some(other_ty_is_authenticated) => { + if ty_is_authenticated != other_ty_is_authenticated { + return true; + } + } + } + } + } + + false + } + + fn implementors_with_different_field_requirements( + &self, + parent_type: &str, + field: &hir::Field, + ) -> bool { + if let Some(t) = self + .compiler + .db + .find_type_definition_by_name(parent_type.to_string()) + { + if t.is_interface_type_definition() { + let mut is_authenticated: Option = None; + + for ty in self + .compiler + .db + .subtype_map() + .get(t.name()) + .into_iter() + .flatten() + .cloned() + .filter_map(|ty| self.compiler.db.find_type_definition_by_name(ty)) + { + if let Some(f) = ty.field(&self.compiler.db, field.name()) { + let field_is_authenticated = + f.directive_by_name(AUTHENTICATED_DIRECTIVE_NAME).is_some(); + match is_authenticated { + Some(other) => { + if field_is_authenticated != other { + return true; + } + } + _ => { + is_authenticated = Some(field_is_authenticated); + } + } + } + } + } + } + false + } } impl<'a> transform::Visitor for AuthenticatedVisitor<'a> { @@ -81,7 +176,16 @@ impl<'a> transform::Visitor for AuthenticatedVisitor<'a> { self.current_path.push(PathElement::Flatten); } - let res = if field_requires_authentication { + let implementors_with_different_requirements = + self.implementors_with_different_requirements(parent_type, node); + + let implementors_with_different_field_requirements = + self.implementors_with_different_field_requirements(parent_type, node); + + let res = if field_requires_authentication + || implementors_with_different_requirements + || implementors_with_different_field_requirements + { self.unauthorized_paths.push(self.current_path.clone()); self.query_requires_authentication = true; Ok(None) @@ -248,10 +352,11 @@ mod tests { } "#; - fn filter(query: &str) -> (apollo_encoder::Document, Vec) { + #[track_caller] + fn filter(schema: &str, query: &str) -> (apollo_encoder::Document, Vec) { let mut compiler = ApolloCompiler::new(); - let _schema_id = compiler.add_type_system(BASIC_SCHEMA, "schema.graphql"); + let _schema_id = compiler.add_type_system(schema, "schema.graphql"); let file_id = compiler.add_executable(query, "query.graphql"); let diagnostics = compiler.validate(); @@ -278,7 +383,7 @@ mod tests { } "#; - let (doc, paths) = filter(QUERY); + let (doc, paths) = filter(BASIC_SCHEMA, QUERY); insta::assert_display_snapshot!(doc); insta::assert_debug_snapshot!(paths); @@ -298,7 +403,27 @@ mod tests { } "#; - let (doc, paths) = filter(QUERY); + let (doc, paths) = filter(BASIC_SCHEMA, QUERY); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + } + + #[test] + fn query_field_alias() { + static QUERY: &str = r#" + query { + topProducts { + type + } + + moi: me { + name + } + } + "#; + + let (doc, paths) = filter(BASIC_SCHEMA, QUERY); insta::assert_display_snapshot!(doc); insta::assert_debug_snapshot!(paths); @@ -315,7 +440,7 @@ mod tests { } "#; - let (doc, paths) = filter(QUERY); + let (doc, paths) = filter(BASIC_SCHEMA, QUERY); insta::assert_display_snapshot!(doc); insta::assert_debug_snapshot!(paths); @@ -337,7 +462,7 @@ mod tests { } "#; - let (doc, paths) = filter(QUERY); + let (doc, paths) = filter(BASIC_SCHEMA, QUERY); insta::assert_display_snapshot!(doc); insta::assert_debug_snapshot!(paths); @@ -359,7 +484,7 @@ mod tests { } "#; - let (doc, paths) = filter(QUERY); + let (doc, paths) = filter(BASIC_SCHEMA, QUERY); insta::assert_display_snapshot!(doc); insta::assert_debug_snapshot!(paths); @@ -383,7 +508,7 @@ mod tests { } "#; - let (doc, paths) = filter(QUERY); + let (doc, paths) = filter(BASIC_SCHEMA, QUERY); insta::assert_display_snapshot!(doc); insta::assert_debug_snapshot!(paths); @@ -403,7 +528,7 @@ mod tests { } "#; - let (doc, paths) = filter(QUERY); + let (doc, paths) = filter(BASIC_SCHEMA, QUERY); insta::assert_display_snapshot!(doc); insta::assert_debug_snapshot!(paths); @@ -426,7 +551,177 @@ mod tests { } "#; - let (doc, paths) = filter(QUERY); + let (doc, paths) = filter(BASIC_SCHEMA, QUERY); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + } + + static INTERFACE_SCHEMA: &str = r#" + directive @authenticated on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM + directive @defer on INLINE_FRAGMENT | FRAGMENT_SPREAD + + 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 + } + "#; + + #[test] + fn interface_type() { + static QUERY: &str = r#" + query { + test + itf { + id + } + } + "#; + + let (doc, paths) = filter(INTERFACE_SCHEMA, QUERY); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + + static QUERY2: &str = r#" + query { + test + itf { + ... on A { + id + } + + ... on B { + id + } + } + } + "#; + + let (doc, paths) = filter(INTERFACE_SCHEMA, QUERY2); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + } + + static INTERFACE_FIELD_SCHEMA: &str = r#" + directive @authenticated on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM + directive @defer on INLINE_FRAGMENT | FRAGMENT_SPREAD + + type Query { + test: String + itf: I! + } + + interface I { + id: ID + other: String + } + + type A implements I { + id: ID + other: String + a: String + } + + type B implements I { + id: ID @authenticated + other: String + b: String + } + "#; + + #[test] + fn interface_field() { + static QUERY: &str = r#" + query { + test + itf { + id + other + } + } + "#; + + let (doc, paths) = filter(INTERFACE_FIELD_SCHEMA, QUERY); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + + static QUERY2: &str = r#" + query { + test + itf { + ... on A { + id + other + } + + ... on B { + id + other + } + } + } + "#; + + let (doc, paths) = filter(INTERFACE_FIELD_SCHEMA, QUERY2); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + } + + #[test] + fn union() { + static UNION_MEMBERS_SCHEMA: &str = r#" + directive @authenticated on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM + directive @defer on INLINE_FRAGMENT | FRAGMENT_SPREAD + + type Query { + test: String + uni: I! + } + + union I = A | B + + type A { + id: ID + } + + type B @authenticated { + id: ID + } + "#; + + static QUERY: &str = r#" + query { + test + uni { + ... on A { + id + } + ... on B { + id + } + } + } + "#; + + let (doc, paths) = filter(UNION_MEMBERS_SCHEMA, QUERY); insta::assert_display_snapshot!(doc); insta::assert_debug_snapshot!(paths); diff --git a/apollo-router/src/plugins/authorization/policy.rs b/apollo-router/src/plugins/authorization/policy.rs index d85df9c390..325c7a0083 100644 --- a/apollo-router/src/plugins/authorization/policy.rs +++ b/apollo-router/src/plugins/authorization/policy.rs @@ -1,5 +1,10 @@ //! Authorization plugin - +//! +//! Implementation of the `@policy` directive: +//! +//! ```graphql +//! directive @policy(policies: [String!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM +//! ``` use std::collections::HashSet; use apollo_compiler::hir; @@ -14,6 +19,7 @@ use tower::BoxError; use crate::json_ext::Path; use crate::json_ext::PathElement; use crate::spec::query::transform; +use crate::spec::query::transform::get_field_type; use crate::spec::query::traverse; pub(crate) struct PolicyExtractionVisitor<'a> { @@ -194,6 +200,122 @@ impl<'a> PolicyFilteringVisitor<'a> { .next() .is_some() } + + fn implementors_with_different_requirements( + &self, + parent_type: &str, + node: &hir::Field, + ) -> bool { + // if all selections under the interface field are fragments with type conditions + // then we don't need to check that they have the same authorization requirements + if node.selection_set().fields().is_empty() { + return false; + } + + if let Some(type_definition) = get_field_type(self, parent_type, node.name()) + .and_then(|ty| self.compiler.db.find_type_definition_by_name(ty)) + { + if self.implementors_with_different_type_requirements(&type_definition) { + return true; + } + } + false + } + + fn implementors_with_different_type_requirements(&self, t: &TypeDefinition) -> bool { + if t.is_interface_type_definition() { + let mut policies: Option> = None; + + for ty in self + .compiler + .db + .subtype_map() + .get(t.name()) + .into_iter() + .flatten() + .cloned() + .filter_map(|ty| self.compiler.db.find_type_definition_by_name(ty)) + { + // aggregate the list of scope sets + // we transform to a common representation of sorted vectors because the element order + // of hashsets is not stable + let field_policies = ty + .directive_by_name(POLICY_DIRECTIVE_NAME) + .map(|directive| { + let mut v = policy_argument(Some(directive)) + .cloned() + .collect::>(); + v.sort(); + v + }) + .unwrap_or_default(); + + match &policies { + None => policies = Some(field_policies), + Some(other_policies) => { + if field_policies != *other_policies { + return true; + } + } + } + } + } + + false + } + + fn implementors_with_different_field_requirements( + &self, + parent_type: &str, + field: &hir::Field, + ) -> bool { + if let Some(t) = self + .compiler + .db + .find_type_definition_by_name(parent_type.to_string()) + { + if t.is_interface_type_definition() { + let mut policies: Option> = None; + + for ty in self + .compiler + .db + .subtype_map() + .get(t.name()) + .into_iter() + .flatten() + .cloned() + .filter_map(|ty| self.compiler.db.find_type_definition_by_name(ty)) + { + if let Some(f) = ty.field(&self.compiler.db, field.name()) { + // aggregate the list of scope sets + // we transform to a common representation of sorted vectors because the element order + // of hashsets is not stable + let field_policies = f + .directive_by_name(POLICY_DIRECTIVE_NAME) + .map(|directive| { + let mut v = policy_argument(Some(directive)) + .cloned() + .collect::>(); + v.sort(); + v + }) + .unwrap_or_default(); + + match &policies { + None => policies = Some(field_policies), + Some(other_policies) => { + if field_policies != *other_policies { + return true; + } + } + } + } + } + } + } + false + } } impl<'a> transform::Visitor for PolicyFilteringVisitor<'a> { @@ -225,18 +347,24 @@ impl<'a> transform::Visitor for PolicyFilteringVisitor<'a> { } }); + let implementors_with_different_requirements = + self.implementors_with_different_requirements(parent_type, node); + + let implementors_with_different_field_requirements = + self.implementors_with_different_field_requirements(parent_type, node); + self.current_path.push(PathElement::Key(field_name.into())); if is_field_list { self.current_path.push(PathElement::Flatten); } - if !is_authorized { - self.unauthorized_paths.push(self.current_path.clone()); - } - - let res = if is_authorized { + let res = if is_authorized + && !implementors_with_different_requirements + && !implementors_with_different_field_requirements + { transform::field(self, parent_type, node) } else { + self.unauthorized_paths.push(self.current_path.clone()); self.query_requires_policies = true; Ok(None) }; @@ -428,10 +556,11 @@ mod tests { insta::assert_debug_snapshot!(doc); } - fn filter(query: &str, policies: HashSet) -> (Document, Vec) { + #[track_caller] + fn filter(schema: &str, query: &str, policies: HashSet) -> (Document, Vec) { let mut compiler = ApolloCompiler::new(); - let _schema_id = compiler.add_type_system(BASIC_SCHEMA, "schema.graphql"); + let _schema_id = compiler.add_type_system(schema, "schema.graphql"); let file_id = compiler.add_executable(query, "query.graphql"); let diagnostics = compiler.validate(); @@ -466,11 +595,12 @@ mod tests { let doc = extract(QUERY); insta::assert_debug_snapshot!(doc); - let (doc, paths) = filter(QUERY, HashSet::new()); + let (doc, paths) = filter(BASIC_SCHEMA, QUERY, HashSet::new()); insta::assert_display_snapshot!(doc); insta::assert_debug_snapshot!(paths); let (doc, paths) = filter( + BASIC_SCHEMA, QUERY, ["profile".to_string(), "internal".to_string()] .into_iter() @@ -480,6 +610,7 @@ mod tests { insta::assert_debug_snapshot!(paths); let (doc, paths) = filter( + BASIC_SCHEMA, QUERY, [ "profile".to_string(), @@ -493,6 +624,7 @@ mod tests { insta::assert_debug_snapshot!(paths); let (doc, paths) = filter( + BASIC_SCHEMA, QUERY, [ "profile".to_string(), @@ -519,7 +651,7 @@ mod tests { let doc = extract(QUERY); insta::assert_debug_snapshot!(doc); - let (doc, paths) = filter(QUERY, HashSet::new()); + let (doc, paths) = filter(BASIC_SCHEMA, QUERY, HashSet::new()); insta::assert_display_snapshot!(doc); insta::assert_debug_snapshot!(paths); @@ -542,7 +674,30 @@ mod tests { let doc = extract(QUERY); insta::assert_debug_snapshot!(doc); - let (doc, paths) = filter(QUERY, HashSet::new()); + let (doc, paths) = filter(BASIC_SCHEMA, QUERY, HashSet::new()); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + } + + #[test] + fn query_field_alias() { + static QUERY: &str = r#" + query { + topProducts { + type + } + + moi: me { + name + } + } + "#; + + let doc = extract(QUERY); + insta::assert_debug_snapshot!(doc); + + let (doc, paths) = filter(BASIC_SCHEMA, QUERY, HashSet::new()); insta::assert_display_snapshot!(doc); insta::assert_debug_snapshot!(paths); @@ -562,7 +717,7 @@ mod tests { let doc = extract(QUERY); insta::assert_debug_snapshot!(doc); - let (doc, paths) = filter(QUERY, HashSet::new()); + let (doc, paths) = filter(BASIC_SCHEMA, QUERY, HashSet::new()); insta::assert_display_snapshot!(doc); insta::assert_debug_snapshot!(paths); @@ -587,7 +742,7 @@ mod tests { let doc = extract(QUERY); insta::assert_debug_snapshot!(doc); - let (doc, paths) = filter(QUERY, HashSet::new()); + let (doc, paths) = filter(BASIC_SCHEMA, QUERY, HashSet::new()); insta::assert_display_snapshot!(doc); insta::assert_debug_snapshot!(paths); @@ -612,7 +767,7 @@ mod tests { let doc = extract(QUERY); insta::assert_debug_snapshot!(doc); - let (doc, paths) = filter(QUERY, HashSet::new()); + let (doc, paths) = filter(BASIC_SCHEMA, QUERY, HashSet::new()); insta::assert_display_snapshot!(doc); insta::assert_debug_snapshot!(paths); @@ -639,12 +794,13 @@ mod tests { let doc = extract(QUERY); insta::assert_debug_snapshot!(doc); - let (doc, paths) = filter(QUERY, HashSet::new()); + let (doc, paths) = filter(BASIC_SCHEMA, QUERY, HashSet::new()); insta::assert_display_snapshot!(doc); insta::assert_debug_snapshot!(paths); let (doc, paths) = filter( + BASIC_SCHEMA, QUERY, ["read user".to_string(), "read username".to_string()] .into_iter() @@ -654,4 +810,191 @@ mod tests { insta::assert_display_snapshot!(doc); insta::assert_debug_snapshot!(paths); } + + static INTERFACE_SCHEMA: &str = r#" + directive @policy(policies: [String]) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM + directive @defer on INLINE_FRAGMENT | FRAGMENT_SPREAD + type Query { + test: String + itf: I! + } + interface I @policy(policies: ["itf"]) { + id: ID + } + type A implements I @policy(policies: ["a"]) { + id: ID + a: String + } + type B implements I @policy(policies: ["b"]) { + id: ID + b: String + } + "#; + + #[test] + fn interface_type() { + static QUERY: &str = r#" + query { + test + itf { + id + } + } + "#; + + let (doc, paths) = filter(INTERFACE_SCHEMA, QUERY, HashSet::new()); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + + let (doc, paths) = filter( + INTERFACE_SCHEMA, + QUERY, + ["itf".to_string()].into_iter().collect(), + ); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + + static QUERY2: &str = r#" + query { + test + itf { + ... on A { + id + } + ... on B { + id + } + } + } + "#; + + let (doc, paths) = filter(INTERFACE_SCHEMA, QUERY2, HashSet::new()); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + + let (doc, paths) = filter( + INTERFACE_SCHEMA, + QUERY2, + ["itf".to_string()].into_iter().collect(), + ); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + + let (doc, paths) = filter( + INTERFACE_SCHEMA, + QUERY2, + ["itf".to_string(), "a".to_string()].into_iter().collect(), + ); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + } + + static INTERFACE_FIELD_SCHEMA: &str = r#" + directive @policy(policies: [String]) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM + directive @defer on INLINE_FRAGMENT | FRAGMENT_SPREAD + type Query { + test: String + itf: I! + } + interface I { + id: ID + other: String + } + type A implements I { + id: ID @policy(policies: ["a"]) + other: String + a: String + } + type B implements I { + id: ID @policy(policies: ["b"]) + other: String + b: String + } + "#; + + #[test] + fn interface_field() { + static QUERY: &str = r#" + query { + test + itf { + id + other + } + } + "#; + + let (doc, paths) = filter(INTERFACE_FIELD_SCHEMA, QUERY, HashSet::new()); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + + static QUERY2: &str = r#" + query { + test + itf { + ... on A { + id + other + } + ... on B { + id + other + } + } + } + "#; + + let (doc, paths) = filter(INTERFACE_FIELD_SCHEMA, QUERY2, HashSet::new()); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + } + + #[test] + fn union() { + static UNION_MEMBERS_SCHEMA: &str = r#" + directive @policy(policies: [String]) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM + directive @defer on INLINE_FRAGMENT | FRAGMENT_SPREAD + type Query { + test: String + uni: I! + } + union I = A | B + type A @policy(policies: ["a"]) { + id: ID + } + type B @policy(policies: ["b"]) { + id: ID + } + "#; + + static QUERY: &str = r#" + query { + test + uni { + ... on A { + id + } + ... on B { + id + } + } + } + "#; + + let (doc, paths) = filter( + UNION_MEMBERS_SCHEMA, + QUERY, + ["a".to_string()].into_iter().collect(), + ); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + } } diff --git a/apollo-router/src/plugins/authorization/scopes.rs b/apollo-router/src/plugins/authorization/scopes.rs index a5c0eac532..1e4fcdb8db 100644 --- a/apollo-router/src/plugins/authorization/scopes.rs +++ b/apollo-router/src/plugins/authorization/scopes.rs @@ -1,6 +1,6 @@ //! Authorization plugin //! -//! Implementation of the `@policy` directive: +//! Implementation of the `@requiresScopes` directive: //! //! ```graphql //! directive @requiresScopes(scopes: [[String!]!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM @@ -19,6 +19,7 @@ use tower::BoxError; use crate::json_ext::Path; use crate::json_ext::PathElement; use crate::spec::query::transform; +use crate::spec::query::transform::get_field_type; use crate::spec::query::traverse; pub(crate) struct ScopeExtractionVisitor<'a> { @@ -241,6 +242,131 @@ impl<'a> ScopeFilteringVisitor<'a> { } } } + + fn implementors_with_different_requirements( + &self, + parent_type: &str, + node: &hir::Field, + ) -> bool { + // if all selections under the interface field are fragments with type conditions + // then we don't need to check that they have the same authorization requirements + if node.selection_set().fields().is_empty() { + return false; + } + + if let Some(type_definition) = get_field_type(self, parent_type, node.name()) + .and_then(|ty| self.compiler.db.find_type_definition_by_name(ty)) + { + if self.implementors_with_different_type_requirements(&type_definition) { + return true; + } + } + false + } + + fn implementors_with_different_type_requirements(&self, t: &TypeDefinition) -> bool { + if t.is_interface_type_definition() { + let mut scope_sets = None; + + for ty in self + .compiler + .db + .subtype_map() + .get(t.name()) + .into_iter() + .flatten() + .cloned() + .filter_map(|ty| self.compiler.db.find_type_definition_by_name(ty)) + { + // aggregate the list of scope sets + // we transform to a common representation of sorted vectors because the element order + // of hashsets is not stable + let ty_scope_sets = ty + .directive_by_name(REQUIRES_SCOPES_DIRECTIVE_NAME) + .map(|directive| { + let mut v = scopes_sets_argument(directive) + .map(|h| { + let mut v = h.into_iter().collect::>(); + v.sort(); + v + }) + .collect::>(); + v.sort(); + v + }) + .unwrap_or_default(); + + match &scope_sets { + None => scope_sets = Some(ty_scope_sets), + Some(other_scope_sets) => { + if ty_scope_sets != *other_scope_sets { + return true; + } + } + } + } + } + + false + } + + fn implementors_with_different_field_requirements( + &self, + parent_type: &str, + field: &hir::Field, + ) -> bool { + if let Some(t) = self + .compiler + .db + .find_type_definition_by_name(parent_type.to_string()) + { + if t.is_interface_type_definition() { + let mut scope_sets = None; + + for ty in self + .compiler + .db + .subtype_map() + .get(t.name()) + .into_iter() + .flatten() + .cloned() + .filter_map(|ty| self.compiler.db.find_type_definition_by_name(ty)) + { + if let Some(f) = ty.field(&self.compiler.db, field.name()) { + // aggregate the list of scope sets + // we transform to a common representation of sorted vectors because the element order + // of hashsets is not stable + let field_scope_sets = f + .directive_by_name(REQUIRES_SCOPES_DIRECTIVE_NAME) + .map(|directive| { + let mut v = scopes_sets_argument(directive) + .map(|h| { + let mut v = h.into_iter().collect::>(); + v.sort(); + v + }) + .collect::>(); + v.sort(); + v + }) + .unwrap_or_default(); + + match &scope_sets { + None => scope_sets = Some(field_scope_sets), + Some(other_scope_sets) => { + if field_scope_sets != *other_scope_sets { + return true; + } + } + } + } + } + } + } + + false + } } impl<'a> transform::Visitor for ScopeFilteringVisitor<'a> { @@ -254,6 +380,7 @@ impl<'a> transform::Visitor for ScopeFilteringVisitor<'a> { node: &hir::Field, ) -> Result, BoxError> { let field_name = node.name(); + let mut is_field_list = false; let is_authorized = self @@ -272,18 +399,24 @@ impl<'a> transform::Visitor for ScopeFilteringVisitor<'a> { } }); + let implementors_with_different_requirements = + self.implementors_with_different_requirements(parent_type, node); + + let implementors_with_different_field_requirements = + self.implementors_with_different_field_requirements(parent_type, node); + self.current_path.push(PathElement::Key(field_name.into())); if is_field_list { self.current_path.push(PathElement::Flatten); } - if !is_authorized { - self.unauthorized_paths.push(self.current_path.clone()); - } - - let res = if is_authorized { + let res = if is_authorized + && !implementors_with_different_requirements + && !implementors_with_different_field_requirements + { transform::field(self, parent_type, node) } else { + self.unauthorized_paths.push(self.current_path.clone()); self.query_requires_scopes = true; Ok(None) }; @@ -481,10 +614,11 @@ mod tests { insta::assert_debug_snapshot!(doc); } - fn filter(query: &str, scopes: HashSet) -> (Document, Vec) { + #[track_caller] + fn filter(schema: &str, query: &str, scopes: HashSet) -> (Document, Vec) { let mut compiler = ApolloCompiler::new(); - let _schema_id = compiler.add_type_system(BASIC_SCHEMA, "schema.graphql"); + let _schema_id = compiler.add_type_system(schema, "schema.graphql"); let file_id = compiler.add_executable(query, "query.graphql"); let diagnostics = compiler.validate(); @@ -519,11 +653,12 @@ mod tests { let doc = extract(QUERY); insta::assert_debug_snapshot!(doc); - let (doc, paths) = filter(QUERY, HashSet::new()); + let (doc, paths) = filter(BASIC_SCHEMA, QUERY, HashSet::new()); insta::assert_display_snapshot!(doc); insta::assert_debug_snapshot!(paths); let (doc, paths) = filter( + BASIC_SCHEMA, QUERY, ["profile".to_string(), "internal".to_string()] .into_iter() @@ -533,6 +668,7 @@ mod tests { insta::assert_debug_snapshot!(paths); let (doc, paths) = filter( + BASIC_SCHEMA, QUERY, [ "profile".to_string(), @@ -547,6 +683,7 @@ mod tests { insta::assert_debug_snapshot!(paths); let (doc, paths) = filter( + BASIC_SCHEMA, QUERY, [ "profile".to_string(), @@ -573,7 +710,7 @@ mod tests { let doc = extract(QUERY); insta::assert_debug_snapshot!(doc); - let (doc, paths) = filter(QUERY, HashSet::new()); + let (doc, paths) = filter(BASIC_SCHEMA, QUERY, HashSet::new()); insta::assert_display_snapshot!(doc); insta::assert_debug_snapshot!(paths); @@ -596,7 +733,30 @@ mod tests { let doc = extract(QUERY); insta::assert_debug_snapshot!(doc); - let (doc, paths) = filter(QUERY, HashSet::new()); + let (doc, paths) = filter(BASIC_SCHEMA, QUERY, HashSet::new()); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + } + + #[test] + fn query_field_alias() { + static QUERY: &str = r#" + query { + topProducts { + type + } + + moi: me { + name + } + } + "#; + + let doc = extract(QUERY); + insta::assert_debug_snapshot!(doc); + + let (doc, paths) = filter(BASIC_SCHEMA, QUERY, HashSet::new()); insta::assert_display_snapshot!(doc); insta::assert_debug_snapshot!(paths); @@ -616,7 +776,7 @@ mod tests { let doc = extract(QUERY); insta::assert_debug_snapshot!(doc); - let (doc, paths) = filter(QUERY, HashSet::new()); + let (doc, paths) = filter(BASIC_SCHEMA, QUERY, HashSet::new()); insta::assert_display_snapshot!(doc); insta::assert_debug_snapshot!(paths); @@ -641,7 +801,7 @@ mod tests { let doc = extract(QUERY); insta::assert_debug_snapshot!(doc); - let (doc, paths) = filter(QUERY, HashSet::new()); + let (doc, paths) = filter(BASIC_SCHEMA, QUERY, HashSet::new()); insta::assert_display_snapshot!(doc); insta::assert_debug_snapshot!(paths); @@ -667,17 +827,22 @@ mod tests { let doc = extract(QUERY); insta::assert_debug_snapshot!(doc); - let (doc, paths) = filter(QUERY, HashSet::new()); + let (doc, paths) = filter(BASIC_SCHEMA, QUERY, HashSet::new()); insta::assert_display_snapshot!(doc); insta::assert_debug_snapshot!(paths); - let (doc, paths) = filter(QUERY, ["read:user".to_string()].into_iter().collect()); + let (doc, paths) = filter( + BASIC_SCHEMA, + QUERY, + ["read:user".to_string()].into_iter().collect(), + ); insta::assert_display_snapshot!(doc); insta::assert_debug_snapshot!(paths); let (doc, paths) = filter( + BASIC_SCHEMA, QUERY, ["read:user".to_string(), "read:username".to_string()] .into_iter() @@ -710,17 +875,22 @@ mod tests { let doc = extract(QUERY); insta::assert_debug_snapshot!(doc); - let (doc, paths) = filter(QUERY, HashSet::new()); + let (doc, paths) = filter(BASIC_SCHEMA, QUERY, HashSet::new()); insta::assert_display_snapshot!(doc); insta::assert_debug_snapshot!(paths); - let (doc, paths) = filter(QUERY, ["read:user".to_string()].into_iter().collect()); + let (doc, paths) = filter( + BASIC_SCHEMA, + QUERY, + ["read:user".to_string()].into_iter().collect(), + ); insta::assert_display_snapshot!(doc); insta::assert_debug_snapshot!(paths); let (doc, paths) = filter( + BASIC_SCHEMA, QUERY, ["read:user".to_string(), "read:username".to_string()] .into_iter() @@ -730,4 +900,194 @@ mod tests { insta::assert_display_snapshot!(doc); insta::assert_debug_snapshot!(paths); } + + static INTERFACE_SCHEMA: &str = r#" + directive @requiresScopes(scopes: [[String!]!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM + directive @defer on INLINE_FRAGMENT | FRAGMENT_SPREAD + type Query { + test: String + itf: I! + } + interface I @requiresScopes(scopes: [["itf"]]) { + id: ID + } + type A implements I @requiresScopes(scopes: [["a", "b"]]) { + id: ID + a: String + } + type B implements I @requiresScopes(scopes: [["c", "d"]]) { + id: ID + b: String + } + "#; + + #[test] + fn interface_type() { + static QUERY: &str = r#" + query { + test + itf { + id + } + } + "#; + + let (doc, paths) = filter(INTERFACE_SCHEMA, QUERY, HashSet::new()); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + + let (doc, paths) = filter( + INTERFACE_SCHEMA, + QUERY, + ["itf".to_string()].into_iter().collect(), + ); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + + static QUERY2: &str = r#" + query { + test + itf { + ... on A { + id + } + ... on B { + id + } + } + } + "#; + + let (doc, paths) = filter(INTERFACE_SCHEMA, QUERY2, HashSet::new()); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + + let (doc, paths) = filter( + INTERFACE_SCHEMA, + QUERY2, + ["itf".to_string()].into_iter().collect(), + ); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + + let (doc, paths) = filter( + INTERFACE_SCHEMA, + QUERY2, + ["itf".to_string(), "a".to_string(), "b".to_string()] + .into_iter() + .collect(), + ); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + } + + static INTERFACE_FIELD_SCHEMA: &str = r#" + directive @requiresScopes(scopes: [[String!]!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM + directive @defer on INLINE_FRAGMENT | FRAGMENT_SPREAD + type Query { + test: String + itf: I! + } + interface I { + id: ID + other: String + } + type A implements I { + id: ID @requiresScopes(scopes: [["a", "b"]]) + other: String + a: String + } + type B implements I { + id: ID @requiresScopes(scopes: [["c", "d"]]) + other: String + b: String + } + "#; + + #[test] + fn interface_field() { + static QUERY: &str = r#" + query { + test + itf { + id + other + } + } + "#; + + let (doc, paths) = filter(INTERFACE_FIELD_SCHEMA, QUERY, HashSet::new()); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + + static QUERY2: &str = r#" + query { + test + itf { + ... on A { + id + other + } + ... on B { + id + other + } + } + } + "#; + + let (doc, paths) = filter(INTERFACE_FIELD_SCHEMA, QUERY2, HashSet::new()); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + } + + #[test] + fn union() { + static UNION_MEMBERS_SCHEMA: &str = r#" + directive @requiresScopes(scopes: [[String!]!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM + + directive @defer on INLINE_FRAGMENT | FRAGMENT_SPREAD + type Query { + test: String + uni: I! + } + union I = A | B + type A @requiresScopes(scopes: [["a", "b"]]) { + id: ID + } + type B @requiresScopes(scopes: [["c", "d"]]) { + id: ID + } + "#; + + static QUERY: &str = r#" + query { + test + uni { + ... on A { + id + } + ... on B { + id + } + } + } + "#; + + let (doc, paths) = filter( + UNION_MEMBERS_SCHEMA, + QUERY, + ["a".to_string(), "b".to_string()].into_iter().collect(), + ); + + insta::assert_display_snapshot!(doc); + insta::assert_debug_snapshot!(paths); + } } diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_field-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_field-2.snap new file mode 100644 index 0000000000..2a6c558147 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_field-2.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: paths +--- +[ + Path( + [ + Key( + "itf", + ), + Key( + "id", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_field-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_field-3.snap new file mode 100644 index 0000000000..6d1de215cc --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_field-3.snap @@ -0,0 +1,17 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: doc +--- +query { + test + itf { + ... on A { + id + other + } + ... on B { + other + } + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_field-4.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_field-4.snap new file mode 100644 index 0000000000..3e5aa2cec7 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_field-4.snap @@ -0,0 +1,19 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: paths +--- +[ + Path( + [ + Key( + "itf", + ), + Fragment( + "B", + ), + Key( + "id", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_field.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_field.snap new file mode 100644 index 0000000000..78b312e1eb --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_field.snap @@ -0,0 +1,11 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: doc +--- +query { + test + itf { + other + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_type-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_type-2.snap new file mode 100644 index 0000000000..b178b224b5 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_type-2.snap @@ -0,0 +1,13 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: paths +--- +[ + Path( + [ + Key( + "itf", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_type-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_type-3.snap new file mode 100644 index 0000000000..7e609edb06 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_type-3.snap @@ -0,0 +1,13 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: doc +--- +query { + test + itf { + ... on A { + id + } + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_type-4.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_type-4.snap new file mode 100644 index 0000000000..2da68374d7 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_type-4.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: paths +--- +[ + Path( + [ + Key( + "itf", + ), + Fragment( + "B", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_type.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_type.snap new file mode 100644 index 0000000000..7fa88ccfa3 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_type.snap @@ -0,0 +1,8 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: doc +--- +query { + test +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__query_field_alias-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__query_field_alias-2.snap new file mode 100644 index 0000000000..4a1bcd42be --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__query_field_alias-2.snap @@ -0,0 +1,13 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: paths +--- +[ + Path( + [ + Key( + "me", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__query_field_alias.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__query_field_alias.snap new file mode 100644 index 0000000000..78ccae8dc0 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__query_field_alias.snap @@ -0,0 +1,10 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: doc +--- +query { + topProducts { + type + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__union-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__union-2.snap new file mode 100644 index 0000000000..1630a33f7e --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__union-2.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: paths +--- +[ + Path( + [ + Key( + "uni", + ), + Fragment( + "B", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__union.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__union.snap new file mode 100644 index 0000000000..03d23edc00 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__union.snap @@ -0,0 +1,13 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: doc +--- +query { + test + uni { + ... on A { + id + } + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_field-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_field-2.snap new file mode 100644 index 0000000000..b5c6f9ab16 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_field-2.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: paths +--- +[ + Path( + [ + Key( + "itf", + ), + Key( + "id", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_field-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_field-3.snap new file mode 100644 index 0000000000..2f4521d824 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_field-3.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: doc +--- +query { + test + itf { + ... on A { + other + } + ... on B { + other + } + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_field-4.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_field-4.snap new file mode 100644 index 0000000000..b99befdded --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_field-4.snap @@ -0,0 +1,32 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: paths +--- +[ + Path( + [ + Key( + "itf", + ), + Fragment( + "A", + ), + Key( + "id", + ), + ], + ), + Path( + [ + Key( + "itf", + ), + Fragment( + "B", + ), + Key( + "id", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_field.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_field.snap new file mode 100644 index 0000000000..86c5f1940d --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_field.snap @@ -0,0 +1,11 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: doc +--- +query { + test + itf { + other + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-10.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-10.snap new file mode 100644 index 0000000000..9a7a7257f6 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-10.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: paths +--- +[ + Path( + [ + Key( + "itf", + ), + Fragment( + "B", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-2.snap new file mode 100644 index 0000000000..9e4411de83 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-2.snap @@ -0,0 +1,13 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: paths +--- +[ + Path( + [ + Key( + "itf", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-3.snap new file mode 100644 index 0000000000..b78cca30ee --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-3.snap @@ -0,0 +1,8 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: doc +--- +query { + test +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-4.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-4.snap new file mode 100644 index 0000000000..9e4411de83 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-4.snap @@ -0,0 +1,13 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: paths +--- +[ + Path( + [ + Key( + "itf", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-5.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-5.snap new file mode 100644 index 0000000000..b78cca30ee --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-5.snap @@ -0,0 +1,8 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: doc +--- +query { + test +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-6.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-6.snap new file mode 100644 index 0000000000..9e4411de83 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-6.snap @@ -0,0 +1,13 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: paths +--- +[ + Path( + [ + Key( + "itf", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-7.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-7.snap new file mode 100644 index 0000000000..b78cca30ee --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-7.snap @@ -0,0 +1,8 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: doc +--- +query { + test +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-8.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-8.snap new file mode 100644 index 0000000000..9fa8269e3b --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-8.snap @@ -0,0 +1,26 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: paths +--- +[ + Path( + [ + Key( + "itf", + ), + Fragment( + "A", + ), + ], + ), + Path( + [ + Key( + "itf", + ), + Fragment( + "B", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-9.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-9.snap new file mode 100644 index 0000000000..5994e8567a --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-9.snap @@ -0,0 +1,13 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: doc +--- +query { + test + itf { + ... on A { + id + } + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type.snap new file mode 100644 index 0000000000..b78cca30ee --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type.snap @@ -0,0 +1,8 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: doc +--- +query { + test +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__query_field_alias-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__query_field_alias-2.snap new file mode 100644 index 0000000000..34750f1eca --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__query_field_alias-2.snap @@ -0,0 +1,10 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: doc +--- +query { + topProducts { + type + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__query_field_alias-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__query_field_alias-3.snap new file mode 100644 index 0000000000..9bdc7c6438 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__query_field_alias-3.snap @@ -0,0 +1,13 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: paths +--- +[ + Path( + [ + Key( + "me", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__query_field_alias.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__query_field_alias.snap new file mode 100644 index 0000000000..27ede2615b --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__query_field_alias.snap @@ -0,0 +1,9 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: doc +--- +{ + "profile", + "read user", + "read username", +} diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__union-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__union-2.snap new file mode 100644 index 0000000000..1e7094504b --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__union-2.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: paths +--- +[ + Path( + [ + Key( + "uni", + ), + Fragment( + "B", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__union.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__union.snap new file mode 100644 index 0000000000..e968662f05 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__union.snap @@ -0,0 +1,13 @@ +--- +source: apollo-router/src/plugins/authorization/policy.rs +expression: doc +--- +query { + test + uni { + ... on A { + id + } + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_field-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_field-2.snap new file mode 100644 index 0000000000..152fdb0782 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_field-2.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: paths +--- +[ + Path( + [ + Key( + "itf", + ), + Key( + "id", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_field-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_field-3.snap new file mode 100644 index 0000000000..9f8ecb6de2 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_field-3.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: doc +--- +query { + test + itf { + ... on A { + other + } + ... on B { + other + } + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_field-4.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_field-4.snap new file mode 100644 index 0000000000..2ccbf2a02b --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_field-4.snap @@ -0,0 +1,32 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: paths +--- +[ + Path( + [ + Key( + "itf", + ), + Fragment( + "A", + ), + Key( + "id", + ), + ], + ), + Path( + [ + Key( + "itf", + ), + Fragment( + "B", + ), + Key( + "id", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_field.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_field.snap new file mode 100644 index 0000000000..6bdb719301 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_field.snap @@ -0,0 +1,11 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: doc +--- +query { + test + itf { + other + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-10.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-10.snap new file mode 100644 index 0000000000..359df81d2d --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-10.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: paths +--- +[ + Path( + [ + Key( + "itf", + ), + Fragment( + "B", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-2.snap new file mode 100644 index 0000000000..a3f411de5c --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-2.snap @@ -0,0 +1,13 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: paths +--- +[ + Path( + [ + Key( + "itf", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-3.snap new file mode 100644 index 0000000000..dd76a2fa47 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-3.snap @@ -0,0 +1,8 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: doc +--- +query { + test +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-4.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-4.snap new file mode 100644 index 0000000000..a3f411de5c --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-4.snap @@ -0,0 +1,13 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: paths +--- +[ + Path( + [ + Key( + "itf", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-5.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-5.snap new file mode 100644 index 0000000000..dd76a2fa47 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-5.snap @@ -0,0 +1,8 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: doc +--- +query { + test +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-6.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-6.snap new file mode 100644 index 0000000000..a3f411de5c --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-6.snap @@ -0,0 +1,13 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: paths +--- +[ + Path( + [ + Key( + "itf", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-7.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-7.snap new file mode 100644 index 0000000000..dd76a2fa47 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-7.snap @@ -0,0 +1,8 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: doc +--- +query { + test +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-8.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-8.snap new file mode 100644 index 0000000000..57a148cce4 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-8.snap @@ -0,0 +1,26 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: paths +--- +[ + Path( + [ + Key( + "itf", + ), + Fragment( + "A", + ), + ], + ), + Path( + [ + Key( + "itf", + ), + Fragment( + "B", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-9.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-9.snap new file mode 100644 index 0000000000..b0254ec32d --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-9.snap @@ -0,0 +1,13 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: doc +--- +query { + test + itf { + ... on A { + id + } + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type.snap new file mode 100644 index 0000000000..dd76a2fa47 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type.snap @@ -0,0 +1,8 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: doc +--- +query { + test +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field_alias-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field_alias-2.snap new file mode 100644 index 0000000000..930db08570 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field_alias-2.snap @@ -0,0 +1,10 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: doc +--- +query { + topProducts { + type + } +} + diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field_alias-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field_alias-3.snap new file mode 100644 index 0000000000..70241af91d --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field_alias-3.snap @@ -0,0 +1,13 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: paths +--- +[ + Path( + [ + Key( + "me", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field_alias.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field_alias.snap new file mode 100644 index 0000000000..b2b2038103 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field_alias.snap @@ -0,0 +1,9 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: doc +--- +{ + "profile", + "read:user", + "read:username", +} diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__union-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__union-2.snap new file mode 100644 index 0000000000..92d3c89df9 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__union-2.snap @@ -0,0 +1,16 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: paths +--- +[ + Path( + [ + Key( + "uni", + ), + Fragment( + "B", + ), + ], + ), +] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__union.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__union.snap new file mode 100644 index 0000000000..61cb101b84 --- /dev/null +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__union.snap @@ -0,0 +1,13 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: doc +--- +query { + test + uni { + ... on A { + id + } + } +} + From 0e775d95f2a24824d990689b92724aa7556e3b16 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Tue, 22 Aug 2023 11:12:24 +0200 Subject: [PATCH 70/82] deactivate the policy directive for now --- .../src/plugins/authorization/mod.rs | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/apollo-router/src/plugins/authorization/mod.rs b/apollo-router/src/plugins/authorization/mod.rs index ab2f802520..3527e67b0d 100644 --- a/apollo-router/src/plugins/authorization/mod.rs +++ b/apollo-router/src/plugins/authorization/mod.rs @@ -138,19 +138,22 @@ impl AuthorizationPlugin { context.insert(REQUIRED_SCOPES_KEY, scopes).unwrap(); } - let mut visitor = PolicyExtractionVisitor::new(&compiler, file_id); - - // if this fails, the query is invalid and will fail at the query planning phase. - // We do not return validation errors here for now because that would imply a huge - // refactoring of telemetry and tests - if traverse::document(&mut visitor, file_id).is_ok() { - let policies: HashMap> = visitor - .extracted_policies - .into_iter() - .map(|policy| (policy, None)) - .collect(); - - context.insert(REQUIRED_POLICIES_KEY, policies).unwrap(); + // TODO: @policy is out of scope for preview, this will be reactivated later + if false { + let mut visitor = PolicyExtractionVisitor::new(&compiler, file_id); + + // if this fails, the query is invalid and will fail at the query planning phase. + // We do not return validation errors here for now because that would imply a huge + // refactoring of telemetry and tests + if traverse::document(&mut visitor, file_id).is_ok() { + let policies: HashMap> = visitor + .extracted_policies + .into_iter() + .map(|policy| (policy, None)) + .collect(); + + context.insert(REQUIRED_POLICIES_KEY, policies).unwrap(); + } } } From eb21b272f0a82466b43e95e3c7552c3ffbda9f37 Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Tue, 22 Aug 2023 09:16:39 -0600 Subject: [PATCH 71/82] Add docs for OR logic in @requireScopes --- docs/source/configuration/authorization.mdx | 96 +++++++++++++++++++-- 1 file changed, 89 insertions(+), 7 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index cea5141e14..2caae7f53b 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -26,7 +26,7 @@ Services may have their own access controls, but enforcing authorization _in the clients -->|"⚠️Unauthorized
request"| router; ``` - - If every field in a particular subquery requires authorization, the router's [query planner](../customizations/overview#request-path) can _eliminate entire subgraph requests_ for unauthorized requests. For example, a request may have permission to view a particular user's posts on a social media platform but not have permission to view any of that user's PII. + - If every field in a particular subquery requires authorization, the router's [query planner](../customizations/overview#request-path) can _eliminate entire subgraph requests_ for unauthorized requests. For example, a request may have permission to view a particular user's posts on a social media platform but not have permission to view any of that user's PII. Check out [How it works](#how-it-works) to learn more. ```mermaid flowchart LR; @@ -114,7 +114,7 @@ To declare which scopes are required, the directive should include a `scopes` ar Depending on the scopes present on the request, the router filters out unauthorized fields and types. -> If a field's required `scopes` array includes multiple scopes, the request must include _all_ required scopes to resolve it. +> You can use Boolean logic to define the required scopes. See [Combining required scopes](#combining-required-scopes-with-andor-logic) for details. The directive validates the required scopes by loading the claims object at the `apollo_authentication::JWT::claims` key in a request's context. The claims object's `scope` key's value should be a space-separated string of scopes in the format defined by the [OAuth2 RFC for access token scopes](https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). @@ -124,7 +124,7 @@ claims = context["apollo_authentication::JWT::claims"] claims["scope"] = "scope1 scope2 scope3" ``` - + If the `apollo_authentication::JWT::claims` object holds scopes in another format, for example, an array of strings, or at a key other than `"scope"`, you can edit the claims with a [Rhai script](../customizations/rhai). @@ -168,6 +168,33 @@ extend schema import: [..., "@requiresScopes"]) ``` +#### Combining required scopes with `AND`/`OR` logic + +A request must include _all_ elements in the top-level `scopes` array to resolve the associated field or type. In other words, the authorization validation uses **AND** logic between the elements in the top-level `scopes` array. + +```graphql +@requiresScopes(scopes: ["scope1", "scope2", "scope3"]) +``` + +For the preceding example, a request would need `scope1` **AND** `scope2` **AND** `scope3` to be authorized. + +You can use nested arrays to introduce **OR** logic: + +```graphql +@requiresScopes(scopes: [["scope1"], ["scope2"], ["scope3"]]) +``` + +For the preceding example, a request would need `scope1` **OR** `scope2` **OR** `scope3` to be authorized. + +You can nest arrays and elements as needed to achieve your desired logic. For example: + +```graphql +@requiresScopes(scopes: [["scope1", "scope2"], ["scope3"]]) +``` + +This syntax requires requests to have either (`scope1` **AND** `scope2`) **OR** just `scope3` to be authorized. + + #### Example `@requiresScopes` use case Imagine the social media platform you're building lets users view other users' information only if they have the required permissions. @@ -354,9 +381,42 @@ If _every_ requested field requires authentication and a request is unauthentica ## 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 `@authenticated` or `@requiresScopes` 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` or `@authenticated` directives, they are set on the supergraph too. + +#### Composition with `AND`/`OR` logic + +If shared subgraph fields include multiple directives, composition merges them. For example, suppose the `me` query requires `@authentication` in one subgraph: -If multiple shared subgraph fields include `@requiresScopes`, the supergraph schema merges the sets of scopes required by each subgraph. For example, imagine one subgraph requires the `read:others` scope on the `users` query: + +```graphql title="Subgraph A" +type Query { + me: User @authenticated +} + +type User { + id: ID! + username: String + email: String +} +``` + +and the `read:user` scope in another subgraph: + +```graphql title="Subgraph B" +type Query { + me: User @requiresScopes(scopes: ["read:user"]) +} + +type User { + id: ID! + username: String + email: String +} +``` + +A request would need to both be authenticated **AND** have the required scope. Recall that the `@authenticated` directive only checks for the existence of the `apollo_authentication::JWT::claims` key in a request's context, so authentication is guaranteed if the request includes scopes. + +If multiple shared subgraph fields include `@requiresScopes`, the supergraph schema merges them with the same logic used to [combine scopes for a single use of `@requiresScopes`](#combining-required-scopes-with-andor-logic). For example, if one subgraph requires the `read:others` scope on the `users` query: ```graphql title="Subgraph A" type Query { @@ -364,7 +424,7 @@ type Query { } ``` -And another subgraph requires the `read:profiles` scope on `users` query: +and another subgraph requires the `read:profiles` scope on `users` query: ```graphql title="Subgraph B" type Query { @@ -380,7 +440,29 @@ type Query { } ``` -This accumulative approach ensures that each subgraph's restrictions are respected, even if they apply to the same type or field. +As with [combining scopes for a single use of `@requiresScopes`](#combining-required-scopes-with-andor-logic), you can use nested arrays to introduce **OR** logic: + +```graphql title="Subgraph A" +type Query { + users: [User!]! @requiresScopes(scopes: [["read:others", "read:users"]]) +} +``` + +```graphql title="Subgraph B" +type Query { + users: [User!]! @requiresScopes(scopes: [["read:profiles"]]) +} +``` + +Since both `scopes` arrays are nested arrays, they would be composed using **OR** logic into the supergraph schema: + +```graphql title="Supergraph" +type Query { + users: [User!]! @requiresScopes(scopes: [["read:others", "read:users"], ["read:profiles"]]) +} +``` + +This syntax means a request needs either (`read:others` **AND** `read:users`) scopes **OR** just the `read:profiles` scope to be authorized. ### Authorization and `@key` fields From c32f84f5e83737883a3d3064a99a64907844c5a4 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Wed, 23 Aug 2023 11:39:46 +0200 Subject: [PATCH 72/82] refactor tests to reduce the number of snapshots now snapshots will display the query, filtered query and paths in one file --- .../plugins/authorization/authenticated.rs | 117 +++++-- .../src/plugins/authorization/scopes.rs | 291 ++++++++++++++---- ...zation__authenticated__tests__array-2.snap | 20 -- ...rization__authenticated__tests__array.snap | 18 +- ...zation__authenticated__tests__defer-2.snap | 19 -- ...rization__authenticated__tests__defer.snap | 16 +- ...thenticated__tests__interface_field-2.snap | 46 ++- ...thenticated__tests__interface_field-3.snap | 17 - ...thenticated__tests__interface_field-4.snap | 19 -- ...authenticated__tests__interface_field.snap | 14 +- ...nticated__tests__interface_fragment-2.snap | 16 - ...henticated__tests__interface_fragment.snap | 20 +- ...d__tests__interface_inline_fragment-2.snap | 16 - ...ted__tests__interface_inline_fragment.snap | 18 +- ...uthenticated__tests__interface_type-2.snap | 37 ++- ...uthenticated__tests__interface_type-3.snap | 13 - ...uthenticated__tests__interface_type-4.snap | 16 - ..._authenticated__tests__interface_type.snap | 13 +- ...ion__authenticated__tests__mutation-2.snap | 13 - ...ation__authenticated__tests__mutation.snap | 12 +- ...__authenticated__tests__query_field-2.snap | 13 - ...on__authenticated__tests__query_field.snap | 16 +- ...enticated__tests__query_field_alias-2.snap | 13 - ...thenticated__tests__query_field_alias.snap | 16 +- ...ation__authenticated__tests__scalar-2.snap | 16 - ...ization__authenticated__tests__scalar.snap | 13 +- ...ization__authenticated__tests__test-2.snap | 24 -- ...orization__authenticated__tests__test.snap | 19 +- ...zation__authenticated__tests__union-2.snap | 16 - ...rization__authenticated__tests__union.snap | 18 +- ...authorization__scopes__tests__array-2.snap | 10 - ...authorization__scopes__tests__array-3.snap | 17 - ...__authorization__scopes__tests__array.snap | 29 +- ...__scopes__tests__filter_basic_query-2.snap | 20 +- ...__scopes__tests__filter_basic_query-3.snap | 49 +-- ...__scopes__tests__filter_basic_query-4.snap | 24 +- ...__scopes__tests__filter_basic_query-5.snap | 23 -- ...__scopes__tests__filter_basic_query-6.snap | 14 - ...__scopes__tests__filter_basic_query-7.snap | 16 - ...__scopes__tests__filter_basic_query-8.snap | 14 - ...__scopes__tests__filter_basic_query-9.snap | 16 - ...on__scopes__tests__filter_basic_query.snap | 31 +- ...ion__scopes__tests__interface_field-2.snap | 46 ++- ...ion__scopes__tests__interface_field-3.snap | 16 - ...ion__scopes__tests__interface_field-4.snap | 32 -- ...ation__scopes__tests__interface_field.snap | 16 +- ...__scopes__tests__interface_fragment-2.snap | 27 +- ...__scopes__tests__interface_fragment-3.snap | 49 ++- ...__scopes__tests__interface_fragment-4.snap | 17 - ...__scopes__tests__interface_fragment-5.snap | 13 - ...__scopes__tests__interface_fragment-6.snap | 18 -- ...__scopes__tests__interface_fragment-7.snap | 5 - ...on__scopes__tests__interface_fragment.snap | 34 +- ...s__tests__interface_inline_fragment-2.snap | 24 +- ...s__tests__interface_inline_fragment-3.snap | 46 ++- ...s__tests__interface_inline_fragment-4.snap | 16 - ...s__tests__interface_inline_fragment-5.snap | 19 -- ...s__tests__interface_inline_fragment-6.snap | 17 - ...s__tests__interface_inline_fragment-7.snap | 5 - ...pes__tests__interface_inline_fragment.snap | 32 +- ...ion__scopes__tests__interface_type-10.snap | 16 - ...tion__scopes__tests__interface_type-2.snap | 28 +- ...tion__scopes__tests__interface_type-3.snap | 20 +- ...tion__scopes__tests__interface_type-4.snap | 33 +- ...tion__scopes__tests__interface_type-5.snap | 25 +- ...tion__scopes__tests__interface_type-6.snap | 13 - ...tion__scopes__tests__interface_type-7.snap | 8 - ...tion__scopes__tests__interface_type-8.snap | 26 -- ...tion__scopes__tests__interface_type-9.snap | 13 - ...zation__scopes__tests__interface_type.snap | 15 +- ...horization__scopes__tests__mutation-2.snap | 5 - ...horization__scopes__tests__mutation-3.snap | 13 - ...uthorization__scopes__tests__mutation.snap | 20 +- ...ization__scopes__tests__query_field-2.snap | 10 - ...ization__scopes__tests__query_field-3.snap | 13 - ...orization__scopes__tests__query_field.snap | 27 +- ...n__scopes__tests__query_field_alias-2.snap | 10 - ...n__scopes__tests__query_field_alias-3.snap | 13 - ...ion__scopes__tests__query_field_alias.snap | 27 +- ...uthorization__scopes__tests__scalar-2.snap | 10 - ...uthorization__scopes__tests__scalar-3.snap | 16 - ..._authorization__scopes__tests__scalar.snap | 23 +- ...authorization__scopes__tests__union-2.snap | 16 - ...__authorization__scopes__tests__union.snap | 20 +- 84 files changed, 1091 insertions(+), 939 deletions(-) delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__array-2.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__defer-2.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_field-3.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_field-4.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_fragment-2.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_inline_fragment-2.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_type-3.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_type-4.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__mutation-2.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__query_field-2.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__query_field_alias-2.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__scalar-2.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__test-2.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__union-2.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__array-2.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__array-3.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-5.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-6.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-7.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-8.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-9.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_field-3.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_field-4.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-4.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-5.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-6.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-7.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-4.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-5.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-6.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-7.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-10.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-6.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-7.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-8.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-9.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__mutation-2.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__mutation-3.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field-2.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field-3.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field_alias-2.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field_alias-3.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__scalar-2.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__scalar-3.snap delete mode 100644 apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__union-2.snap diff --git a/apollo-router/src/plugins/authorization/authenticated.rs b/apollo-router/src/plugins/authorization/authenticated.rs index 981940d0fc..6cc758bafe 100644 --- a/apollo-router/src/plugins/authorization/authenticated.rs +++ b/apollo-router/src/plugins/authorization/authenticated.rs @@ -373,6 +373,24 @@ mod tests { ) } + struct TestResult<'a> { + query: &'a str, + result: apollo_encoder::Document, + paths: Vec, + } + + impl<'a> std::fmt::Display for TestResult<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "query:\n{}\nfiltered:\n{}\npaths: {:?}", + self.query, + self.result, + self.paths.iter().map(|p| p.to_string()).collect::>() + ) + } + } + #[test] fn mutation() { static QUERY: &str = r#" @@ -385,8 +403,11 @@ mod tests { let (doc, paths) = filter(BASIC_SCHEMA, QUERY); - insta::assert_display_snapshot!(doc); - insta::assert_debug_snapshot!(paths); + insta::assert_display_snapshot!(TestResult { + query: QUERY, + result: doc, + paths + }); } #[test] @@ -405,8 +426,11 @@ mod tests { let (doc, paths) = filter(BASIC_SCHEMA, QUERY); - insta::assert_display_snapshot!(doc); - insta::assert_debug_snapshot!(paths); + insta::assert_display_snapshot!(TestResult { + query: QUERY, + result: doc, + paths + }); } #[test] @@ -425,8 +449,11 @@ mod tests { let (doc, paths) = filter(BASIC_SCHEMA, QUERY); - insta::assert_display_snapshot!(doc); - insta::assert_debug_snapshot!(paths); + insta::assert_display_snapshot!(TestResult { + query: QUERY, + result: doc, + paths + }); } #[test] @@ -442,8 +469,11 @@ mod tests { let (doc, paths) = filter(BASIC_SCHEMA, QUERY); - insta::assert_display_snapshot!(doc); - insta::assert_debug_snapshot!(paths); + insta::assert_display_snapshot!(TestResult { + query: QUERY, + result: doc, + paths + }); } #[test] @@ -464,8 +494,11 @@ mod tests { let (doc, paths) = filter(BASIC_SCHEMA, QUERY); - insta::assert_display_snapshot!(doc); - insta::assert_debug_snapshot!(paths); + insta::assert_display_snapshot!(TestResult { + query: QUERY, + result: doc, + paths + }); } #[test] @@ -486,8 +519,11 @@ mod tests { let (doc, paths) = filter(BASIC_SCHEMA, QUERY); - insta::assert_display_snapshot!(doc); - insta::assert_debug_snapshot!(paths); + insta::assert_display_snapshot!(TestResult { + query: QUERY, + result: doc, + paths + }); } #[test] @@ -510,8 +546,11 @@ mod tests { let (doc, paths) = filter(BASIC_SCHEMA, QUERY); - insta::assert_display_snapshot!(doc); - insta::assert_debug_snapshot!(paths); + insta::assert_display_snapshot!(TestResult { + query: QUERY, + result: doc, + paths + }); } #[test] @@ -530,8 +569,11 @@ mod tests { let (doc, paths) = filter(BASIC_SCHEMA, QUERY); - insta::assert_display_snapshot!(doc); - insta::assert_debug_snapshot!(paths); + insta::assert_display_snapshot!(TestResult { + query: QUERY, + result: doc, + paths + }); } #[test] @@ -553,8 +595,11 @@ mod tests { let (doc, paths) = filter(BASIC_SCHEMA, QUERY); - insta::assert_display_snapshot!(doc); - insta::assert_debug_snapshot!(paths); + insta::assert_display_snapshot!(TestResult { + query: QUERY, + result: doc, + paths + }); } static INTERFACE_SCHEMA: &str = r#" @@ -594,8 +639,11 @@ mod tests { let (doc, paths) = filter(INTERFACE_SCHEMA, QUERY); - insta::assert_display_snapshot!(doc); - insta::assert_debug_snapshot!(paths); + insta::assert_display_snapshot!(TestResult { + query: QUERY, + result: doc, + paths + }); static QUERY2: &str = r#" query { @@ -614,8 +662,11 @@ mod tests { let (doc, paths) = filter(INTERFACE_SCHEMA, QUERY2); - insta::assert_display_snapshot!(doc); - insta::assert_debug_snapshot!(paths); + insta::assert_display_snapshot!(TestResult { + query: QUERY2, + result: doc, + paths + }); } static INTERFACE_FIELD_SCHEMA: &str = r#" @@ -659,8 +710,11 @@ mod tests { let (doc, paths) = filter(INTERFACE_FIELD_SCHEMA, QUERY); - insta::assert_display_snapshot!(doc); - insta::assert_debug_snapshot!(paths); + insta::assert_display_snapshot!(TestResult { + query: QUERY, + result: doc, + paths + }); static QUERY2: &str = r#" query { @@ -681,8 +735,11 @@ mod tests { let (doc, paths) = filter(INTERFACE_FIELD_SCHEMA, QUERY2); - insta::assert_display_snapshot!(doc); - insta::assert_debug_snapshot!(paths); + insta::assert_display_snapshot!(TestResult { + query: QUERY2, + result: doc, + paths + }); } #[test] @@ -723,8 +780,11 @@ mod tests { let (doc, paths) = filter(UNION_MEMBERS_SCHEMA, QUERY); - insta::assert_display_snapshot!(doc); - insta::assert_debug_snapshot!(paths); + insta::assert_display_snapshot!(TestResult { + query: QUERY, + result: doc, + paths + }); } const SCHEMA: &str = r#"schema @@ -988,7 +1048,6 @@ mod tests { .unwrap() .clone(), ) - //.headers(headers) .context(context) .build() .unwrap(); diff --git a/apollo-router/src/plugins/authorization/scopes.rs b/apollo-router/src/plugins/authorization/scopes.rs index 1e4fcdb8db..ea5655aead 100644 --- a/apollo-router/src/plugins/authorization/scopes.rs +++ b/apollo-router/src/plugins/authorization/scopes.rs @@ -576,10 +576,10 @@ mod tests { } "#; - fn extract(query: &str) -> BTreeSet { + fn extract(schema: &str, query: &str) -> BTreeSet { let mut compiler = ApolloCompiler::new(); - let _schema_id = compiler.add_type_system(BASIC_SCHEMA, "schema.graphql"); + let _schema_id = compiler.add_type_system(schema, "schema.graphql"); let id = compiler.add_executable(query, "query.graphql"); let diagnostics = compiler.validate(); @@ -609,7 +609,7 @@ mod tests { } "#; - let doc = extract(QUERY); + let doc = extract(BASIC_SCHEMA, QUERY); insta::assert_debug_snapshot!(doc); } @@ -634,6 +634,28 @@ mod tests { ) } + struct TestResult<'a> { + query: &'a str, + extracted_scopes: &'a BTreeSet, + result: apollo_encoder::Document, + scopes: Vec, + paths: Vec, + } + + impl<'a> std::fmt::Display for TestResult<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "query:\n{}\nextracted_scopes: {:?}\nrequest scopes: {:?}\nfiltered:\n{}\npaths: {:?}", + self.query, + self.extracted_scopes, + self.scopes, + self.result, + self.paths.iter().map(|p| p.to_string()).collect::>() + ) + } + } + #[test] fn filter_basic_query() { static QUERY: &str = r#" @@ -650,12 +672,16 @@ mod tests { } "#; - let doc = extract(QUERY); - insta::assert_debug_snapshot!(doc); - + let extracted_scopes = extract(BASIC_SCHEMA, QUERY); let (doc, paths) = filter(BASIC_SCHEMA, QUERY, HashSet::new()); - insta::assert_display_snapshot!(doc); - insta::assert_debug_snapshot!(paths); + + insta::assert_display_snapshot!(TestResult { + query: QUERY, + extracted_scopes: &extracted_scopes, + scopes: Vec::new(), + result: doc, + paths + }); let (doc, paths) = filter( BASIC_SCHEMA, @@ -664,8 +690,16 @@ mod tests { .into_iter() .collect(), ); - insta::assert_display_snapshot!(doc); - insta::assert_debug_snapshot!(paths); + + insta::assert_display_snapshot!(TestResult { + query: QUERY, + extracted_scopes: &extracted_scopes, + scopes: ["profile".to_string(), "internal".to_string()] + .into_iter() + .collect(), + result: doc, + paths + }); let (doc, paths) = filter( BASIC_SCHEMA, @@ -679,8 +713,20 @@ mod tests { .into_iter() .collect(), ); - insta::assert_display_snapshot!(doc); - insta::assert_debug_snapshot!(paths); + insta::assert_display_snapshot!(TestResult { + query: QUERY, + extracted_scopes: &extracted_scopes, + scopes: [ + "profile".to_string(), + "read:user".to_string(), + "internal".to_string(), + "test".to_string(), + ] + .into_iter() + .collect(), + result: doc, + paths + }); let (doc, paths) = filter( BASIC_SCHEMA, @@ -693,8 +739,19 @@ mod tests { .into_iter() .collect(), ); - insta::assert_display_snapshot!(doc); - insta::assert_debug_snapshot!(paths); + insta::assert_display_snapshot!(TestResult { + query: QUERY, + extracted_scopes: &extracted_scopes, + scopes: [ + "profile".to_string(), + "read:user".to_string(), + "read:username".to_string(), + ] + .into_iter() + .collect(), + result: doc, + paths + }); } #[test] @@ -707,13 +764,17 @@ mod tests { } "#; - let doc = extract(QUERY); - insta::assert_debug_snapshot!(doc); + let extracted_scopes = extract(BASIC_SCHEMA, QUERY); let (doc, paths) = filter(BASIC_SCHEMA, QUERY, HashSet::new()); - insta::assert_display_snapshot!(doc); - insta::assert_debug_snapshot!(paths); + insta::assert_display_snapshot!(TestResult { + query: QUERY, + extracted_scopes: &extracted_scopes, + scopes: Vec::new(), + result: doc, + paths + }); } #[test] @@ -730,13 +791,17 @@ mod tests { } "#; - let doc = extract(QUERY); - insta::assert_debug_snapshot!(doc); + let extracted_scopes = extract(BASIC_SCHEMA, QUERY); let (doc, paths) = filter(BASIC_SCHEMA, QUERY, HashSet::new()); - insta::assert_display_snapshot!(doc); - insta::assert_debug_snapshot!(paths); + insta::assert_display_snapshot!(TestResult { + query: QUERY, + extracted_scopes: &extracted_scopes, + scopes: Vec::new(), + result: doc, + paths + }); } #[test] @@ -753,13 +818,17 @@ mod tests { } "#; - let doc = extract(QUERY); - insta::assert_debug_snapshot!(doc); + let extracted_scopes = extract(BASIC_SCHEMA, QUERY); let (doc, paths) = filter(BASIC_SCHEMA, QUERY, HashSet::new()); - insta::assert_display_snapshot!(doc); - insta::assert_debug_snapshot!(paths); + insta::assert_display_snapshot!(TestResult { + query: QUERY, + extracted_scopes: &extracted_scopes, + scopes: Vec::new(), + result: doc, + paths + }); } #[test] @@ -773,13 +842,17 @@ mod tests { } "#; - let doc = extract(QUERY); - insta::assert_debug_snapshot!(doc); + let extracted_scopes = extract(BASIC_SCHEMA, QUERY); let (doc, paths) = filter(BASIC_SCHEMA, QUERY, HashSet::new()); - insta::assert_display_snapshot!(doc); - insta::assert_debug_snapshot!(paths); + insta::assert_display_snapshot!(TestResult { + query: QUERY, + extracted_scopes: &extracted_scopes, + scopes: Vec::new(), + result: doc, + paths + }); } #[test] @@ -798,13 +871,17 @@ mod tests { } "#; - let doc = extract(QUERY); - insta::assert_debug_snapshot!(doc); + let extracted_scopes = extract(BASIC_SCHEMA, QUERY); let (doc, paths) = filter(BASIC_SCHEMA, QUERY, HashSet::new()); - insta::assert_display_snapshot!(doc); - insta::assert_debug_snapshot!(paths); + insta::assert_display_snapshot!(TestResult { + query: QUERY, + extracted_scopes: &extracted_scopes, + scopes: Vec::new(), + result: doc, + paths + }); } #[test] @@ -824,13 +901,17 @@ mod tests { } "#; - let doc = extract(QUERY); - insta::assert_debug_snapshot!(doc); + let extracted_scopes = extract(BASIC_SCHEMA, QUERY); let (doc, paths) = filter(BASIC_SCHEMA, QUERY, HashSet::new()); - insta::assert_display_snapshot!(doc); - insta::assert_debug_snapshot!(paths); + insta::assert_display_snapshot!(TestResult { + query: QUERY, + extracted_scopes: &extracted_scopes, + scopes: Vec::new(), + result: doc, + paths + }); let (doc, paths) = filter( BASIC_SCHEMA, @@ -838,8 +919,13 @@ mod tests { ["read:user".to_string()].into_iter().collect(), ); - insta::assert_display_snapshot!(doc); - insta::assert_debug_snapshot!(paths); + insta::assert_display_snapshot!(TestResult { + query: QUERY, + extracted_scopes: &extracted_scopes, + scopes: ["read:user".to_string()].into_iter().collect(), + result: doc, + paths + }); let (doc, paths) = filter( BASIC_SCHEMA, @@ -849,8 +935,15 @@ mod tests { .collect(), ); - insta::assert_display_snapshot!(doc); - insta::assert_debug_snapshot!(paths); + insta::assert_display_snapshot!(TestResult { + query: QUERY, + extracted_scopes: &extracted_scopes, + scopes: ["read:user".to_string(), "read:username".to_string()] + .into_iter() + .collect(), + result: doc, + paths + }); } #[test] @@ -872,13 +965,17 @@ mod tests { } "#; - let doc = extract(QUERY); - insta::assert_debug_snapshot!(doc); + let extracted_scopes = extract(BASIC_SCHEMA, QUERY); let (doc, paths) = filter(BASIC_SCHEMA, QUERY, HashSet::new()); - insta::assert_display_snapshot!(doc); - insta::assert_debug_snapshot!(paths); + insta::assert_display_snapshot!(TestResult { + query: QUERY, + extracted_scopes: &extracted_scopes, + scopes: Vec::new(), + result: doc, + paths + }); let (doc, paths) = filter( BASIC_SCHEMA, @@ -886,8 +983,13 @@ mod tests { ["read:user".to_string()].into_iter().collect(), ); - insta::assert_display_snapshot!(doc); - insta::assert_debug_snapshot!(paths); + insta::assert_display_snapshot!(TestResult { + query: QUERY, + extracted_scopes: &extracted_scopes, + scopes: ["read:user".to_string()].into_iter().collect(), + result: doc, + paths + }); let (doc, paths) = filter( BASIC_SCHEMA, @@ -897,8 +999,15 @@ mod tests { .collect(), ); - insta::assert_display_snapshot!(doc); - insta::assert_debug_snapshot!(paths); + insta::assert_display_snapshot!(TestResult { + query: QUERY, + extracted_scopes: &extracted_scopes, + scopes: ["read:user".to_string(), "read:username".to_string()] + .into_iter() + .collect(), + result: doc, + paths + }); } static INTERFACE_SCHEMA: &str = r#" @@ -932,10 +1041,16 @@ mod tests { } "#; + let extracted_scopes = extract(INTERFACE_SCHEMA, QUERY); let (doc, paths) = filter(INTERFACE_SCHEMA, QUERY, HashSet::new()); - insta::assert_display_snapshot!(doc); - insta::assert_debug_snapshot!(paths); + insta::assert_display_snapshot!(TestResult { + query: QUERY, + extracted_scopes: &extracted_scopes, + scopes: Vec::new(), + result: doc, + paths + }); let (doc, paths) = filter( INTERFACE_SCHEMA, @@ -943,8 +1058,13 @@ mod tests { ["itf".to_string()].into_iter().collect(), ); - insta::assert_display_snapshot!(doc); - insta::assert_debug_snapshot!(paths); + insta::assert_display_snapshot!(TestResult { + query: QUERY, + extracted_scopes: &extracted_scopes, + scopes: ["itf".to_string()].into_iter().collect(), + result: doc, + paths + }); static QUERY2: &str = r#" query { @@ -960,10 +1080,16 @@ mod tests { } "#; + let extracted_scopes = extract(INTERFACE_SCHEMA, QUERY2); let (doc, paths) = filter(INTERFACE_SCHEMA, QUERY2, HashSet::new()); - insta::assert_display_snapshot!(doc); - insta::assert_debug_snapshot!(paths); + insta::assert_display_snapshot!(TestResult { + query: QUERY2, + extracted_scopes: &extracted_scopes, + scopes: Vec::new(), + result: doc, + paths + }); let (doc, paths) = filter( INTERFACE_SCHEMA, @@ -971,8 +1097,13 @@ mod tests { ["itf".to_string()].into_iter().collect(), ); - insta::assert_display_snapshot!(doc); - insta::assert_debug_snapshot!(paths); + insta::assert_display_snapshot!(TestResult { + query: QUERY2, + extracted_scopes: &extracted_scopes, + scopes: ["itf".to_string()].into_iter().collect(), + result: doc, + paths + }); let (doc, paths) = filter( INTERFACE_SCHEMA, @@ -982,8 +1113,15 @@ mod tests { .collect(), ); - insta::assert_display_snapshot!(doc); - insta::assert_debug_snapshot!(paths); + insta::assert_display_snapshot!(TestResult { + query: QUERY2, + extracted_scopes: &extracted_scopes, + scopes: ["itf".to_string(), "a".to_string(), "b".to_string()] + .into_iter() + .collect(), + result: doc, + paths + }); } static INTERFACE_FIELD_SCHEMA: &str = r#" @@ -1021,10 +1159,17 @@ mod tests { } "#; + let extracted_scopes: BTreeSet = extract(INTERFACE_FIELD_SCHEMA, QUERY); + let (doc, paths) = filter(INTERFACE_FIELD_SCHEMA, QUERY, HashSet::new()); - insta::assert_display_snapshot!(doc); - insta::assert_debug_snapshot!(paths); + insta::assert_display_snapshot!(TestResult { + query: QUERY, + extracted_scopes: &extracted_scopes, + scopes: Vec::new(), + result: doc, + paths + }); static QUERY2: &str = r#" query { @@ -1042,10 +1187,17 @@ mod tests { } "#; + let extracted_scopes: BTreeSet = extract(INTERFACE_FIELD_SCHEMA, QUERY2); + let (doc, paths) = filter(INTERFACE_FIELD_SCHEMA, QUERY2, HashSet::new()); - insta::assert_display_snapshot!(doc); - insta::assert_debug_snapshot!(paths); + insta::assert_display_snapshot!(TestResult { + query: QUERY2, + extracted_scopes: &extracted_scopes, + scopes: Vec::new(), + result: doc, + paths + }); } #[test] @@ -1081,13 +1233,20 @@ mod tests { } "#; + let extracted_scopes: BTreeSet = extract(UNION_MEMBERS_SCHEMA, QUERY); + let (doc, paths) = filter( UNION_MEMBERS_SCHEMA, QUERY, ["a".to_string(), "b".to_string()].into_iter().collect(), ); - insta::assert_display_snapshot!(doc); - insta::assert_debug_snapshot!(paths); + insta::assert_display_snapshot!(TestResult { + query: QUERY, + extracted_scopes: &extracted_scopes, + scopes: ["a".to_string(), "b".to_string()].into_iter().collect(), + result: doc, + paths + }); } } diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__array-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__array-2.snap deleted file mode 100644 index 8cbf1dc214..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__array-2.snap +++ /dev/null @@ -1,20 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/authenticated.rs -expression: paths ---- -[ - Path( - [ - Key( - "topProducts", - ), - Key( - "publicReviews", - ), - Flatten, - Key( - "author", - ), - ], - ), -] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__array.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__array.snap index d1e7a69ddb..20a3c8e19b 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__array.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__array.snap @@ -1,7 +1,22 @@ --- source: apollo-router/src/plugins/authorization/authenticated.rs -expression: doc +expression: "TestResult { query: QUERY, result: doc, paths }" --- +query: + + query { + topProducts { + type + publicReviews { + body + author { + name + } + } + } + } + +filtered: query { topProducts { type @@ -11,3 +26,4 @@ query { } } +paths: ["/topProducts/publicReviews/@/author"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__defer-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__defer-2.snap deleted file mode 100644 index eb1ced7add..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__defer-2.snap +++ /dev/null @@ -1,19 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/authenticated.rs -expression: paths ---- -[ - Path( - [ - Key( - "topProducts", - ), - Fragment( - "", - ), - Key( - "nonNullId", - ), - ], - ), -] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__defer.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__defer.snap index 78ccae8dc0..0ae1fefd61 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__defer.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__defer.snap @@ -1,10 +1,24 @@ --- source: apollo-router/src/plugins/authorization/authenticated.rs -expression: doc +expression: "TestResult { query: QUERY, result: doc, paths }" --- +query: + + query { + topProducts { + type + + ...@defer { + nonNullId + } + } + } + +filtered: query { topProducts { type } } +paths: ["/topProducts/... on /nonNullId"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_field-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_field-2.snap index 2a6c558147..48558ebd59 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_field-2.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_field-2.snap @@ -1,16 +1,36 @@ --- source: apollo-router/src/plugins/authorization/authenticated.rs -expression: paths +expression: "TestResult { query: QUERY2, result: doc, paths }" --- -[ - Path( - [ - Key( - "itf", - ), - Key( - "id", - ), - ], - ), -] +query: + + query { + test + itf { + ... on A { + id + other + } + + ... on B { + id + other + } + } + } + +filtered: +query { + test + itf { + ... on A { + id + other + } + ... on B { + other + } + } +} + +paths: ["/itf/... on B/id"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_field-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_field-3.snap deleted file mode 100644 index 6d1de215cc..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_field-3.snap +++ /dev/null @@ -1,17 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/authenticated.rs -expression: doc ---- -query { - test - itf { - ... on A { - id - other - } - ... on B { - other - } - } -} - diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_field-4.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_field-4.snap deleted file mode 100644 index 3e5aa2cec7..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_field-4.snap +++ /dev/null @@ -1,19 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/authenticated.rs -expression: paths ---- -[ - Path( - [ - Key( - "itf", - ), - Fragment( - "B", - ), - Key( - "id", - ), - ], - ), -] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_field.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_field.snap index 78b312e1eb..2cc8e91ab0 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_field.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_field.snap @@ -1,7 +1,18 @@ --- source: apollo-router/src/plugins/authorization/authenticated.rs -expression: doc +expression: "TestResult { query: QUERY, result: doc, paths }" --- +query: + + query { + test + itf { + id + other + } + } + +filtered: query { test itf { @@ -9,3 +20,4 @@ query { } } +paths: ["/itf/id"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_fragment-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_fragment-2.snap deleted file mode 100644 index 43a002b1ba..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_fragment-2.snap +++ /dev/null @@ -1,16 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/authenticated.rs -expression: paths ---- -[ - Path( - [ - Key( - "itf", - ), - Fragment( - "User", - ), - ], - ), -] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_fragment.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_fragment.snap index ab5f39ec12..b969b6615f 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_fragment.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_fragment.snap @@ -1,7 +1,24 @@ --- source: apollo-router/src/plugins/authorization/authenticated.rs -expression: doc +expression: "TestResult { query: QUERY, result: doc, paths }" --- +query: + + query { + topProducts { + type + } + itf { + id + ...F + } + } + + fragment F on User { + name + } + +filtered: query { topProducts { type @@ -11,3 +28,4 @@ query { } } +paths: ["/itf/... on User"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_inline_fragment-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_inline_fragment-2.snap deleted file mode 100644 index 43a002b1ba..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_inline_fragment-2.snap +++ /dev/null @@ -1,16 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/authenticated.rs -expression: paths ---- -[ - Path( - [ - Key( - "itf", - ), - Fragment( - "User", - ), - ], - ), -] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_inline_fragment.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_inline_fragment.snap index ab5f39ec12..10a80d5b05 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_inline_fragment.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_inline_fragment.snap @@ -1,7 +1,22 @@ --- source: apollo-router/src/plugins/authorization/authenticated.rs -expression: doc +expression: "TestResult { query: QUERY, result: doc, paths }" --- +query: + + query { + topProducts { + type + } + itf { + id + ... on User { + name + } + } + } + +filtered: query { topProducts { type @@ -11,3 +26,4 @@ query { } } +paths: ["/itf/... on User"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_type-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_type-2.snap index b178b224b5..73480e9d5e 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_type-2.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_type-2.snap @@ -1,13 +1,30 @@ --- source: apollo-router/src/plugins/authorization/authenticated.rs -expression: paths +expression: "TestResult { query: QUERY2, result: doc, paths }" --- -[ - Path( - [ - Key( - "itf", - ), - ], - ), -] +query: + + query { + test + itf { + ... on A { + id + } + + ... on B { + id + } + } + } + +filtered: +query { + test + itf { + ... on A { + id + } + } +} + +paths: ["/itf/... on B"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_type-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_type-3.snap deleted file mode 100644 index 7e609edb06..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_type-3.snap +++ /dev/null @@ -1,13 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/authenticated.rs -expression: doc ---- -query { - test - itf { - ... on A { - id - } - } -} - diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_type-4.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_type-4.snap deleted file mode 100644 index 2da68374d7..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_type-4.snap +++ /dev/null @@ -1,16 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/authenticated.rs -expression: paths ---- -[ - Path( - [ - Key( - "itf", - ), - Fragment( - "B", - ), - ], - ), -] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_type.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_type.snap index 7fa88ccfa3..1d06b6a5f0 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_type.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_type.snap @@ -1,8 +1,19 @@ --- source: apollo-router/src/plugins/authorization/authenticated.rs -expression: doc +expression: "TestResult { query: QUERY, result: doc, paths }" --- +query: + + query { + test + itf { + id + } + } + +filtered: query { test } +paths: ["/itf"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__mutation-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__mutation-2.snap deleted file mode 100644 index 974d958186..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__mutation-2.snap +++ /dev/null @@ -1,13 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/authenticated.rs -expression: paths ---- -[ - Path( - [ - Key( - "ping", - ), - ], - ), -] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__mutation.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__mutation.snap index 865d64decd..dfff698876 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__mutation.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__mutation.snap @@ -1,5 +1,15 @@ --- source: apollo-router/src/plugins/authorization/authenticated.rs -expression: doc +expression: "TestResult { query: QUERY, result: doc, paths }" --- +query: + mutation { + ping { + name + } + } + +filtered: + +paths: ["/ping"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__query_field-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__query_field-2.snap deleted file mode 100644 index 4a1bcd42be..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__query_field-2.snap +++ /dev/null @@ -1,13 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/authenticated.rs -expression: paths ---- -[ - Path( - [ - Key( - "me", - ), - ], - ), -] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__query_field.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__query_field.snap index 78ccae8dc0..d4691a6c97 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__query_field.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__query_field.snap @@ -1,10 +1,24 @@ --- source: apollo-router/src/plugins/authorization/authenticated.rs -expression: doc +expression: "TestResult { query: QUERY, result: doc, paths }" --- +query: + + query { + topProducts { + type + } + + me { + name + } + } + +filtered: query { topProducts { type } } +paths: ["/me"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__query_field_alias-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__query_field_alias-2.snap deleted file mode 100644 index 4a1bcd42be..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__query_field_alias-2.snap +++ /dev/null @@ -1,13 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/authenticated.rs -expression: paths ---- -[ - Path( - [ - Key( - "me", - ), - ], - ), -] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__query_field_alias.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__query_field_alias.snap index 78ccae8dc0..bb3ccaeec0 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__query_field_alias.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__query_field_alias.snap @@ -1,10 +1,24 @@ --- source: apollo-router/src/plugins/authorization/authenticated.rs -expression: doc +expression: "TestResult { query: QUERY, result: doc, paths }" --- +query: + + query { + topProducts { + type + } + + moi: me { + name + } + } + +filtered: query { topProducts { type } } +paths: ["/me"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__scalar-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__scalar-2.snap deleted file mode 100644 index d4cfac90a7..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__scalar-2.snap +++ /dev/null @@ -1,16 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/authenticated.rs -expression: paths ---- -[ - Path( - [ - Key( - "topProducts", - ), - Key( - "internal", - ), - ], - ), -] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__scalar.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__scalar.snap index 78ccae8dc0..22bb2f4d4c 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__scalar.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__scalar.snap @@ -1,10 +1,21 @@ --- source: apollo-router/src/plugins/authorization/authenticated.rs -expression: doc +expression: "TestResult { query: QUERY, result: doc, paths }" --- +query: + + query { + topProducts { + type + internal + } + } + +filtered: query { topProducts { type } } +paths: ["/topProducts/internal"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__test-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__test-2.snap deleted file mode 100644 index 4327e9ef8b..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__test-2.snap +++ /dev/null @@ -1,24 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/authenticated.rs -expression: paths ---- -[ - Path( - [ - Key( - "topProducts", - ), - Key( - "reviews", - ), - Flatten, - ], - ), - Path( - [ - Key( - "customer", - ), - ], - ), -] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__test.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__test.snap index 78ccae8dc0..25c169cd20 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__test.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__test.snap @@ -1,10 +1,27 @@ --- source: apollo-router/src/plugins/authorization/authenticated.rs -expression: doc +expression: "TestResult { query: QUERY, result: doc, paths }" --- +query: + + query { + topProducts { + type + reviews { + body + } + } + + customer { + name + } + } + +filtered: query { topProducts { type } } +paths: ["/topProducts/reviews/@", "/customer"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__union-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__union-2.snap deleted file mode 100644 index 1630a33f7e..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__union-2.snap +++ /dev/null @@ -1,16 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/authenticated.rs -expression: paths ---- -[ - Path( - [ - Key( - "uni", - ), - Fragment( - "B", - ), - ], - ), -] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__union.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__union.snap index 03d23edc00..98a7d35b73 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__union.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__union.snap @@ -1,7 +1,22 @@ --- source: apollo-router/src/plugins/authorization/authenticated.rs -expression: doc +expression: "TestResult { query: QUERY, result: doc, paths }" --- +query: + + query { + test + uni { + ... on A { + id + } + ... on B { + id + } + } + } + +filtered: query { test uni { @@ -11,3 +26,4 @@ query { } } +paths: ["/uni/... on B"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__array-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__array-2.snap deleted file mode 100644 index 930db08570..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__array-2.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/scopes.rs -expression: doc ---- -query { - topProducts { - type - } -} - diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__array-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__array-3.snap deleted file mode 100644 index 31370c2aef..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__array-3.snap +++ /dev/null @@ -1,17 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/scopes.rs -expression: paths ---- -[ - Path( - [ - Key( - "topProducts", - ), - Key( - "publicReviews", - ), - Flatten, - ], - ), -] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__array.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__array.snap index d89d19d331..1dfb5b2ef0 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__array.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__array.snap @@ -1,9 +1,28 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: doc +expression: "TestResult {\n query: QUERY,\n extracted_scopes: &extracted_scopes,\n scopes: Vec::new(),\n result: doc,\n paths,\n}" --- -{ - "read:user", - "read:username", - "review", +query: + + query { + topProducts { + type + publicReviews { + body + author { + name + } + } + } + } + +extracted_scopes: {"read:user", "read:username", "review"} +request scopes: [] +filtered: +query { + topProducts { + type + } } + +paths: ["/topProducts/publicReviews/@"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-2.snap index 930db08570..c349120848 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-2.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-2.snap @@ -1,10 +1,28 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: doc +expression: "TestResult {\n query: QUERY,\n extracted_scopes: &extracted_scopes,\n scopes: [\"profile\".to_string(),\n \"internal\".to_string()].into_iter().collect(),\n result: doc,\n paths,\n}" --- +query: + + { + topProducts { + type + internal + } + + me { + id + name + } + } + +extracted_scopes: {"internal", "profile", "read:user", "read:username", "test"} +request scopes: ["profile", "internal"] +filtered: query { topProducts { type } } +paths: ["/topProducts/internal", "/me"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-3.snap index f0192cbe84..90a1b954d9 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-3.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-3.snap @@ -1,23 +1,32 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: paths +expression: "TestResult {\n query: QUERY,\n extracted_scopes: &extracted_scopes,\n scopes: [\"profile\".to_string(), \"read:user\".to_string(),\n \"internal\".to_string(),\n \"test\".to_string()].into_iter().collect(),\n result: doc,\n paths,\n}" --- -[ - Path( - [ - Key( - "topProducts", - ), - Key( - "internal", - ), - ], - ), - Path( - [ - Key( - "me", - ), - ], - ), -] +query: + + { + topProducts { + type + internal + } + + me { + id + name + } + } + +extracted_scopes: {"internal", "profile", "read:user", "read:username", "test"} +request scopes: ["profile", "read:user", "internal", "test"] +filtered: +query { + topProducts { + type + internal + } + me { + id + } +} + +paths: ["/me/name"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-4.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-4.snap index 930db08570..dc9f551e7c 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-4.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-4.snap @@ -1,10 +1,32 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: doc +expression: "TestResult {\n query: QUERY,\n extracted_scopes: &extracted_scopes,\n scopes: [\"profile\".to_string(), \"read:user\".to_string(),\n \"read:username\".to_string()].into_iter().collect(),\n result: doc,\n paths,\n}" --- +query: + + { + topProducts { + type + internal + } + + me { + id + name + } + } + +extracted_scopes: {"internal", "profile", "read:user", "read:username", "test"} +request scopes: ["profile", "read:user", "read:username"] +filtered: query { topProducts { type } + me { + id + name + } } +paths: ["/topProducts/internal"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-5.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-5.snap deleted file mode 100644 index f0192cbe84..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-5.snap +++ /dev/null @@ -1,23 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/scopes.rs -expression: paths ---- -[ - Path( - [ - Key( - "topProducts", - ), - Key( - "internal", - ), - ], - ), - Path( - [ - Key( - "me", - ), - ], - ), -] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-6.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-6.snap deleted file mode 100644 index c673d28a79..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-6.snap +++ /dev/null @@ -1,14 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/scopes.rs -expression: doc ---- -query { - topProducts { - type - internal - } - me { - id - } -} - diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-7.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-7.snap deleted file mode 100644 index e45e102000..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-7.snap +++ /dev/null @@ -1,16 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/scopes.rs -expression: paths ---- -[ - Path( - [ - Key( - "me", - ), - Key( - "name", - ), - ], - ), -] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-8.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-8.snap deleted file mode 100644 index df7b1a5852..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-8.snap +++ /dev/null @@ -1,14 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/scopes.rs -expression: doc ---- -query { - topProducts { - type - } - me { - id - name - } -} - diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-9.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-9.snap deleted file mode 100644 index 3b37534097..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query-9.snap +++ /dev/null @@ -1,16 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/scopes.rs -expression: paths ---- -[ - Path( - [ - Key( - "topProducts", - ), - Key( - "internal", - ), - ], - ), -] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query.snap index c2b0de1dbf..cc094cab8a 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__filter_basic_query.snap @@ -1,11 +1,28 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: doc +expression: "TestResult {\n query: QUERY,\n extracted_scopes: &extracted_scopes,\n scopes: Vec::new(),\n result: doc,\n paths,\n}" --- -{ - "internal", - "profile", - "read:user", - "read:username", - "test", +query: + + { + topProducts { + type + internal + } + + me { + id + name + } + } + +extracted_scopes: {"internal", "profile", "read:user", "read:username", "test"} +request scopes: [] +filtered: +query { + topProducts { + type + } } + +paths: ["/topProducts/internal", "/me"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_field-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_field-2.snap index 152fdb0782..d872f7d72e 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_field-2.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_field-2.snap @@ -1,16 +1,36 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: paths +expression: "TestResult {\n query: QUERY2,\n extracted_scopes: &extracted_scopes,\n scopes: Vec::new(),\n result: doc,\n paths,\n}" --- -[ - Path( - [ - Key( - "itf", - ), - Key( - "id", - ), - ], - ), -] +query: + + query { + test + itf { + ... on A { + id + other + } + ... on B { + id + other + } + } + } + +extracted_scopes: {"a", "b", "c", "d"} +request scopes: [] +filtered: +query { + test + itf { + ... on A { + other + } + ... on B { + other + } + } +} + +paths: ["/itf/... on A/id", "/itf/... on B/id"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_field-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_field-3.snap deleted file mode 100644 index 9f8ecb6de2..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_field-3.snap +++ /dev/null @@ -1,16 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/scopes.rs -expression: doc ---- -query { - test - itf { - ... on A { - other - } - ... on B { - other - } - } -} - diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_field-4.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_field-4.snap deleted file mode 100644 index 2ccbf2a02b..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_field-4.snap +++ /dev/null @@ -1,32 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/scopes.rs -expression: paths ---- -[ - Path( - [ - Key( - "itf", - ), - Fragment( - "A", - ), - Key( - "id", - ), - ], - ), - Path( - [ - Key( - "itf", - ), - Fragment( - "B", - ), - Key( - "id", - ), - ], - ), -] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_field.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_field.snap index 6bdb719301..ff68fe0481 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_field.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_field.snap @@ -1,7 +1,20 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: doc +expression: "TestResult {\n query: QUERY,\n extracted_scopes: &extracted_scopes,\n scopes: Vec::new(),\n result: doc,\n paths,\n}" --- +query: + + query { + test + itf { + id + other + } + } + +extracted_scopes: {} +request scopes: [] +filtered: query { test itf { @@ -9,3 +22,4 @@ query { } } +paths: ["/itf/id"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-2.snap index cd12631865..4a9ecd227f 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-2.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-2.snap @@ -1,13 +1,38 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: doc +expression: "TestResult {\n query: QUERY,\n extracted_scopes: &extracted_scopes,\n scopes: [\"read:user\".to_string()].into_iter().collect(),\n result: doc,\n paths,\n}" --- +query: + + query { + topProducts { + type + } + itf { + id + ...F + } + } + + fragment F on User { + id2: id + name + } + +extracted_scopes: {"read:user", "read:username"} +request scopes: ["read:user"] +filtered: query { topProducts { type } itf { id + ...F } } +fragment F on User { + id2: id +} +paths: ["/name"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-3.snap index 3461adf398..0061051f93 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-3.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-3.snap @@ -1,16 +1,39 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: paths +expression: "TestResult {\n query: QUERY,\n extracted_scopes: &extracted_scopes,\n scopes: [\"read:user\".to_string(),\n \"read:username\".to_string()].into_iter().collect(),\n result: doc,\n paths,\n}" --- -[ - Path( - [ - Key( - "itf", - ), - Fragment( - "User", - ), - ], - ), -] +query: + + query { + topProducts { + type + } + itf { + id + ...F + } + } + + fragment F on User { + id2: id + name + } + +extracted_scopes: {"read:user", "read:username"} +request scopes: ["read:user", "read:username"] +filtered: +query { + topProducts { + type + } + itf { + id + ...F + } +} +fragment F on User { + id2: id + name +} + +paths: [] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-4.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-4.snap deleted file mode 100644 index 66f3830544..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-4.snap +++ /dev/null @@ -1,17 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/scopes.rs -expression: doc ---- -query { - topProducts { - type - } - itf { - id - ...F - } -} -fragment F on User { - id2: id -} - diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-5.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-5.snap deleted file mode 100644 index c72063e600..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-5.snap +++ /dev/null @@ -1,13 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/scopes.rs -expression: paths ---- -[ - Path( - [ - Key( - "name", - ), - ], - ), -] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-6.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-6.snap deleted file mode 100644 index 49a4d3731a..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-6.snap +++ /dev/null @@ -1,18 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/scopes.rs -expression: doc ---- -query { - topProducts { - type - } - itf { - id - ...F - } -} -fragment F on User { - id2: id - name -} - diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-7.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-7.snap deleted file mode 100644 index f521439d55..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment-7.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/scopes.rs -expression: paths ---- -[] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment.snap index eff331c158..40d8661819 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment.snap @@ -1,8 +1,34 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: doc +expression: "TestResult {\n query: QUERY,\n extracted_scopes: &extracted_scopes,\n scopes: Vec::new(),\n result: doc,\n paths,\n}" --- -{ - "read:user", - "read:username", +query: + + query { + topProducts { + type + } + itf { + id + ...F + } + } + + fragment F on User { + id2: id + name + } + +extracted_scopes: {"read:user", "read:username"} +request scopes: [] +filtered: +query { + topProducts { + type + } + itf { + id + } } + +paths: ["/itf/... on User"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-2.snap index cd12631865..21a2667724 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-2.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-2.snap @@ -1,13 +1,35 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: doc +expression: "TestResult {\n query: QUERY,\n extracted_scopes: &extracted_scopes,\n scopes: [\"read:user\".to_string()].into_iter().collect(),\n result: doc,\n paths,\n}" --- +query: + + query { + topProducts { + type + } + itf { + id + ... on User { + id2: id + name + } + } + } + +extracted_scopes: {"read:user", "read:username"} +request scopes: ["read:user"] +filtered: query { topProducts { type } itf { id + ... on User { + id2: id + } } } +paths: ["/itf/... on User/name"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-3.snap index 3461adf398..e737d787ff 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-3.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-3.snap @@ -1,16 +1,36 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: paths +expression: "TestResult {\n query: QUERY,\n extracted_scopes: &extracted_scopes,\n scopes: [\"read:user\".to_string(),\n \"read:username\".to_string()].into_iter().collect(),\n result: doc,\n paths,\n}" --- -[ - Path( - [ - Key( - "itf", - ), - Fragment( - "User", - ), - ], - ), -] +query: + + query { + topProducts { + type + } + itf { + id + ... on User { + id2: id + name + } + } + } + +extracted_scopes: {"read:user", "read:username"} +request scopes: ["read:user", "read:username"] +filtered: +query { + topProducts { + type + } + itf { + id + ... on User { + id2: id + name + } + } +} + +paths: [] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-4.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-4.snap deleted file mode 100644 index 95d7fc9638..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-4.snap +++ /dev/null @@ -1,16 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/scopes.rs -expression: doc ---- -query { - topProducts { - type - } - itf { - id - ... on User { - id2: id - } - } -} - diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-5.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-5.snap deleted file mode 100644 index 962ba94b84..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-5.snap +++ /dev/null @@ -1,19 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/scopes.rs -expression: paths ---- -[ - Path( - [ - Key( - "itf", - ), - Fragment( - "User", - ), - Key( - "name", - ), - ], - ), -] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-6.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-6.snap deleted file mode 100644 index 347b294cd0..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-6.snap +++ /dev/null @@ -1,17 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/scopes.rs -expression: doc ---- -query { - topProducts { - type - } - itf { - id - ... on User { - id2: id - name - } - } -} - diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-7.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-7.snap deleted file mode 100644 index f521439d55..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment-7.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/scopes.rs -expression: paths ---- -[] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment.snap index eff331c158..9c3b1d28f0 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment.snap @@ -1,8 +1,32 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: doc +expression: "TestResult {\n query: QUERY,\n extracted_scopes: &extracted_scopes,\n scopes: Vec::new(),\n result: doc,\n paths,\n}" --- -{ - "read:user", - "read:username", +query: + + query { + topProducts { + type + } + itf { + id + ... on User { + id2: id + name + } + } + } + +extracted_scopes: {"read:user", "read:username"} +request scopes: [] +filtered: +query { + topProducts { + type + } + itf { + id + } } + +paths: ["/itf/... on User"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-10.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-10.snap deleted file mode 100644 index 359df81d2d..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-10.snap +++ /dev/null @@ -1,16 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/scopes.rs -expression: paths ---- -[ - Path( - [ - Key( - "itf", - ), - Fragment( - "B", - ), - ], - ), -] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-2.snap index a3f411de5c..de3cbb4aa1 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-2.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-2.snap @@ -1,13 +1,21 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: paths +expression: "TestResult {\n query: QUERY,\n extracted_scopes: &extracted_scopes,\n scopes: [\"itf\".to_string()].into_iter().collect(),\n result: doc,\n paths,\n}" --- -[ - Path( - [ - Key( - "itf", - ), - ], - ), -] +query: + + query { + test + itf { + id + } + } + +extracted_scopes: {"itf"} +request scopes: ["itf"] +filtered: +query { + test +} + +paths: ["/itf"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-3.snap index dd76a2fa47..0a3890e0cd 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-3.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-3.snap @@ -1,8 +1,26 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: doc +expression: "TestResult {\n query: QUERY2,\n extracted_scopes: &extracted_scopes,\n scopes: Vec::new(),\n result: doc,\n paths,\n}" --- +query: + + query { + test + itf { + ... on A { + id + } + ... on B { + id + } + } + } + +extracted_scopes: {"a", "b", "c", "d", "itf"} +request scopes: [] +filtered: query { test } +paths: ["/itf"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-4.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-4.snap index a3f411de5c..f8a64ef87a 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-4.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-4.snap @@ -1,13 +1,26 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: paths +expression: "TestResult {\n query: QUERY2,\n extracted_scopes: &extracted_scopes,\n scopes: [\"itf\".to_string()].into_iter().collect(),\n result: doc,\n paths,\n}" --- -[ - Path( - [ - Key( - "itf", - ), - ], - ), -] +query: + + query { + test + itf { + ... on A { + id + } + ... on B { + id + } + } + } + +extracted_scopes: {"a", "b", "c", "d", "itf"} +request scopes: ["itf"] +filtered: +query { + test +} + +paths: ["/itf/... on A", "/itf/... on B"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-5.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-5.snap index dd76a2fa47..542b466aaf 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-5.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-5.snap @@ -1,8 +1,31 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: doc +expression: "TestResult {\n query: QUERY2,\n extracted_scopes: &extracted_scopes,\n scopes: [\"itf\".to_string(), \"a\".to_string(),\n \"b\".to_string()].into_iter().collect(),\n result: doc,\n paths,\n}" --- +query: + + query { + test + itf { + ... on A { + id + } + ... on B { + id + } + } + } + +extracted_scopes: {"a", "b", "c", "d", "itf"} +request scopes: ["itf", "a", "b"] +filtered: query { test + itf { + ... on A { + id + } + } } +paths: ["/itf/... on B"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-6.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-6.snap deleted file mode 100644 index a3f411de5c..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-6.snap +++ /dev/null @@ -1,13 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/scopes.rs -expression: paths ---- -[ - Path( - [ - Key( - "itf", - ), - ], - ), -] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-7.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-7.snap deleted file mode 100644 index dd76a2fa47..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-7.snap +++ /dev/null @@ -1,8 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/scopes.rs -expression: doc ---- -query { - test -} - diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-8.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-8.snap deleted file mode 100644 index 57a148cce4..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-8.snap +++ /dev/null @@ -1,26 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/scopes.rs -expression: paths ---- -[ - Path( - [ - Key( - "itf", - ), - Fragment( - "A", - ), - ], - ), - Path( - [ - Key( - "itf", - ), - Fragment( - "B", - ), - ], - ), -] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-9.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-9.snap deleted file mode 100644 index b0254ec32d..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-9.snap +++ /dev/null @@ -1,13 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/scopes.rs -expression: doc ---- -query { - test - itf { - ... on A { - id - } - } -} - diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type.snap index dd76a2fa47..40b0eb974c 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type.snap @@ -1,8 +1,21 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: doc +expression: "TestResult {\n query: QUERY,\n extracted_scopes: &extracted_scopes,\n scopes: Vec::new(),\n result: doc,\n paths,\n}" --- +query: + + query { + test + itf { + id + } + } + +extracted_scopes: {"itf"} +request scopes: [] +filtered: query { test } +paths: ["/itf"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__mutation-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__mutation-2.snap deleted file mode 100644 index 94cd4087ba..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__mutation-2.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/scopes.rs -expression: doc ---- - diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__mutation-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__mutation-3.snap deleted file mode 100644 index ea3ede98ba..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__mutation-3.snap +++ /dev/null @@ -1,13 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/scopes.rs -expression: paths ---- -[ - Path( - [ - Key( - "ping", - ), - ], - ), -] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__mutation.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__mutation.snap index 8c0ff84521..b600b33519 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__mutation.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__mutation.snap @@ -1,9 +1,17 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: doc +expression: "TestResult {\n query: QUERY,\n extracted_scopes: &extracted_scopes,\n scopes: Vec::new(),\n result: doc,\n paths,\n}" --- -{ - "ping", - "read:user", - "read:username", -} +query: + + mutation { + ping { + name + } + } + +extracted_scopes: {"ping", "read:user", "read:username"} +request scopes: [] +filtered: + +paths: ["/ping"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field-2.snap deleted file mode 100644 index 930db08570..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field-2.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/scopes.rs -expression: doc ---- -query { - topProducts { - type - } -} - diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field-3.snap deleted file mode 100644 index 70241af91d..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field-3.snap +++ /dev/null @@ -1,13 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/scopes.rs -expression: paths ---- -[ - Path( - [ - Key( - "me", - ), - ], - ), -] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field.snap index b2b2038103..bca00335b4 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field.snap @@ -1,9 +1,26 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: doc +expression: "TestResult {\n query: QUERY,\n extracted_scopes: &extracted_scopes,\n scopes: Vec::new(),\n result: doc,\n paths,\n}" --- -{ - "profile", - "read:user", - "read:username", +query: + + query { + topProducts { + type + } + + me { + name + } + } + +extracted_scopes: {"profile", "read:user", "read:username"} +request scopes: [] +filtered: +query { + topProducts { + type + } } + +paths: ["/me"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field_alias-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field_alias-2.snap deleted file mode 100644 index 930db08570..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field_alias-2.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/scopes.rs -expression: doc ---- -query { - topProducts { - type - } -} - diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field_alias-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field_alias-3.snap deleted file mode 100644 index 70241af91d..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field_alias-3.snap +++ /dev/null @@ -1,13 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/scopes.rs -expression: paths ---- -[ - Path( - [ - Key( - "me", - ), - ], - ), -] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field_alias.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field_alias.snap index b2b2038103..09af582e5c 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field_alias.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__query_field_alias.snap @@ -1,9 +1,26 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: doc +expression: "TestResult {\n query: QUERY,\n extracted_scopes: &extracted_scopes,\n scopes: Vec::new(),\n result: doc,\n paths,\n}" --- -{ - "profile", - "read:user", - "read:username", +query: + + query { + topProducts { + type + } + + moi: me { + name + } + } + +extracted_scopes: {"profile", "read:user", "read:username"} +request scopes: [] +filtered: +query { + topProducts { + type + } } + +paths: ["/me"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__scalar-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__scalar-2.snap deleted file mode 100644 index 930db08570..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__scalar-2.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/scopes.rs -expression: doc ---- -query { - topProducts { - type - } -} - diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__scalar-3.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__scalar-3.snap deleted file mode 100644 index 3b37534097..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__scalar-3.snap +++ /dev/null @@ -1,16 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/scopes.rs -expression: paths ---- -[ - Path( - [ - Key( - "topProducts", - ), - Key( - "internal", - ), - ], - ), -] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__scalar.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__scalar.snap index 8549daef56..050c8b6d15 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__scalar.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__scalar.snap @@ -1,8 +1,23 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: doc +expression: "TestResult {\n query: QUERY,\n extracted_scopes: &extracted_scopes,\n scopes: Vec::new(),\n result: doc,\n paths,\n}" --- -{ - "internal", - "test", +query: + + query { + topProducts { + type + internal + } + } + +extracted_scopes: {"internal", "test"} +request scopes: [] +filtered: +query { + topProducts { + type + } } + +paths: ["/topProducts/internal"] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__union-2.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__union-2.snap deleted file mode 100644 index 92d3c89df9..0000000000 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__union-2.snap +++ /dev/null @@ -1,16 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/scopes.rs -expression: paths ---- -[ - Path( - [ - Key( - "uni", - ), - Fragment( - "B", - ), - ], - ), -] diff --git a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__union.snap b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__union.snap index 61cb101b84..294035e909 100644 --- a/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__union.snap +++ b/apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__union.snap @@ -1,7 +1,24 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: doc +expression: "TestResult {\n query: QUERY,\n extracted_scopes: &extracted_scopes,\n scopes: [\"a\".to_string(), \"b\".to_string()].into_iter().collect(),\n result: doc,\n paths,\n}" --- +query: + + query { + test + uni { + ... on A { + id + } + ... on B { + id + } + } + } + +extracted_scopes: {"a", "b", "c", "d"} +request scopes: ["a", "b"] +filtered: query { test uni { @@ -11,3 +28,4 @@ query { } } +paths: ["/uni/... on B"] From c364e638859b999d3afd528b005158a2c194469d Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Wed, 23 Aug 2023 12:01:16 +0200 Subject: [PATCH 73/82] move the experimental option to a preview option --- apollo-router/feature_discussions.json | 7 ++++--- ...onfiguration__tests__schema_generation.snap | 14 ++++++++++---- .../testdata/metrics/authorization.router.yaml | 2 +- .../src/plugins/authorization/authenticated.rs | 12 +++++++++--- apollo-router/src/plugins/authorization/mod.rs | 18 ++++++++++++------ .../src/plugins/authorization/tests.rs | 8 ++++++-- .../src/uplink/license_enforcement.rs | 2 +- docs/source/configuration/authorization.mdx | 8 ++++++++ 8 files changed, 51 insertions(+), 20 deletions(-) diff --git a/apollo-router/feature_discussions.json b/apollo-router/feature_discussions.json index 2456d361bb..775e1b84b8 100644 --- a/apollo-router/feature_discussions.json +++ b/apollo-router/feature_discussions.json @@ -3,8 +3,9 @@ "experimental_retry": "https://github.com/apollographql/router/discussions/2241", "experimental_response_trace_id": "https://github.com/apollographql/router/discussions/2147", "experimental_logging": "https://github.com/apollographql/router/discussions/1961", - "experimental_http_max_request_bytes": "https://github.com/apollographql/router/discussions/3220", - "experimental_enable_authorization_directives": "https://github.com/apollographql/router/discussions/???" + "experimental_http_max_request_bytes": "https://github.com/apollographql/router/discussions/3220" }, - "preview": {} + "preview": { + "preview_directives": "https://github.com/apollographql/router/discussions/???" + } } \ No newline at end of file diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap index 13dd2aece6..6bd33a585d 100644 --- a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap @@ -513,10 +513,16 @@ expression: "&schema" "description": "Authorization plugin", "type": "object", "properties": { - "experimental_enable_authorization_directives": { - "description": "enables the `@authenticated`, `@requiresScopes` and `@policy` directives", - "default": false, - "type": "boolean" + "preview_directives": { + "description": "`@authenticated` and `@requiresScopes` directives", + "type": "object", + "properties": { + "enable": { + "description": "enables the `@authenticated` and `@requiresScopes` directives", + "default": false, + "type": "boolean" + } + } }, "require_authentication": { "description": "Reject unauthenticated requests", diff --git a/apollo-router/src/configuration/testdata/metrics/authorization.router.yaml b/apollo-router/src/configuration/testdata/metrics/authorization.router.yaml index db079bd4c2..5753a287e7 100644 --- a/apollo-router/src/configuration/testdata/metrics/authorization.router.yaml +++ b/apollo-router/src/configuration/testdata/metrics/authorization.router.yaml @@ -1,2 +1,2 @@ authorization: - require_authentication: true + require_authentication: true \ No newline at end of file diff --git a/apollo-router/src/plugins/authorization/authenticated.rs b/apollo-router/src/plugins/authorization/authenticated.rs index 6cc758bafe..6247b417b5 100644 --- a/apollo-router/src/plugins/authorization/authenticated.rs +++ b/apollo-router/src/plugins/authorization/authenticated.rs @@ -867,7 +867,9 @@ mod tests { "all": true }, "authorization": { - "experimental_enable_authorization_directives": true + "preview_directives": { + "enable": true + } }})) .unwrap() .schema(SCHEMA) @@ -940,7 +942,9 @@ mod tests { "all": true }, "authorization": { - "experimental_enable_authorization_directives": true + "preview_directives": { + "enable": true + } }})) .unwrap() .schema(SCHEMA) @@ -1013,7 +1017,9 @@ mod tests { "all": true }, "authorization": { - "experimental_enable_authorization_directives": true + "preview_directives": { + "enable": true + } }})) .unwrap() .schema(SCHEMA) diff --git a/apollo-router/src/plugins/authorization/mod.rs b/apollo-router/src/plugins/authorization/mod.rs index 3527e67b0d..e03d37a63f 100644 --- a/apollo-router/src/plugins/authorization/mod.rs +++ b/apollo-router/src/plugins/authorization/mod.rs @@ -68,9 +68,17 @@ pub(crate) struct Conf { /// Reject unauthenticated requests #[serde(default)] require_authentication: bool, - /// enables the `@authenticated`, `@requiresScopes` and `@policy` directives + /// `@authenticated` and `@requiresScopes` directives #[serde(default)] - experimental_enable_authorization_directives: bool, + preview_directives: Directives, +} + +#[derive(Clone, Debug, Default, Deserialize, JsonSchema)] +#[allow(dead_code)] +pub(crate) struct Directives { + /// enables the `@authenticated` and `@requiresScopes` directives + #[serde(default)] + enable: bool, } pub(crate) struct AuthorizationPlugin { @@ -87,10 +95,8 @@ impl AuthorizationPlugin { .plugins .iter() .find(|(s, _)| s.as_str() == "authorization") - .and_then(|(_, v)| { - v.get("experimental_enable_authorization_directives") - .and_then(|v| v.as_bool()) - }); + .and_then(|(_, v)| v.get("preview_directives").and_then(|v| v.as_object())) + .and_then(|v| v.get("enable").and_then(|v| v.as_bool())); let has_authorization_directives = schema .type_system .definitions diff --git a/apollo-router/src/plugins/authorization/tests.rs b/apollo-router/src/plugins/authorization/tests.rs index 161203202c..2094aecb3b 100644 --- a/apollo-router/src/plugins/authorization/tests.rs +++ b/apollo-router/src/plugins/authorization/tests.rs @@ -271,7 +271,9 @@ async fn authenticated_directive() { "all": true }, "authorization": { - "experimental_enable_authorization_directives": true + "preview_directives": { + "enable": true + } }})) .unwrap() .schema(AUTHENTICATED_SCHEMA) @@ -423,7 +425,9 @@ async fn scopes_directive() { "all": true }, "authorization": { - "experimental_enable_authorization_directives": true + "preview_directives": { + "enable": true + } }})) .unwrap() .schema(SCOPES_SCHEMA) diff --git a/apollo-router/src/uplink/license_enforcement.rs b/apollo-router/src/uplink/license_enforcement.rs index 1feaaa6e89..d6f07429d2 100644 --- a/apollo-router/src/uplink/license_enforcement.rs +++ b/apollo-router/src/uplink/license_enforcement.rs @@ -135,7 +135,7 @@ impl LicenseEnforcementReport { .name("Authentication plugin") .build(), ConfigurationRestriction::builder() - .path("$.authorization.experimental_enable_authorization_directives") + .path("$.authorization.preview_directives") .name("Authorization directives") .build(), ConfigurationRestriction::builder() diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 2caae7f53b..df4a268766 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -103,6 +103,14 @@ To provide the router with the claims it needs to evaluate authorization directi ## Authorization directives +Authorization directives are enabled in the configuration through the following option: + +```yaml title="router.yaml" +authorization: + preview_directives: + enable: true +``` + ### `@requiresScopes` The `@requiresScopes` directive marks fields and types as restricted based on required scopes. From 89764d4870600372e6b35012ec1b83b5f0756057 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Wed, 23 Aug 2023 12:05:16 +0200 Subject: [PATCH 74/82] fix snapshots --- .../snapshots/lifecycle_tests__cli_config_experimental.snap | 1 - .../tests/snapshots/lifecycle_tests__cli_config_preview.snap | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apollo-router/tests/snapshots/lifecycle_tests__cli_config_experimental.snap b/apollo-router/tests/snapshots/lifecycle_tests__cli_config_experimental.snap index 864887ca6b..8f377f6c81 100644 --- a/apollo-router/tests/snapshots/lifecycle_tests__cli_config_experimental.snap +++ b/apollo-router/tests/snapshots/lifecycle_tests__cli_config_experimental.snap @@ -9,7 +9,6 @@ stderr: stdout: List of all experimental configurations with related GitHub discussions: - - experimental_enable_authorization_directives: https://github.com/apollographql/router/discussions/??? - experimental_http_max_request_bytes: https://github.com/apollographql/router/discussions/3220 - experimental_logging: https://github.com/apollographql/router/discussions/1961 - experimental_response_trace_id: https://github.com/apollographql/router/discussions/2147 diff --git a/apollo-router/tests/snapshots/lifecycle_tests__cli_config_preview.snap b/apollo-router/tests/snapshots/lifecycle_tests__cli_config_preview.snap index c77db0d03f..7aea987112 100644 --- a/apollo-router/tests/snapshots/lifecycle_tests__cli_config_preview.snap +++ b/apollo-router/tests/snapshots/lifecycle_tests__cli_config_preview.snap @@ -7,5 +7,7 @@ Exit code: Some(0) stderr: stdout: -This Router version has no preview configuration +List of all preview configurations with related GitHub discussions: + + - preview_directives: https://github.com/apollographql/router/discussions/??? From 26b1cc47197acd392d1f0f5409829df51fce9132 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Wed, 23 Aug 2023 12:18:19 +0200 Subject: [PATCH 75/82] enable -> enabled --- ...llo_router__configuration__tests__schema_generation.snap | 2 +- apollo-router/src/plugins/authorization/authenticated.rs | 6 +++--- apollo-router/src/plugins/authorization/mod.rs | 4 ++-- apollo-router/src/plugins/authorization/tests.rs | 4 ++-- docs/source/configuration/authorization.mdx | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap index 6bd33a585d..e54c9226cd 100644 --- a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap @@ -517,7 +517,7 @@ expression: "&schema" "description": "`@authenticated` and `@requiresScopes` directives", "type": "object", "properties": { - "enable": { + "enabled": { "description": "enables the `@authenticated` and `@requiresScopes` directives", "default": false, "type": "boolean" diff --git a/apollo-router/src/plugins/authorization/authenticated.rs b/apollo-router/src/plugins/authorization/authenticated.rs index 6247b417b5..67a3c28141 100644 --- a/apollo-router/src/plugins/authorization/authenticated.rs +++ b/apollo-router/src/plugins/authorization/authenticated.rs @@ -868,7 +868,7 @@ mod tests { }, "authorization": { "preview_directives": { - "enable": true + "enabled": true } }})) .unwrap() @@ -943,7 +943,7 @@ mod tests { }, "authorization": { "preview_directives": { - "enable": true + "enabled": true } }})) .unwrap() @@ -1018,7 +1018,7 @@ mod tests { }, "authorization": { "preview_directives": { - "enable": true + "enabled": true } }})) .unwrap() diff --git a/apollo-router/src/plugins/authorization/mod.rs b/apollo-router/src/plugins/authorization/mod.rs index e03d37a63f..2ab958665d 100644 --- a/apollo-router/src/plugins/authorization/mod.rs +++ b/apollo-router/src/plugins/authorization/mod.rs @@ -78,7 +78,7 @@ pub(crate) struct Conf { pub(crate) struct Directives { /// enables the `@authenticated` and `@requiresScopes` directives #[serde(default)] - enable: bool, + enabled: bool, } pub(crate) struct AuthorizationPlugin { @@ -96,7 +96,7 @@ impl AuthorizationPlugin { .iter() .find(|(s, _)| s.as_str() == "authorization") .and_then(|(_, v)| v.get("preview_directives").and_then(|v| v.as_object())) - .and_then(|v| v.get("enable").and_then(|v| v.as_bool())); + .and_then(|v| v.get("enabled").and_then(|v| v.as_bool())); let has_authorization_directives = schema .type_system .definitions diff --git a/apollo-router/src/plugins/authorization/tests.rs b/apollo-router/src/plugins/authorization/tests.rs index 2094aecb3b..3a1f455e18 100644 --- a/apollo-router/src/plugins/authorization/tests.rs +++ b/apollo-router/src/plugins/authorization/tests.rs @@ -272,7 +272,7 @@ async fn authenticated_directive() { }, "authorization": { "preview_directives": { - "enable": true + "enabled": true } }})) .unwrap() @@ -426,7 +426,7 @@ async fn scopes_directive() { }, "authorization": { "preview_directives": { - "enable": true + "enabled": true } }})) .unwrap() diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index df4a268766..99d1fe9170 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -108,7 +108,7 @@ Authorization directives are enabled in the configuration through the following ```yaml title="router.yaml" authorization: preview_directives: - enable: true + enabled: true ``` ### `@requiresScopes` From a21e54d2af8bd1bdd8fef5e75605b3670d5220da Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Wed, 23 Aug 2023 12:18:59 +0200 Subject: [PATCH 76/82] analytics for authorization configuration --- apollo-router/src/configuration/metrics.rs | 4 +++- ...metrics__test__metrics@authorization.router.yaml.snap | 3 ++- ...st__metrics@authorization_directives.router.yaml.snap | 9 +++++++++ .../metrics/authorization_directives.router.yaml | 3 +++ 4 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__metrics@authorization_directives.router.yaml.snap create mode 100644 apollo-router/src/configuration/testdata/metrics/authorization_directives.router.yaml diff --git a/apollo-router/src/configuration/metrics.rs b/apollo-router/src/configuration/metrics.rs index ee448d379c..5fce06a6c0 100644 --- a/apollo-router/src/configuration/metrics.rs +++ b/apollo-router/src/configuration/metrics.rs @@ -144,7 +144,9 @@ impl Metrics { value.apollo.router.config.authorization, "$.authorization", opt.require_authentication, - "$[?(@.require_authentication == true)]" + "$[?(@.require_authentication == true)]", + opt.preview_directives, + "$.preview_directives[?(@.enabled == true)]" ); log_usage_metrics!( value.apollo.router.config.coprocessor, diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__metrics@authorization.router.yaml.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__metrics@authorization.router.yaml.snap index 9d5f45728b..e5eb5044cc 100644 --- a/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__metrics@authorization.router.yaml.snap +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__metrics@authorization.router.yaml.snap @@ -4,5 +4,6 @@ expression: "&metrics.metrics" --- value.apollo.router.config.authorization: - 1 - - opt__require_authentication__: "true" + - opt__preview_directives__: "false" + opt__require_authentication__: "true" diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__metrics@authorization_directives.router.yaml.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__metrics@authorization_directives.router.yaml.snap new file mode 100644 index 0000000000..9b7d6ce283 --- /dev/null +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__metrics@authorization_directives.router.yaml.snap @@ -0,0 +1,9 @@ +--- +source: apollo-router/src/configuration/metrics.rs +expression: "&metrics.metrics" +--- +value.apollo.router.config.authorization: + - 1 + - opt__preview_directives__: "true" + opt__require_authentication__: "false" + diff --git a/apollo-router/src/configuration/testdata/metrics/authorization_directives.router.yaml b/apollo-router/src/configuration/testdata/metrics/authorization_directives.router.yaml new file mode 100644 index 0000000000..02a4060ada --- /dev/null +++ b/apollo-router/src/configuration/testdata/metrics/authorization_directives.router.yaml @@ -0,0 +1,3 @@ +authorization: + preview_directives: + enabled: true \ No newline at end of file From 7d705a74247bbe05d53db42f691ab030eb50d899 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Wed, 23 Aug 2023 12:25:53 +0200 Subject: [PATCH 77/82] Apply suggestions from code review --- docs/source/configuration/authorization.mdx | 24 ++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/source/configuration/authorization.mdx b/docs/source/configuration/authorization.mdx index 99d1fe9170..8651fa67fa 100644 --- a/docs/source/configuration/authorization.mdx +++ b/docs/source/configuration/authorization.mdx @@ -75,7 +75,7 @@ For example, imagine you're building a social media platform that includes a `Us ```graphql type Query { - users: [User!]! @requiresScopes(scopes: ["read:users"]) + users: [User!]! @requiresScopes(scopes: [["read:users"]]) } ``` @@ -117,7 +117,7 @@ The `@requiresScopes` directive marks fields and types as restricted based on re To declare which scopes are required, the directive should include a `scopes` argument with an array of the required scopes. ```graphql -@requiresScopes(scopes: ["scope1", "scope2", "scope3"]) +@requiresScopes(scopes: [["scope1", "scope2", "scope3"]]) ``` Depending on the scopes present on the request, the router filters out unauthorized fields and types. @@ -178,10 +178,10 @@ extend schema #### Combining required scopes with `AND`/`OR` logic -A request must include _all_ elements in the top-level `scopes` array to resolve the associated field or type. In other words, the authorization validation uses **AND** logic between the elements in the top-level `scopes` array. +A request must include _all_ elements in the inner-level `scopes` array to resolve the associated field or type. In other words, the authorization validation uses **AND** logic between the elements in the inner-level `scopes` array. ```graphql -@requiresScopes(scopes: ["scope1", "scope2", "scope3"]) +@requiresScopes(scopes: [["scope1", "scope2", "scope3"]]) ``` For the preceding example, a request would need `scope1` **AND** `scope2` **AND** `scope3` to be authorized. @@ -210,15 +210,15 @@ Your schema may look something like this: ```graphql type Query { - user(id: ID!): User @requiresScopes(scopes: ["read:others"]) - users: [User!]! @requiresScopes(scopes: ["read:others"]) + user(id: ID!): User @requiresScopes(scopes: [["read:others"]]) + users: [User!]! @requiresScopes(scopes: [["read:others"]]) post(id: ID!): Post } type User { id: ID! username: String - email: String @requiresScopes(scopes: ["read:email"]) + email: String @requiresScopes(scopes: [["read:email"]]) profileImage: String posts: [Post!]! } @@ -263,7 +263,7 @@ The router can execute the entire query successfully if the request includes the ### `@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. +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. If the request is unauthenticated, the router removes `@authenticated` fields before planning the query and only executes the parts of the query that don't require authentication. @@ -412,7 +412,7 @@ and the `read:user` scope in another subgraph: ```graphql title="Subgraph B" type Query { - me: User @requiresScopes(scopes: ["read:user"]) + me: User @requiresScopes(scopes: [["read:user"]]) } type User { @@ -428,7 +428,7 @@ If multiple shared subgraph fields include `@requiresScopes`, the supergraph sch ```graphql title="Subgraph A" type Query { - users: [User!]! @requiresScopes(scopes: ["read:others"]) + users: [User!]! @requiresScopes(scopes: [["read:others"]]) } ``` @@ -436,7 +436,7 @@ and another subgraph requires the `read:profiles` scope on `users` query: ```graphql title="Subgraph B" type Query { - users: [User!]! @requiresScopes(scopes: ["read:profiles"]) + users: [User!]! @requiresScopes(scopes: [["read:profiles"]]) } ``` @@ -444,7 +444,7 @@ Then the supergraph schema would require _both_ scopes for it. ```graphql title="Supergraph" type Query { - users: [User!]! @requiresScopes(scopes: ["read:others", "read:profiles"]) + users: [User!]! @requiresScopes(scopes: [["read:others", "read:profiles"]]) } ``` From 13ee68b2763c673a32e76c0c6d19643f88732f49 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Wed, 23 Aug 2023 12:29:06 +0200 Subject: [PATCH 78/82] align requiresScopes tests with the main definition --- apollo-router/src/plugins/authorization/scopes.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apollo-router/src/plugins/authorization/scopes.rs b/apollo-router/src/plugins/authorization/scopes.rs index ea5655aead..ee8b491172 100644 --- a/apollo-router/src/plugins/authorization/scopes.rs +++ b/apollo-router/src/plugins/authorization/scopes.rs @@ -538,7 +538,8 @@ mod tests { use crate::spec::query::traverse; static BASIC_SCHEMA: &str = r#" - directive @requiresScopes(scopes: [[String!]!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM + scalar federation__Scope @specifiedBy(url: "http://apollographql.com") + directive @requiresScopes(scopes: [[federation__Scope!]!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM type Query { topProducts: Product From b240ea950ff56c38183812fad889100770076140 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Wed, 23 Aug 2023 12:42:52 +0200 Subject: [PATCH 79/82] remove warnings from diagnostics --- apollo-router/src/plugins/authorization/scopes.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/apollo-router/src/plugins/authorization/scopes.rs b/apollo-router/src/plugins/authorization/scopes.rs index ee8b491172..bb0409d37c 100644 --- a/apollo-router/src/plugins/authorization/scopes.rs +++ b/apollo-router/src/plugins/authorization/scopes.rs @@ -538,7 +538,7 @@ mod tests { use crate::spec::query::traverse; static BASIC_SCHEMA: &str = r#" - scalar federation__Scope @specifiedBy(url: "http://apollographql.com") + scalar federation__Scope directive @requiresScopes(scopes: [[federation__Scope!]!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM type Query { @@ -583,7 +583,11 @@ mod tests { let _schema_id = compiler.add_type_system(schema, "schema.graphql"); let id = compiler.add_executable(query, "query.graphql"); - let diagnostics = compiler.validate(); + let diagnostics = compiler + .validate() + .into_iter() + .filter(|err| err.data.is_error()) + .collect::>(); for diagnostic in &diagnostics { println!("{diagnostic}"); } @@ -622,7 +626,11 @@ mod tests { let _schema_id = compiler.add_type_system(schema, "schema.graphql"); let file_id = compiler.add_executable(query, "query.graphql"); - let diagnostics = compiler.validate(); + let diagnostics = compiler + .validate() + .into_iter() + .filter(|err| err.data.is_error()) + .collect::>(); for diagnostic in &diagnostics { println!("{diagnostic}"); } From 95906f17bb6cc6bf373cdc0e409fdafb64e6d4a4 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Wed, 23 Aug 2023 13:04:51 +0200 Subject: [PATCH 80/82] Update .changesets/feat_geal_authorization_directives.md --- .changesets/feat_geal_authorization_directives.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.changesets/feat_geal_authorization_directives.md b/.changesets/feat_geal_authorization_directives.md index 3af6e10851..ba9464d768 100644 --- a/.changesets/feat_geal_authorization_directives.md +++ b/.changesets/feat_geal_authorization_directives.md @@ -7,7 +7,8 @@ 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 +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: From c78c3cf1f2c5467d3e3b569a113a3afd887022a3 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Wed, 23 Aug 2023 13:33:18 +0200 Subject: [PATCH 81/82] Update apollo-router/src/configuration/metrics.rs Co-authored-by: Bryn Cooke --- apollo-router/src/configuration/metrics.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apollo-router/src/configuration/metrics.rs b/apollo-router/src/configuration/metrics.rs index 5fce06a6c0..90b5c8753a 100644 --- a/apollo-router/src/configuration/metrics.rs +++ b/apollo-router/src/configuration/metrics.rs @@ -145,7 +145,7 @@ impl Metrics { "$.authorization", opt.require_authentication, "$[?(@.require_authentication == true)]", - opt.preview_directives, + opt.directives, "$.preview_directives[?(@.enabled == true)]" ); log_usage_metrics!( From 7e845dc7c8c845309c1ecc6f49c89ff32c13e4eb Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Wed, 23 Aug 2023 14:29:05 +0200 Subject: [PATCH 82/82] snapshots --- ...ation__metrics__test__metrics@authorization.router.yaml.snap | 2 +- ...ics__test__metrics@authorization_directives.router.yaml.snap | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__metrics@authorization.router.yaml.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__metrics@authorization.router.yaml.snap index e5eb5044cc..11f9160614 100644 --- a/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__metrics@authorization.router.yaml.snap +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__metrics@authorization.router.yaml.snap @@ -4,6 +4,6 @@ expression: "&metrics.metrics" --- value.apollo.router.config.authorization: - 1 - - opt__preview_directives__: "false" + - opt__directives__: "false" opt__require_authentication__: "true" diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__metrics@authorization_directives.router.yaml.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__metrics@authorization_directives.router.yaml.snap index 9b7d6ce283..61b5d4c144 100644 --- a/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__metrics@authorization_directives.router.yaml.snap +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__metrics__test__metrics@authorization_directives.router.yaml.snap @@ -4,6 +4,6 @@ expression: "&metrics.metrics" --- value.apollo.router.config.authorization: - 1 - - opt__preview_directives__: "true" + - opt__directives__: "true" opt__require_authentication__: "false"