CVE-2025-64173
Description
Apollo Router Core is a configurable graph router written in Rust to run a federated supergraph using Apollo Federation 2. In versions 1.61.11 below, as well as 2.0.0-alpha.0 through 2.8.1-rc.0, a vulnerability allowed for unauthenticated queries to access data that required additional access controls. Router incorrectly handled access control directives on interface types/fields and their implementing object types/fields, applying them to interface types/fields while ignoring directives on their implementing object types/fields when all implementations had the same requirements. Apollo Router customers defining @authenticated, @requiresScopes, or @policy directives inconsistently on polymorphic types (i.e., object types that implement interface types) are impacted. This issue is fixed in versions 1.61.12 and 2.8.1.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
apollo-routercrates.io | < 1.61.12 | 1.61.12 |
apollo-routercrates.io | >= 2.0.0-alpha.0, < 2.8.1 | 2.8.1 |
Affected products
1- Range: v0.1.0-alpha.0, v0.1.0-alpha.1, v0.1.0-alpha.10, …
Patches
175ca43ecb9d3fix: update auth plugin handling of polymorphic types
37 files changed · +863 −668
apollo-router/src/plugins/authorization/authenticated.rs+211 −35 modified@@ -232,7 +232,7 @@ impl<'a> AuthenticatedVisitor<'a> { .flatten() } - fn implementors_with_different_requirements( + fn implementors_with_authenticated_requirements( &self, field_def: &ast::FieldDefinition, node: &ast::Field, @@ -253,64 +253,45 @@ impl<'a> AuthenticatedVisitor<'a> { let type_name = field_def.ty.inner_named_type(); if let Some(type_definition) = self.schema.types.get(type_name) - && self.implementors_with_different_type_requirements(type_name, type_definition) + && self.implementors_with_authenticated_type_requirements(type_name, type_definition) { return true; } false } - fn implementors_with_different_type_requirements( + fn implementors_with_authenticated_type_requirements( &self, type_name: &str, t: &schema::ExtendedType, ) -> bool { if t.is_interface() { - let mut is_authenticated: Option<bool> = None; - for ty in self .implementors(type_name) .filter_map(|ty| self.schema.types.get(ty)) { - let ty_is_authenticated = ty.directives().has(&self.authenticated_directive_name); - 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; - } - } + if self.is_type_authenticated(ty) { + return true; } } } false } - fn implementors_with_different_field_requirements( + fn implementors_with_authenticated_field_requirements( &self, parent_type: &str, field: &ast::Field, ) -> bool { if let Some(t) = self.schema.types.get(parent_type) && t.is_interface() { - let mut is_authenticated: Option<bool> = None; - for ty in self.implementors(parent_type) { - if let Ok(f) = self.schema.type_field(ty, &field.name) { - let field_is_authenticated = - f.directives.has(&self.authenticated_directive_name); - match is_authenticated { - Some(other) => { - if field_is_authenticated != other { - return true; - } - } - _ => { - is_authenticated = Some(field_is_authenticated); - } - } + if let Ok(f) = self.schema.type_field(ty, &field.name) + && self.is_field_authenticated(f) + { + return true; } } } @@ -359,15 +340,15 @@ impl transform::Visitor for AuthenticatedVisitor<'_> { self.current_path.push(PathElement::Flatten(None)); } - let implementors_with_different_requirements = - self.implementors_with_different_requirements(field_def, node); + let implementors_with_authenticated_requirements = + self.implementors_with_authenticated_requirements(field_def, node); - let implementors_with_different_field_requirements = - self.implementors_with_different_field_requirements(parent_type, node); + let implementors_with_authenticated_field_requirements = + self.implementors_with_authenticated_field_requirements(parent_type, node); let res = if field_requires_authentication - || implementors_with_different_requirements - || implementors_with_different_field_requirements + || implementors_with_authenticated_requirements + || implementors_with_authenticated_field_requirements { self.unauthorized_paths.push(self.current_path.clone()); self.query_requires_authentication = true; @@ -1996,4 +1977,199 @@ mod tests { assert!(response.next_response().await.is_none()); } + + #[test] + fn implementations_with_same_auth() { + static SCHEMA: &str = r#" + schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/authenticated/v0.1", for: SECURITY) + { + query: Query + } + directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + directive @authenticated on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM + directive @defer on INLINE_FRAGMENT | FRAGMENT_SPREAD + scalar link__Import + enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION + } + type Query { + test: String + intf(id: ID!): I + } + + interface I { + id: ID! + name: String + } + + type T implements I @authenticated { + id: ID! + name: String + } + + type U implements I @authenticated { + id: ID! + name: String + } + "#; + + static QUERY: &str = r#" + query Anonymous { + test + intf(id: "1") { + id + name + } + } + "#; + + let (doc, paths) = filter(SCHEMA, QUERY); + + insta::assert_snapshot!(TestResult { + query: QUERY, + result: doc, + paths + }); + } + + #[test] + fn implementations_with_different_field_auth() { + static SCHEMA: &str = r#" + schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/authenticated/v0.1", for: SECURITY) + { + query: Query + } + directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + directive @authenticated on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM + directive @defer on INLINE_FRAGMENT | FRAGMENT_SPREAD + scalar link__Import + enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION + } + type Query { + test: String + intf(id: ID!): I + } + + interface I { + id: ID! + name: String + } + + type T implements I { + id: ID! @authenticated + name: String + } + + type U implements I { + id: ID! + name: String + } + "#; + + static QUERY: &str = r#" + query Anonymous { + test + intf(id: "1") { + id + name + } + } + "#; + + let (doc, paths) = filter(SCHEMA, QUERY); + + insta::assert_snapshot!(TestResult { + query: QUERY, + result: doc, + paths + }); + } + + #[test] + fn implementations_with_different_type_auth() { + static SCHEMA: &str = r#" + schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/authenticated/v0.1", for: SECURITY) + { + query: Query + } + directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + directive @authenticated on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM + directive @defer on INLINE_FRAGMENT | FRAGMENT_SPREAD + scalar link__Import + enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION + } + type Query { + test: String + intf(id: ID!): I + } + + interface I { + id: ID! + name: String + } + + type T implements I @authenticated { + id: ID! + name: String + } + + type U implements I { + id: ID! + name: String + } + "#; + + static QUERY: &str = r#" + query Anonymous { + test + intf(id: "1") { + id + name + } + } + "#; + + let (doc, paths) = filter(SCHEMA, QUERY); + + insta::assert_snapshot!(TestResult { + query: QUERY, + result: doc, + paths + }); + } }
apollo-router/src/plugins/authorization/policy.rs+117 −164 modified@@ -245,7 +245,7 @@ impl<'a> PolicyFilteringVisitor<'a> { }) } - fn is_field_authorized(&mut self, field: &schema::FieldDefinition) -> bool { + fn is_field_authorized(&self, field: &schema::FieldDefinition) -> bool { if let Some(directive) = field.directives.get(&self.policy_directive_name) { let mut field_policies_sets = policies_sets_argument(directive); @@ -297,7 +297,7 @@ impl<'a> PolicyFilteringVisitor<'a> { .flatten() } - fn implementors_with_different_requirements( + fn implementors_with_missing_requirements( &self, field_def: &ast::FieldDefinition, node: &ast::Field, @@ -318,96 +318,50 @@ impl<'a> PolicyFilteringVisitor<'a> { let type_name = field_def.ty.inner_named_type(); if let Some(type_definition) = self.schema.types.get(type_name) - && self.implementors_with_different_type_requirements(type_name, type_definition) + && self.implementors_with_missing_type_requirements(type_name, type_definition) { return true; } false } - fn implementors_with_different_type_requirements( + fn implementors_with_missing_type_requirements( &self, type_name: &str, t: &schema::ExtendedType, ) -> bool { if t.is_interface() { - let mut policies_sets: Option<Vec<Vec<String>>> = None; - + // we are querying the interface directly so we need to check if all implementations + // have right policies for ty in self .implementors(type_name) .filter_map(|ty| self.schema.types.get(ty)) { - // aggregate the list of policies sets - // we transform to a common representation of sorted vectors because the element order - // of hashsets is not stable - let ty_policies_sets = ty - .directives() - .get(&self.policy_directive_name) - .map(|directive| { - let mut v = policies_sets_argument(directive) - .map(|h| { - let mut v = h.into_iter().collect::<Vec<_>>(); - v.sort(); - v - }) - .collect::<Vec<_>>(); - v.sort(); - v - }) - .unwrap_or_default(); - - match &policies_sets { - None => policies_sets = Some(ty_policies_sets), - Some(other_policies) => { - if ty_policies_sets != *other_policies { - return true; - } - } - } + // verify whether the type specifies policies that match the requirements + if !self.is_type_authorized(ty) { + return true; + }; } } false } - fn implementors_with_different_field_requirements( + fn implementors_with_missing_field_requirements( &self, parent_type: &str, field: &ast::Field, ) -> bool { + // we are querying the interface directly so we need to check if all implementations + // have right policies if let Some(t) = self.schema.types.get(parent_type) && t.is_interface() { - let mut policies_sets: Option<Vec<Vec<String>>> = None; - for ty in self.implementors(parent_type) { if let Ok(f) = self.schema.type_field(ty, &field.name) { - // aggregate the list of policies sets - // we transform to a common representation of sorted vectors because the element order - // of hashsets is not stable - let field_policies = f - .directives - .get(&self.policy_directive_name) - .map(|directive| { - let mut v = policies_sets_argument(directive) - .map(|h| { - let mut v = h.into_iter().collect::<Vec<_>>(); - v.sort(); - v - }) - .collect::<Vec<_>>(); - v.sort(); - v - }) - .unwrap_or_default(); - - match &policies_sets { - None => policies_sets = Some(field_policies), - Some(other_policies) => { - if field_policies != *other_policies { - return true; - } - } + // verify whether target field specifies policies that match the requirements + if !self.is_field_authorized(f) { + return true; } } } @@ -469,23 +423,21 @@ impl transform::Visitor for PolicyFilteringVisitor<'_> { let is_authorized = self.is_field_authorized(field_def); - let implementors_with_different_requirements = - self.implementors_with_different_requirements(field_def, node); + let implementors_with_missing_requirements = + self.implementors_with_missing_requirements(field_def, node); - let implementors_with_different_field_requirements = - self.implementors_with_different_field_requirements(parent_type, node); + let implementors_with_missing_field_requirements = + self.implementors_with_missing_field_requirements(parent_type, node); self.current_path .push(PathElement::Key(field_name.as_str().into(), None)); if is_field_list { self.current_path.push(PathElement::Flatten(None)); } - let res = if is_authorized - && !implementors_with_different_requirements - && !implementors_with_different_field_requirements + let res = if !is_authorized + || implementors_with_missing_requirements + || implementors_with_missing_field_requirements { - transform::field(self, field_def, node) - } else { self.unauthorized_paths.push(self.current_path.clone()); self.query_requires_policies = true; @@ -494,6 +446,8 @@ impl transform::Visitor for PolicyFilteringVisitor<'_> { } else { Ok(None) } + } else { + transform::field(self, field_def, node) }; if is_field_list { @@ -1191,7 +1145,7 @@ mod tests { test: String itf: I! } - interface I @policy(policies: [["itf"]]) { + interface I { id: ID } type A implements I @policy(policies: [["a"]]) { @@ -1225,19 +1179,6 @@ mod tests { paths }); - let (doc, paths) = filter( - INTERFACE_SCHEMA, - QUERY, - ["itf".to_string()].into_iter().collect(), - ); - insta::assert_snapshot!(TestResult { - query: QUERY, - extracted_policies: &extracted_policies, - successful_policies: ["itf".to_string()].into_iter().collect(), - result: doc, - paths - }); - static QUERY2: &str = r#" query { test @@ -1265,25 +1206,12 @@ mod tests { let (doc, paths) = filter( INTERFACE_SCHEMA, QUERY2, - ["itf".to_string()].into_iter().collect(), - ); - insta::assert_snapshot!(TestResult { - query: QUERY2, - extracted_policies: &extracted_policies, - successful_policies: ["itf".to_string()].into_iter().collect(), - result: doc, - paths - }); - - let (doc, paths) = filter( - INTERFACE_SCHEMA, - QUERY2, - ["itf".to_string(), "a".to_string()].into_iter().collect(), + ["a".to_string()].into_iter().collect(), ); insta::assert_snapshot!(TestResult { query: QUERY2, extracted_policies: &extracted_policies, - successful_policies: ["itf".to_string(), "a".to_string()].into_iter().collect(), + successful_policies: ["a".to_string()].into_iter().collect(), result: doc, paths }); @@ -1334,7 +1262,7 @@ mod tests { "#; #[test] - fn interface_with_implementor_not_defining_field_level_policy() { + fn implementations_with_different_field_policies() { static SCHEMA: &str = r#" schema @link(url: "https://specs.apollo.dev/link/v1.0") @@ -1361,7 +1289,7 @@ mod tests { index(id: ID!): ParentInterface } interface ParentInterface { - index: Int @policy(policies: [["read"]]) + index: Int } type LeftChildInterface implements ParentInterface { index: Int @policy(policies: [["read"]]) @@ -1383,6 +1311,16 @@ mod tests { } "#; let extracted_policies = extract(SCHEMA, QUERY); + // an empty set of policies represents a request with _no_ policies + let (doc, paths) = filter(SCHEMA, QUERY, HashSet::new()); + insta::assert_snapshot!(TestResult { + query: QUERY, + extracted_policies: &extracted_policies, + successful_policies: vec![], + result: doc, + paths + }); + let read_policy: HashSet<String> = ["read".to_string()].into_iter().collect(); let (doc, paths) = filter(SCHEMA, QUERY, read_policy); insta::assert_snapshot!(TestResult { @@ -1396,8 +1334,7 @@ mod tests { } #[test] - fn interface_with_implementor_not_defining_field_level_policy_with_request_not_sending_policies() - { + fn implementations_with_different_type_policies() { static SCHEMA: &str = r#" schema @link(url: "https://specs.apollo.dev/link/v1.0") @@ -1424,15 +1361,15 @@ mod tests { index(id: ID!): ParentInterface } interface ParentInterface { - index: Int @policy(policies: [["read"]]) + index: Int } - type LeftChildInterface implements ParentInterface { - index: Int @policy(policies: [["read"]]) + type LeftChildInterface implements ParentInterface @policy(policies: [["read"]]){ + index: Int } - type RightSiblingInterface implements ParentInterface { - index: Int @policy(policies: [["read"]]) + type RightSiblingInterface implements ParentInterface @policy(policies: [["read"]]){ + index: Int } - # NOTE: this doesn't have a field-level @policy + # NOTE: this doesn't have a type-level @policy type ChildInterface implements ParentInterface { index: Int } @@ -1446,67 +1383,16 @@ mod tests { } "#; let extracted_policies = extract(SCHEMA, QUERY); - // an empty set of policies represents a request with _no_ policies - let (doc, paths) = filter(SCHEMA, QUERY, HashSet::new()); + let (doc, paths) = filter(SCHEMA, QUERY, HashSet::default()); insta::assert_snapshot!(TestResult { query: QUERY, + // this should have `read` as the extracted policy extracted_policies: &extracted_policies, - successful_policies: vec![], + successful_policies: Vec::new(), result: doc, paths }); - } - - #[test] - fn interface_with_implementor_not_defining_type_level_policy() { - static SCHEMA: &str = r#" - schema - @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) - @link(url: "https://specs.apollo.dev/policy/v0.1", for: SECURITY) - { - query: Query - } - directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA - directive @policy(policies: [[String!]!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM - scalar link__Import - enum link__Purpose { - """ - `SECURITY` features provide metadata necessary to securely resolve fields. - """ - SECURITY - - """ - `EXECUTION` features provide metadata necessary for operation execution. - """ - EXECUTION - } - type Query { - index(id: ID!): ParentInterface - } - interface ParentInterface { - index: Int @policy(policies: [["read"]]) - } - type LeftChildInterface implements ParentInterface @policy(policies: [["read"]]){ - index: Int @policy(policies: [["read"]]) - } - type RightSiblingInterface implements ParentInterface @policy(policies: [["read"]]){ - index: Int @policy(policies: [["read"]]) - } - # NOTE: this doesn't have a type-level @policy - type ChildInterface implements ParentInterface { - index: Int - } - "#; - static QUERY: &str = r#" - query TestIssue { - index(id: "1") { - index - } - } - "#; - let extracted_policies = extract(SCHEMA, QUERY); let read_policy: HashSet<String> = ["read".to_string()].into_iter().collect(); let (doc, paths) = filter(SCHEMA, QUERY, read_policy); insta::assert_snapshot!(TestResult { @@ -1807,4 +1693,71 @@ mod tests { insta::assert_snapshot!(doc); insta::assert_debug_snapshot!(paths); } + + #[test] + fn implementations_with_same_policies() { + static SCHEMA: &str = r#" + schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/policy/v0.1", for: SECURITY) + { + query: Query + } + directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + directive @policy(policies: [[String!]!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM + directive @defer on INLINE_FRAGMENT | FRAGMENT_SPREAD + scalar link__Import + enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION + } + + 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: [["a"]]) + other: String + b: String + } + "#; + + static QUERY: &str = r#" + query { + test + itf { + id + other + } + } + "#; + + let extracted_policies = extract(SCHEMA, QUERY); + let (doc, paths) = filter(SCHEMA, QUERY, HashSet::new()); + insta::assert_snapshot!(TestResult { + query: QUERY, + extracted_policies: &extracted_policies, + successful_policies: Vec::new(), + result: doc, + paths + }); + } }
apollo-router/src/plugins/authorization/scopes.rs+241 −111 modified@@ -244,7 +244,7 @@ impl<'a> ScopeFilteringVisitor<'a> { }) } - fn is_field_authorized(&mut self, field: &schema::FieldDefinition) -> bool { + fn is_field_authorized(&self, field: &schema::FieldDefinition) -> bool { if let Some(directive) = field.directives.get(&self.requires_scopes_directive_name) { let mut field_scopes_sets = scopes_sets_argument(directive); @@ -296,7 +296,7 @@ impl<'a> ScopeFilteringVisitor<'a> { .flatten() } - fn implementors_with_different_requirements( + fn implementors_with_missing_requirements( &self, field_def: &ast::FieldDefinition, node: &ast::Field, @@ -316,100 +316,47 @@ impl<'a> ScopeFilteringVisitor<'a> { return false; } - let field_type = field_def.ty.inner_named_type(); - if let Some(type_definition) = self.schema.types.get(field_type) - && self.implementors_with_different_type_requirements(field_def, type_definition) + let type_name = field_def.ty.inner_named_type(); + if let Some(type_definition) = self.schema.types.get(type_name) + && self.implementors_with_missing_type_requirements(type_name, type_definition) { return true; } false } - fn implementors_with_different_type_requirements( + fn implementors_with_missing_type_requirements( &self, - field_def: &ast::FieldDefinition, + type_name: &str, t: &schema::ExtendedType, ) -> bool { if t.is_interface() { - let mut scope_sets = None; - let type_name = field_def.ty.inner_named_type(); - for ty in self .implementors(type_name) .filter_map(|ty| self.schema.types.get(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 - .directives() - .get(&self.requires_scopes_directive_name) - .map(|directive| { - let mut v = scopes_sets_argument(directive) - .map(|h| { - let mut v = h.into_iter().collect::<Vec<_>>(); - v.sort(); - v - }) - .collect::<Vec<_>>(); - 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; - } - } + if !self.is_type_authorized(ty) { + return true; } } } false } - fn implementors_with_different_field_requirements( + fn implementors_with_missing_field_requirements( &self, parent_type: &str, field: &ast::Field, ) -> bool { if let Some(t) = self.schema.types.get(parent_type) && t.is_interface() { - let mut scope_sets = None; - for ty in self.implementors(parent_type) { - if let Ok(f) = self.schema.type_field(ty, &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 - .directives - .get(&self.requires_scopes_directive_name) - .map(|directive| { - let mut v = scopes_sets_argument(directive) - .map(|h| { - let mut v = h.into_iter().collect::<Vec<_>>(); - v.sort(); - v - }) - .collect::<Vec<_>>(); - 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; - } - } - } + if let Ok(f) = self.schema.type_field(ty, &field.name) + && !self.is_field_authorized(f) + { + return true; } } } @@ -472,23 +419,21 @@ impl transform::Visitor for ScopeFilteringVisitor<'_> { let is_authorized = self.is_field_authorized(field_def); - let implementors_with_different_requirements = - self.implementors_with_different_requirements(field_def, node); + let implementors_with_missing_requirements = + self.implementors_with_missing_requirements(field_def, node); - let implementors_with_different_field_requirements = - self.implementors_with_different_field_requirements(parent_type, node); + let implementors_with_missing_field_requirements = + self.implementors_with_missing_field_requirements(parent_type, node); self.current_path .push(PathElement::Key(field_name.as_str().into(), None)); if is_field_list { self.current_path.push(PathElement::Flatten(None)); } - let res = if is_authorized - && !implementors_with_different_requirements - && !implementors_with_different_field_requirements + let res = if !is_authorized + || implementors_with_missing_requirements + || implementors_with_missing_field_requirements { - transform::field(self, field_def, node) - } else { self.unauthorized_paths.push(self.current_path.clone()); self.query_requires_scopes = true; @@ -497,6 +442,8 @@ impl transform::Visitor for ScopeFilteringVisitor<'_> { } else { Ok(None) } + } else { + transform::field(self, field_def, node) }; if is_field_list { @@ -1192,7 +1139,7 @@ mod tests { test: String itf: I! } - interface I @requiresScopes(scopes: [["itf"]]) { + interface I { id: ID } type A implements I @requiresScopes(scopes: [["a", "b"]]) { @@ -1227,20 +1174,6 @@ mod tests { paths }); - let (doc, paths) = filter( - INTERFACE_SCHEMA, - QUERY, - ["itf".to_string()].into_iter().collect(), - ); - - insta::assert_snapshot!(TestResult { - query: QUERY, - extracted_scopes: &extracted_scopes, - scopes: ["itf".to_string()].into_iter().collect(), - result: doc, - paths - }); - static QUERY2: &str = r#" query { test @@ -1269,31 +1202,13 @@ mod tests { let (doc, paths) = filter( INTERFACE_SCHEMA, QUERY2, - ["itf".to_string()].into_iter().collect(), - ); - - insta::assert_snapshot!(TestResult { - query: QUERY2, - extracted_scopes: &extracted_scopes, - scopes: ["itf".to_string()].into_iter().collect(), - result: doc, - paths - }); - - let (doc, paths) = filter( - INTERFACE_SCHEMA, - QUERY2, - ["itf".to_string(), "a".to_string(), "b".to_string()] - .into_iter() - .collect(), + ["a".to_string(), "b".to_string()].into_iter().collect(), ); insta::assert_snapshot!(TestResult { query: QUERY2, extracted_scopes: &extracted_scopes, - scopes: ["itf".to_string(), "a".to_string(), "b".to_string()] - .into_iter() - .collect(), + scopes: ["a".to_string(), "b".to_string()].into_iter().collect(), result: doc, paths }); @@ -1658,4 +1573,219 @@ mod tests { paths }); } + + #[test] + fn implementations_with_same_scopes() { + static SCHEMA: &str = r#" + schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/requiresScopes/v0.1", for: SECURITY) + { + query: Query + } + directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + directive @requiresScopes(scopes: [[String!]!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM + directive @defer on INLINE_FRAGMENT | FRAGMENT_SPREAD + scalar link__Import + enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION + } + 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: [["a", "b"]]) + other: String + b: String + } + "#; + + static QUERY: &str = r#" + query { + test + itf { + id + other + } + } + "#; + + let extracted_scopes: BTreeSet<String> = extract(SCHEMA, QUERY); + let (doc, paths) = filter(SCHEMA, QUERY, HashSet::new()); + + insta::assert_snapshot!(TestResult { + query: QUERY, + extracted_scopes: &extracted_scopes, + scopes: Vec::new(), + result: doc, + paths + }); + } + + #[test] + fn implementations_with_different_field_scopes() { + static SCHEMA: &str = r#" + schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/requiresScopes/v0.1", for: SECURITY) + { + query: Query + } + directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + directive @requiresScopes(scopes: [[String!]!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM + directive @defer on INLINE_FRAGMENT | FRAGMENT_SPREAD + scalar link__Import + enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION + } + type Query { + test: String + itf: I! + } + interface I { + id: ID + } + type A implements I { + id: ID @requiresScopes(scopes: [["read"]]) + a: String + } + # does not have field level @requiresScopes + type B implements I { + id: ID + b: String + } + "#; + + static QUERY: &str = r#" + query TestIssue { + test + itf { + id + } + } + "#; + let extracted_scopes = extract(SCHEMA, QUERY); + // an empty set of scopes represents a request with _no_ scopes + let (doc, paths) = filter(SCHEMA, QUERY, HashSet::new()); + insta::assert_snapshot!(TestResult { + query: QUERY, + extracted_scopes: &extracted_scopes, + scopes: vec![], + result: doc, + paths + }); + + let read_scope: HashSet<String> = ["read".to_string()].into_iter().collect(); + let (doc, paths) = filter(SCHEMA, QUERY, read_scope); + insta::assert_snapshot!(TestResult { + query: QUERY, + // this should have `read` as the extracted policy + extracted_scopes: &extracted_scopes, + scopes: vec!["read".to_string()], + result: doc, + paths + }); + } + + #[test] + fn implementations_with_different_type_scopes() { + static SCHEMA: &str = r#" + schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) + @link(url: "https://specs.apollo.dev/requiresScopes/v0.1", for: SECURITY) + { + query: Query + } + directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + directive @requiresScopes(scopes: [[String!]!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM + directive @defer on INLINE_FRAGMENT | FRAGMENT_SPREAD + scalar link__Import + enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION + } + type Query { + test: String + itf: I! + } + interface I { + id: ID + } + type A implements I @requiresScopes(scopes: [["read"]]) { + id: ID + a: String + } + # does not have type level @requiresScopes + type B implements I { + id: ID + b: String + } + "#; + + static QUERY: &str = r#" + query TestIssue { + test + itf { + id + } + } + "#; + let extracted_scopes = extract(SCHEMA, QUERY); + let (doc, paths) = filter(SCHEMA, QUERY, HashSet::default()); + insta::assert_snapshot!(TestResult { + query: QUERY, + // this should have `read` as the extracted policy + extracted_scopes: &extracted_scopes, + scopes: Vec::new(), + result: doc, + paths + }); + + let read_scope: HashSet<String> = ["read".to_string()].into_iter().collect(); + let (doc, paths) = filter(SCHEMA, QUERY, read_scope); + insta::assert_snapshot!(TestResult { + query: QUERY, + // this should have `read` as the extracted policy + extracted_scopes: &extracted_scopes, + scopes: vec!["read".to_string()], + result: doc, + paths + }); + } }
apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__implementations_with_different_field_auth.snap+23 −0 added@@ -0,0 +1,23 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: "TestResult { query: QUERY, result: doc, paths }" +--- +query: + + query Anonymous { + test + intf(id: "1") { + id + name + } + } + +filtered: +query Anonymous { + test + intf(id: "1") { + name + } +} + +paths: ["/intf/id"]
apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__implementations_with_different_type_auth.snap+20 −0 added@@ -0,0 +1,20 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: "TestResult { query: QUERY, result: doc, paths }" +--- +query: + + query Anonymous { + test + intf(id: "1") { + id + name + } + } + +filtered: +query Anonymous { + test +} + +paths: ["/intf"]
apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__implementations_with_same_auth.snap+20 −0 added@@ -0,0 +1,20 @@ +--- +source: apollo-router/src/plugins/authorization/authenticated.rs +expression: "TestResult { query: QUERY, result: doc, paths }" +--- +query: + + query Anonymous { + test + intf(id: "1") { + id + name + } + } + +filtered: +query Anonymous { + test +} + +paths: ["/intf"]
apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_fragment.snap+0 −3 modified@@ -23,9 +23,6 @@ filtered: topProducts { type } - itf { - id - } } paths: ["/itf"]
apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__authenticated__tests__interface_inline_fragment.snap+1 −4 modified@@ -21,9 +21,6 @@ filtered: topProducts { type } - itf { - id - } } -paths: ["/itf/... on User"] +paths: ["/itf"]
apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__implementations_with_different_field_policies-2.snap+5 −5 renamed@@ -5,17 +5,17 @@ expression: "TestResult\n{\n query: QUERY, extracted_policies: &extracted_pol query: query TestIssue { - idList(id: "1") { - activeId + index(id: "1") { + index } } -extracted_policies: {"read"} +extracted_policies: {} successful policies: ["read"] filtered: query TestIssue { - idList(id: "1") { - activeId + index(id: "1") { + index } }
apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__implementations_with_different_field_policies.snap+2 −2 renamed@@ -10,8 +10,8 @@ query: } } -extracted_policies: {"read"} -successful policies: ["read"] +extracted_policies: {} +successful policies: [] filtered: paths: ["/index/index"]
apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__implementations_with_different_type_policies-2.snap+9 −4 renamed@@ -1,6 +1,6 @@ --- source: apollo-router/src/plugins/authorization/policy.rs -expression: "TestResult\n{\n query: QUERY, extracted_policies: &extracted_policies,\n successful_policies: vec![], result: doc, paths\n}" +expression: "TestResult\n{\n query: QUERY, extracted_policies: &extracted_policies,\n successful_policies: vec![\"read\".to_string()], result: doc, paths\n}" --- query: @@ -10,8 +10,13 @@ query: } } -extracted_policies: {"read"} -successful policies: [] +extracted_policies: {} +successful policies: ["read"] filtered: +query TestIssue { + index(id: "1") { + index + } +} -paths: ["/index/index"] +paths: []
apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__implementations_with_different_type_policies.snap+2 −2 renamed@@ -10,8 +10,8 @@ query: } } -extracted_policies: {"read"} -successful policies: ["read"] +extracted_policies: {} +successful policies: [] filtered: paths: ["/index"]
apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__implementations_with_same_policies.snap+14 −6 renamed@@ -4,14 +4,22 @@ expression: "TestResult\n{\n query: QUERY, extracted_policies: &extracted_pol --- query: - query TestIssue { - index(id: "1") { - index - } + query { + test + itf { + id + other } + } -extracted_policies: {"read"} +extracted_policies: {} successful policies: [] filtered: +{ + test + itf { + other + } +} -paths: ["/index/index"] +paths: ["/itf/id"]
apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_fragment.snap+0 −3 modified@@ -25,9 +25,6 @@ filtered: topProducts { type } - itf { - id - } } paths: ["/itf"]
apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_inline_fragment.snap+1 −4 modified@@ -23,9 +23,6 @@ filtered: topProducts { type } - itf { - id - } } -paths: ["/itf/... on User"] +paths: ["/itf"]
apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-2.snap+10 −5 modified@@ -1,21 +1,26 @@ --- source: apollo-router/src/plugins/authorization/policy.rs -expression: "TestResult {\n query: QUERY,\n extracted_policies: &extracted_policies,\n successful_policies: [\"itf\".to_string()].into_iter().collect(),\n result: doc,\n paths,\n}" +expression: "TestResult\n{\n query: QUERY2, extracted_policies: &extracted_policies,\n successful_policies: Vec::new(), result: doc, paths\n}" --- query: query { test itf { - id + ... on A { + id + } + ... on B { + id + } } } -extracted_policies: {"itf"} -successful policies: ["itf"] +extracted_policies: {"a", "b"} +successful policies: [] filtered: { test } -paths: ["/itf"] +paths: ["/itf/... on A", "/itf/... on B"]
apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-3.snap+10 −4 modified@@ -1,6 +1,7 @@ --- source: apollo-router/src/plugins/authorization/policy.rs -expression: "TestResult {\n query: QUERY2,\n extracted_policies: &extracted_policies,\n successful_policies: Vec::new(),\n result: doc,\n paths,\n}" +assertion_line: 1237 +expression: "TestResult\n{\n query: QUERY2, extracted_policies: &extracted_policies,\n successful_policies:\n [\"itf\".to_string(), \"a\".to_string()].into_iter().collect(), result: doc,\n paths\n}" --- query: @@ -16,11 +17,16 @@ query: } } -extracted_policies: {"a", "b", "itf"} -successful policies: [] +extracted_policies: {"a", "b"} +successful policies: ["a"] filtered: { test + itf { + ... on A { + id + } + } } -paths: ["/itf"] +paths: ["/itf/... on B"]
apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-4.snap+0 −26 removed@@ -1,26 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/policy.rs -expression: "TestResult {\n query: QUERY2,\n extracted_policies: &extracted_policies,\n successful_policies: [\"itf\".to_string()].into_iter().collect(),\n result: doc,\n paths,\n}" ---- -query: - - query { - test - itf { - ... on A { - id - } - ... on B { - id - } - } - } - -extracted_policies: {"a", "b", "itf"} -successful policies: ["itf"] -filtered: -{ - test -} - -paths: ["/itf/... on A", "/itf/... on B"]
apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-5.snap+0 −31 removed@@ -1,31 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/policy.rs -expression: "TestResult {\n query: QUERY2,\n extracted_policies: &extracted_policies,\n successful_policies: [\"itf\".to_string(),\n \"a\".to_string()].into_iter().collect(),\n result: doc,\n paths,\n}" ---- -query: - - query { - test - itf { - ... on A { - id - } - ... on B { - id - } - } - } - -extracted_policies: {"a", "b", "itf"} -successful policies: ["itf", "a"] -filtered: -{ - test - itf { - ... on A { - id - } - } -} - -paths: ["/itf/... on B"]
apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-7.snap+0 −8 removed@@ -1,8 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/policy.rs -expression: doc ---- -{ - test -} -
apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type-9.snap+0 −13 removed@@ -1,13 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/policy.rs -expression: doc ---- -{ - test - itf { - ... on A { - id - } - } -} -
apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__policy__tests__interface_type.snap+1 −1 modified@@ -11,7 +11,7 @@ query: } } -extracted_policies: {"itf"} +extracted_policies: {} successful policies: [] filtered: {
apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__implementations_with_different_field_scopes-2.snap+24 −0 added@@ -0,0 +1,24 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: "TestResult\n{\n query: QUERY, extracted_scopes: &extracted_scopes, scopes:\n vec![\"read\".to_string()], result: doc, paths\n}" +--- +query: + + query TestIssue { + test + itf { + id + } + } + +extracted_scopes: {} +request scopes: ["read"] +filtered: +query TestIssue { + test + itf { + id + } +} + +paths: []
apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__implementations_with_different_field_scopes.snap+21 −0 added@@ -0,0 +1,21 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: "TestResult\n{\n query: QUERY, extracted_scopes: &extracted_scopes, scopes: vec![], result:\n doc, paths\n}" +--- +query: + + query TestIssue { + test + itf { + id + } + } + +extracted_scopes: {} +request scopes: [] +filtered: +query TestIssue { + test +} + +paths: ["/itf/id"]
apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__implementations_with_different_type_scopes-2.snap+24 −0 added@@ -0,0 +1,24 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: "TestResult\n{\n query: QUERY, extracted_scopes: &extracted_scopes, scopes:\n vec![\"read\".to_string()], result: doc, paths\n}" +--- +query: + + query TestIssue { + test + itf { + id + } + } + +extracted_scopes: {} +request scopes: ["read"] +filtered: +query TestIssue { + test + itf { + id + } +} + +paths: []
apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__implementations_with_different_type_scopes.snap+21 −0 added@@ -0,0 +1,21 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +expression: "TestResult\n{\n query: QUERY, extracted_scopes: &extracted_scopes, scopes: Vec::new(),\n result: doc, paths\n}" +--- +query: + + query TestIssue { + test + itf { + id + } + } + +extracted_scopes: {} +request scopes: [] +filtered: +query TestIssue { + test +} + +paths: ["/itf"]
apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__implementations_with_same_scopes.snap+26 −0 added@@ -0,0 +1,26 @@ +--- +source: apollo-router/src/plugins/authorization/scopes.rs +assertion_line: 1667 +expression: "TestResult\n{\n query: QUERY, extracted_scopes: &extracted_scopes, scopes: Vec::new(),\n result: doc, paths\n}" +--- +query: + + query { + test + itf { + id + other + } + } + +extracted_scopes: {} +request scopes: [] +filtered: +{ + test + itf { + other + } +} + +paths: ["/itf/id"]
apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_fragment.snap+0 −3 modified@@ -26,9 +26,6 @@ filtered: topProducts { type } - itf { - id - } } paths: ["/itf"]
apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_inline_fragment.snap+1 −4 modified@@ -24,9 +24,6 @@ filtered: topProducts { type } - itf { - id - } } -paths: ["/itf/... on User"] +paths: ["/itf"]
apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-2.snap+10 −5 modified@@ -1,21 +1,26 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: "TestResult {\n query: QUERY,\n extracted_scopes: &extracted_scopes,\n scopes: [\"itf\".to_string()].into_iter().collect(),\n result: doc,\n paths,\n}" +expression: "TestResult\n{\n query: QUERY2, extracted_scopes: &extracted_scopes, scopes: Vec::new(),\n result: doc, paths\n}" --- query: query { test itf { - id + ... on A { + id + } + ... on B { + id + } } } -extracted_scopes: {"itf"} -request scopes: ["itf"] +extracted_scopes: {"a", "b", "c", "d"} +request scopes: [] filtered: { test } -paths: ["/itf"] +paths: ["/itf/... on A", "/itf/... on B"]
apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-3.snap+10 −4 modified@@ -1,6 +1,7 @@ --- source: apollo-router/src/plugins/authorization/scopes.rs -expression: "TestResult {\n query: QUERY2,\n extracted_scopes: &extracted_scopes,\n scopes: Vec::new(),\n result: doc,\n paths,\n}" +assertion_line: 1224 +expression: "TestResult\n{\n query: QUERY2, extracted_scopes: &extracted_scopes, scopes:\n [\"itf\".to_string(), \"a\".to_string(),\n \"b\".to_string()].into_iter().collect(), result: doc, paths\n}" --- query: @@ -16,11 +17,16 @@ query: } } -extracted_scopes: {"a", "b", "c", "d", "itf"} -request scopes: [] +extracted_scopes: {"a", "b", "c", "d"} +request scopes: ["a", "b"] filtered: { test + itf { + ... on A { + id + } + } } -paths: ["/itf"] +paths: ["/itf/... on B"]
apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-4.snap+0 −26 removed@@ -1,26 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/scopes.rs -expression: "TestResult {\n query: QUERY2,\n extracted_scopes: &extracted_scopes,\n scopes: [\"itf\".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"] -filtered: -{ - test -} - -paths: ["/itf/... on A", "/itf/... on B"]
apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type-5.snap+0 −31 removed@@ -1,31 +0,0 @@ ---- -source: apollo-router/src/plugins/authorization/scopes.rs -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: -{ - test - itf { - ... on A { - id - } - } -} - -paths: ["/itf/... on B"]
apollo-router/src/plugins/authorization/snapshots/apollo_router__plugins__authorization__scopes__tests__interface_type.snap+1 −1 modified@@ -11,7 +11,7 @@ query: } } -extracted_scopes: {"itf"} +extracted_scopes: {} request scopes: [] filtered: {
apollo-router/tests/fixtures/directives/policy/policy_schema_with_interfaces.graphql+3 −9 modified@@ -44,10 +44,11 @@ type Query { private: Private @join__field(graph: SUBGRAPH_A) @policy(policies: [["admin"]]) public: Public @join__field(graph: SUBGRAPH_A) - opensecret: OpenSecret @join__field(graph: SUBGRAPH_A) secure: Secure @join__field(graph: SUBGRAPH_A) } +# NOTE: despite its name, this wouldn't have the `admin` policy in the subgraph A +# policy would be inherited from the Private type interface Secure @join__type(graph: SUBGRAPH_A) { @@ -62,17 +63,10 @@ type Private implements Secure id: ID @policy(policies: [["admin"]]) @join__field(graph: SUBGRAPH_A) } -# NOTE: despite its name, this _doesn't_ have the `admin` policy -type OpenSecret implements Secure +type Public implements Secure @join__type(graph: SUBGRAPH_A) @join__implements(graph: SUBGRAPH_A, interface: "Secure") { id: ID @join__field(graph: SUBGRAPH_A) } -type Public - @join__type(graph: SUBGRAPH_A) -{ - id: ID @join__field(graph: SUBGRAPH_A) -} -
apollo-router/tests/integration/directives/policy.rs+23 −154 modified@@ -245,13 +245,8 @@ async fn policy_directive_should_not_pass_if_coproc_disallowed() -> Result<(), B Ok(()) } -// FIXME: this expresses the wrong behavior despite being the current behavior; in the future, -// after a fix in composition, no data should be returned but errors for an unauthorized field -// TODO: fix during FED-790; make the return type `Result<(), BoxError>` and return `Ok(())` #[tokio::test(flavor = "multi_thread")] -#[should_panic(expected = "called `Option::unwrap()` on a `None` value")] -async fn policy_directive_interfaces_with_different_implementors_without_policy_should_return_data() -{ +async fn implementations_without_policy_should_return_data() { // GIVEN // * a schema with @policy // * an interface using the @policy with implementors that have different policies @@ -260,7 +255,6 @@ async fn policy_directive_interfaces_with_different_implementors_without_policy_ // * a mock subgraph serving both public and private data // * a context object with the admin policy set to false // * the supergraph service layer - // * a request for a policy-gated field let mock_coprocessor = MockServer::start().await; let coprocessor_address = mock_coprocessor.uri(); @@ -298,8 +292,8 @@ async fn policy_directive_interfaces_with_different_implementors_without_policy_ serde_json::json!({"data": {"public": {"id": "456"}}}), ) .with_json( - serde_json::json!({"query": "{opensecret{id}}"}), - serde_json::json!({"data": {"opensecret": {"id": "789"}}}), + serde_json::json!({"query": "{secure{id}}"}), + serde_json::json!({"data": {"secure": {"id": "789"}}}), ) .build(), ); @@ -327,24 +321,23 @@ async fn policy_directive_interfaces_with_different_implementors_without_policy_ .await .unwrap(); + // WHEN + // * we make a request to an implementation without the policy directive let context = Context::new(); context // NOTE: there is no `admin` policy in the context .insert("apollo::authorization::required_policies", json! { [] }) .unwrap(); - // WHEN - // * we make a request to an implementor without the policy directive let request = supergraph::Request::fake_builder() - .query(r#"{ opensecret { id } }"#) + .query(r#"{ public { id } }"#) .context(context) .method(Method::POST) .build() .unwrap(); // THEN - // * we don't get any data, but we do get errors - + // * we get the data let response = supergraph_harness .oneshot(request) .await @@ -353,29 +346,30 @@ async fn policy_directive_interfaces_with_different_implementors_without_policy_ .await .unwrap(); - let data = response.data.unwrap(); - let error = response.errors.first().unwrap(); + let error = response.errors.first(); + assert!(error.is_none()); + let binding = response.data.unwrap(); + let response = binding + .get("public") + .unwrap() + .get("id") + .unwrap() + .as_str() + .unwrap(); - assert!(data.as_object().unwrap().is_empty()); - assert_eq!( - error.extension_code().unwrap(), - "UNAUTHORIZED_FIELD_OR_TYPE".to_string() - ); + assert_eq!(response, "456"); } #[tokio::test(flavor = "multi_thread")] -async fn policy_directive_interfaces_with_different_implementors_disallowed() -> Result<(), BoxError> -{ +async fn interface_with_different_implementation_policies_should_require_auth() { // GIVEN // * a schema with @policy // * an interface using the @policy with implementors that have different policies // * see the fixture for notes - // * requesting the interface directly, not one of its implementors // * a mock coprocessor that marks the admin policy as false (unused, see note above) // * a mock subgraph serving both public and private data // * a context object with the admin policy set to false // * the supergraph service layer - // * a request for a policy-gated field let mock_coprocessor = MockServer::start().await; let coprocessor_address = mock_coprocessor.uri(); @@ -412,13 +406,9 @@ async fn policy_directive_interfaces_with_different_implementors_disallowed() -> serde_json::json!({"query": "{public{id}}"}), serde_json::json!({"data": {"public": {"id": "456"}}}), ) - .with_json( - serde_json::json!({"query": "{opensecret{id}}"}), - serde_json::json!({"data": {"opensecret": {"id": "789"}}}), - ) .with_json( serde_json::json!({"query": "{secure{id}}"}), - serde_json::json!({"data": {"secure": {"id": "000"}}}), + serde_json::json!({"data": {"secure": {"id": "789"}}}), ) .build(), ); @@ -446,14 +436,14 @@ async fn policy_directive_interfaces_with_different_implementors_disallowed() -> .await .unwrap(); + // WHEN + // * we make a request to an interface with the policy directive let context = Context::new(); context // NOTE: there is no `admin` policy in the context .insert("apollo::authorization::required_policies", json! { [] }) .unwrap(); - // WHEN - // * we make a request with the interface itself off of Query let request = supergraph::Request::fake_builder() .query(r#"{ secure { id } }"#) .context(context) @@ -462,134 +452,15 @@ async fn policy_directive_interfaces_with_different_implementors_disallowed() -> .unwrap(); // THEN - // * we don't get data - + // * we don't get the data and get UNAUTHORIZED_FIELD_OR_TYPE error let response = supergraph_harness .oneshot(request) .await .unwrap() .next_response() .await - .unwrap() - .data - .unwrap(); - - let response = response.as_object().unwrap(); - - assert!(response.is_empty()); - Ok(()) -} - -#[tokio::test(flavor = "multi_thread")] -async fn policy_directive_interfaces_with_different_implementors_open_question() --> Result<(), BoxError> { - // GIVEN - // * a schema with @policy - // * an interface using the @policy with implementors that have different policies - // * see the fixture for notes - // * requesting the interface directly, not one of its implementors - // * a mock coprocessor that marks the admin policy as true (unused, see note above) - // * a mock subgraph serving both public and private data - // * a context object with the admin policy set to true - // * the supergraph service layer - // * a request for a policy-gated field - - let mock_coprocessor = MockServer::start().await; - let coprocessor_address = mock_coprocessor.uri(); - - Mock::given(method("POST")) - .and(path("/")) - .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ - "version": 1, - "stage": "SupergraphRequest", - "control": "continue", - "context": { - "entries": { - "apollo::authorization::required_policies": { - // NOTE: see the note above, but this shouldn't govern how the test - // behaves; it's the context object that does the dirt for the TestHarness. - // Change this value if you use it with the IntegrationTest builder - "admin": true - } - } - } - }))) - .mount(&mock_coprocessor) - .await; - - let mut subgraphs = MockedSubgraphs::default(); - subgraphs.insert( - "subgraph_a", - MockSubgraph::builder() - .with_json( - serde_json::json!({"query": "{private{id}}"}), - serde_json::json!({"data": {"private": {"id": "123"}}}), - ) - .with_json( - serde_json::json!({"query": "{public{id}}"}), - serde_json::json!({"data": {"public": {"id": "456"}}}), - ) - .with_json( - serde_json::json!({"query": "{opensecret{id}}"}), - serde_json::json!({"data": {"opensecret": {"id": "789"}}}), - ) - .with_json( - serde_json::json!({"query": "{secure{id}}"}), - serde_json::json!({"data": {"secure": {"id": "000"}}}), - ) - .build(), - ); - - let supergraph_harness = TestHarness::builder() - .configuration_json(serde_json::json!({ - "coprocessor": { - "url": coprocessor_address, - "supergraph": { - "request": { - "context": "all" - } - } - }, - "include_subgraph_errors": { - "all": true - } - })) - .unwrap() - .schema(include_str!( - "../../fixtures/directives/policy/policy_schema_with_interfaces.graphql" - )) - .extra_plugin(subgraphs) - .build_supergraph() - .await .unwrap(); - let context = Context::new(); - context - .insert( - "apollo::authorization::required_policies", - json! {[ "admin" ]}, - ) - .unwrap(); - - // WHEN - // * we make a request with the interface itself off of Query - let request = supergraph::Request::fake_builder() - .query(r#"{ secure { id } }"#) - .context(context) - .method(Method::POST) - .build() - .unwrap(); - - // THEN - // * we don't get any data, but we do get errors - - let response = supergraph_harness - .oneshot(request) - .await - .unwrap() - .next_response() - .await - .unwrap(); let data = response.data.unwrap(); let error = response.errors.first().unwrap(); @@ -598,6 +469,4 @@ async fn policy_directive_interfaces_with_different_implementors_open_question() error.extension_code().unwrap(), "UNAUTHORIZED_FIELD_OR_TYPE".to_string() ); - - Ok(()) }
.changesets/fix_auction_earn_venom_naval.md+12 −0 added@@ -0,0 +1,12 @@ +### Fix authorization plugin handling of polymorphic types + +Updates the authorization plugin to correctly handle authorization requirements when processing polymorphic types. + +When querying interface fields, the authorization plugin was verifying only whether all implementations shared the same +authorization requirements. In cases where interface did not specify any authorization requirements, this could result in +unauthorized access to protected data. + +The authorization plugin was updated to correctly verify that all polymorphic authorization requirements are satisfied by +the current context. + +By [@dariuszkuc](https://github.com/dariuszkuc) in https://github.com/apollographql/router/pull/PULL_NUMBER
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-x33c-7c2v-mrj9ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-64173ghsaADVISORY
- github.com/apollographql/router/commit/75ca43ecb9d38423b63d09896702f9da425cc754ghsaWEB
- github.com/apollographql/router/releases/tag/v2.8.1nvdWEB
- github.com/apollographql/router/security/advisories/GHSA-x33c-7c2v-mrj9nvdWEB
- www.apollographql.com/docs/graphos/routing/security/authorizationnvdWEB
News mentions
0No linked articles in our index yet.