Skip to content

Commit

Permalink
add a metric tracking authorization usage (#3660)
Browse files Browse the repository at this point in the history
Co-authored-by: Bryn Cooke <[email protected]>
  • Loading branch information
Geal and BrynCooke authored Aug 24, 2023
1 parent dc75fb9 commit a6cf4b0
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 2 deletions.
9 changes: 9 additions & 0 deletions .changesets/maint_geal_authorization_analytics.md
Original file line number Diff line number Diff line change
@@ -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
114 changes: 114 additions & 0 deletions apollo-router/src/plugins/authorization/authenticated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
42 changes: 40 additions & 2 deletions apollo-router/src/plugins/authorization/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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";

Expand Down Expand Up @@ -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.
Expand All @@ -141,7 +153,9 @@ impl AuthorizationPlugin {
if traverse::document(&mut visitor, file_id).is_ok() {
let scopes: Vec<String> = 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
Expand All @@ -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();
}
}
}
}
Expand Down Expand Up @@ -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!
Expand Down

0 comments on commit a6cf4b0

Please sign in to comment.