From b9ed3288f6c8cf8d3e1c1c93f40a79bf8527da4e Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Thu, 24 Aug 2023 15:52:25 +0200 Subject: [PATCH] add a metric tracking authorization usage (#3660) Co-authored-by: Bryn Cooke --- .../maint_geal_authorization_analytics.md | 9 ++ .../plugins/authorization/authenticated.rs | 114 ++++++++++++++++++ .../src/plugins/authorization/mod.rs | 42 ++++++- 3 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 .changesets/maint_geal_authorization_analytics.md diff --git a/.changesets/maint_geal_authorization_analytics.md b/.changesets/maint_geal_authorization_analytics.md new file mode 100644 index 0000000000..97af5f6012 --- /dev/null +++ b/.changesets/maint_geal_authorization_analytics.md @@ -0,0 +1,9 @@ +### add a metric tracking authorization usage ([PR #3660](https://github.com/apollographql/router/pull/3660)) + +The new metrics, for use in Router Analytics, is a counter called `apollo.router.operations.authorization` +and contains the following boolean attributes: +- filtered: some fields were filtered from the query +- authenticated: the query uses fields or types tagged with the `@authenticated` directive +- requires_scopes: the query uses fields or types tagged with the `@requiresScopes` directive + +By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/3660 \ No newline at end of file diff --git a/apollo-router/src/plugins/authorization/authenticated.rs b/apollo-router/src/plugins/authorization/authenticated.rs index 67a3c28141..26728f991d 100644 --- a/apollo-router/src/plugins/authorization/authenticated.rs +++ b/apollo-router/src/plugins/authorization/authenticated.rs @@ -12,9 +12,123 @@ use crate::json_ext::Path; use crate::json_ext::PathElement; use crate::spec::query::transform; use crate::spec::query::transform::get_field_type; +use crate::spec::query::traverse; pub(crate) const AUTHENTICATED_DIRECTIVE_NAME: &str = "authenticated"; +pub(crate) struct AuthenticatedCheckVisitor<'a> { + compiler: &'a ApolloCompiler, + file_id: FileId, + pub(crate) found: bool, +} + +impl<'a> AuthenticatedCheckVisitor<'a> { + pub(crate) fn new(compiler: &'a ApolloCompiler, file_id: FileId) -> Self { + Self { + compiler, + file_id, + found: false, + } + } + + fn is_field_authenticated(&self, field: &FieldDefinition) -> bool { + field + .directive_by_name(AUTHENTICATED_DIRECTIVE_NAME) + .is_some() + || field + .ty() + .type_def(&self.compiler.db) + .map(|t| self.is_type_authenticated(&t)) + .unwrap_or(false) + } + + fn is_type_authenticated(&self, t: &TypeDefinition) -> bool { + t.directive_by_name(AUTHENTICATED_DIRECTIVE_NAME).is_some() + } +} + +impl<'a> traverse::Visitor for AuthenticatedCheckVisitor<'a> { + fn compiler(&self) -> &ApolloCompiler { + self.compiler + } + + fn operation(&mut self, node: &hir::OperationDefinition) -> Result<(), BoxError> { + traverse::operation(self, node) + } + + fn field(&mut self, parent_type: &str, node: &hir::Field) -> Result<(), BoxError> { + let field_name = node.name(); + + if self + .compiler + .db + .types_definitions_by_name() + .get(parent_type) + .and_then(|def| def.field(&self.compiler.db, field_name)) + .is_some_and(|field| self.is_field_authenticated(field)) + { + self.found = true; + return Ok(()); + } + traverse::field(self, parent_type, node) + } + + fn fragment_definition(&mut self, node: &hir::FragmentDefinition) -> Result<(), BoxError> { + if self + .compiler + .db + .types_definitions_by_name() + .get(node.type_condition()) + .is_some_and(|type_definition| self.is_type_authenticated(type_definition)) + { + self.found = true; + return Ok(()); + } + traverse::fragment_definition(self, node) + } + + fn fragment_spread(&mut self, node: &hir::FragmentSpread) -> Result<(), BoxError> { + let fragments = self.compiler.db.fragments(self.file_id); + let condition = fragments + .get(node.name()) + .ok_or("MissingFragmentDefinition")? + .type_condition(); + + if self + .compiler + .db + .types_definitions_by_name() + .get(condition) + .is_some_and(|type_definition| self.is_type_authenticated(type_definition)) + { + self.found = true; + return Ok(()); + } + traverse::fragment_spread(self, node) + } + + fn inline_fragment( + &mut self, + parent_type: &str, + node: &hir::InlineFragment, + ) -> Result<(), BoxError> { + if let Some(name) = node.type_condition() { + if self + .compiler + .db + .types_definitions_by_name() + .get(name) + .is_some_and(|type_definition| self.is_type_authenticated(type_definition)) + { + self.found = true; + return Ok(()); + } + } + + traverse::inline_fragment(self, parent_type, node) + } +} + pub(crate) struct AuthenticatedVisitor<'a> { compiler: &'a ApolloCompiler, file_id: FileId, diff --git a/apollo-router/src/plugins/authorization/mod.rs b/apollo-router/src/plugins/authorization/mod.rs index 2ab958665d..20bfe91db4 100644 --- a/apollo-router/src/plugins/authorization/mod.rs +++ b/apollo-router/src/plugins/authorization/mod.rs @@ -17,6 +17,7 @@ use tower::BoxError; use tower::ServiceBuilder; use tower::ServiceExt; +use self::authenticated::AuthenticatedCheckVisitor; use self::authenticated::AuthenticatedVisitor; use self::authenticated::AUTHENTICATED_DIRECTIVE_NAME; use self::policy::PolicyExtractionVisitor; @@ -37,6 +38,7 @@ use crate::plugins::authentication::APOLLO_AUTHENTICATION_JWT_CLAIMS; use crate::query_planner::FilteredQuery; use crate::query_planner::QueryKey; use crate::register_plugin; +use crate::services::execution; use crate::services::supergraph; use crate::spec::query::transform; use crate::spec::query::traverse; @@ -51,6 +53,7 @@ pub(crate) mod authenticated; pub(crate) mod policy; pub(crate) mod scopes; +const AUTHENTICATED_KEY: &str = "apollo_authorization::authenticated::required"; const REQUIRED_SCOPES_KEY: &str = "apollo_authorization::scopes::required"; const REQUIRED_POLICIES_KEY: &str = "apollo_authorization::policies::required"; @@ -133,6 +136,15 @@ impl AuthorizationPlugin { ) { let (compiler, file_id) = Query::make_compiler(query, schema, configuration); + let mut visitor = AuthenticatedCheckVisitor::new(&compiler, file_id); + + // if this fails, the query is invalid and will fail at the query planning phase. + // We do not return validation errors here for now because that would imply a huge + // refactoring of telemetry and tests + if traverse::document(&mut visitor, file_id).is_ok() && !visitor.found { + context.insert(AUTHENTICATED_KEY, true).unwrap(); + } + let mut visitor = ScopeExtractionVisitor::new(&compiler, file_id); // if this fails, the query is invalid and will fail at the query planning phase. @@ -141,7 +153,9 @@ impl AuthorizationPlugin { if traverse::document(&mut visitor, file_id).is_ok() { let scopes: Vec = visitor.extracted_scopes.into_iter().collect(); - context.insert(REQUIRED_SCOPES_KEY, scopes).unwrap(); + if !scopes.is_empty() { + context.insert(REQUIRED_SCOPES_KEY, scopes).unwrap(); + } } // TODO: @policy is out of scope for preview, this will be reactivated later @@ -158,7 +172,9 @@ impl AuthorizationPlugin { .map(|policy| (policy, None)) .collect(); - context.insert(REQUIRED_POLICIES_KEY, policies).unwrap(); + if !policies.is_empty() { + context.insert(REQUIRED_POLICIES_KEY, policies).unwrap(); + } } } } @@ -455,6 +471,28 @@ impl Plugin for AuthorizationPlugin { service } } + + fn execution_service(&self, service: execution::BoxService) -> execution::BoxService { + ServiceBuilder::new() + .map_request(|request: execution::Request| { + let filtered = !request.query_plan.query.unauthorized_paths.is_empty(); + let needs_authenticated = request.context.contains_key(AUTHENTICATED_KEY); + let needs_requires_scopes = request.context.contains_key(REQUIRED_SCOPES_KEY); + + if needs_authenticated || needs_requires_scopes { + tracing::info!( + monotonic_counter.apollo.router.operations.authorization = 1u64, + filtered = filtered, + authenticated = needs_authenticated, + requires_scopes = needs_requires_scopes, + ); + } + + request + }) + .service(service) + .boxed() + } } // This macro allows us to use it in our plugin registry!