VYPR
High severity7.5OSV Advisory· Published Nov 6, 2025· Updated Apr 15, 2026

CVE-2025-64173

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.

PackageAffected versionsPatched versions
apollo-routercrates.io
< 1.61.121.61.12
apollo-routercrates.io
>= 2.0.0-alpha.0, < 2.8.12.8.1

Affected products

1

Patches

1
75ca43ecb9d3

fix: update auth plugin handling of polymorphic types

https://github.com/apollographql/routerdariuszkucOct 21, 2025via ghsa
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

News mentions

0

No linked articles in our index yet.