1use std::collections::HashMap;
4use std::collections::HashSet;
5use std::ops::ControlFlow;
6
7use apollo_compiler::ExecutableDocument;
8use apollo_compiler::ast;
9use http::StatusCode;
10use schemars::JsonSchema;
11use serde::Deserialize;
12use serde::Serialize;
13use serde_json_bytes::Value;
14use tower::BoxError;
15use tower::ServiceBuilder;
16use tower::ServiceExt;
17
18use self::authenticated::AUTHENTICATED_SPEC_BASE_URL;
19use self::authenticated::AUTHENTICATED_SPEC_VERSION_RANGE;
20use self::authenticated::AuthenticatedCheckVisitor;
21use self::authenticated::AuthenticatedVisitor;
22use self::policy::POLICY_SPEC_BASE_URL;
23use self::policy::POLICY_SPEC_VERSION_RANGE;
24use self::policy::PolicyExtractionVisitor;
25use self::policy::PolicyFilteringVisitor;
26use self::scopes::REQUIRES_SCOPES_SPEC_BASE_URL;
27use self::scopes::REQUIRES_SCOPES_SPEC_VERSION_RANGE;
28use self::scopes::ScopeExtractionVisitor;
29use self::scopes::ScopeFilteringVisitor;
30use crate::Configuration;
31use crate::Context;
32use crate::error::QueryPlannerError;
33use crate::error::ServiceBuildError;
34use crate::graphql;
35use crate::json_ext::Path;
36use crate::layers::ServiceBuilderExt;
37use crate::plugin::Plugin;
38use crate::plugin::PluginInit;
39use crate::plugins::authentication::APOLLO_AUTHENTICATION_JWT_CLAIMS;
40use crate::query_planner::FilteredQuery;
41use crate::query_planner::QueryKey;
42use crate::register_plugin;
43use crate::services::execution;
44use crate::services::layers::query_analysis::ParsedDocumentInner;
45use crate::services::supergraph;
46use crate::spec::Schema;
47use crate::spec::SpecError;
48use crate::spec::query::transform;
49use crate::spec::query::traverse;
50
51pub(crate) mod authenticated;
52pub(crate) mod policy;
53pub(crate) mod scopes;
54
55pub(crate) const AUTHENTICATION_REQUIRED_KEY: &str =
56 "apollo::authorization::authentication_required";
57pub(crate) const REQUIRED_SCOPES_KEY: &str = "apollo::authorization::required_scopes";
58pub(crate) const REQUIRED_POLICIES_KEY: &str = "apollo::authorization::required_policies";
59
60#[derive(Clone, Debug, Default, Hash, PartialEq, Eq, Serialize, Deserialize)]
61pub(crate) struct CacheKeyMetadata {
62 pub(crate) is_authenticated: bool,
63 pub(crate) scopes: Vec<String>,
64 pub(crate) policies: Vec<String>,
65}
66
67#[derive(Clone, Debug, serde_derive_default::Default, Deserialize, JsonSchema)]
69#[allow(dead_code)]
70#[schemars(rename = "AuthorizationConfig")]
71pub(crate) struct Conf {
72 #[serde(default)]
74 require_authentication: bool,
75 #[serde(default)]
77 directives: Directives,
78}
79
80#[derive(Clone, Debug, serde_derive_default::Default, Deserialize, JsonSchema)]
81#[allow(dead_code)]
82#[schemars(rename = "AuthorizationDirectivesConfig")]
83pub(crate) struct Directives {
84 #[serde(default = "default_enable_directives")]
86 enabled: bool,
87 #[serde(default)]
89 dry_run: bool,
90 #[serde(default)]
92 reject_unauthorized: bool,
93 #[serde(default)]
95 errors: ErrorConfig,
96}
97
98#[derive(
99 Clone, Debug, serde_derive_default::Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema,
100)]
101#[allow(dead_code)]
102#[schemars(rename = "AuthorizationErrorConfig")]
103pub(crate) struct ErrorConfig {
104 #[serde(default = "enable_log_errors")]
106 pub(crate) log: bool,
107 #[serde(default)]
109 pub(crate) response: ErrorLocation,
110}
111
112fn enable_log_errors() -> bool {
113 true
114}
115
116#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
117#[serde(rename_all = "snake_case")]
118pub(crate) enum ErrorLocation {
119 #[default]
121 Errors,
122 Extensions,
124 Disabled,
126}
127
128#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
129pub(crate) struct UnauthorizedPaths {
130 pub(crate) paths: Vec<Path>,
131 pub(crate) errors: ErrorConfig,
132}
133
134fn default_enable_directives() -> bool {
135 true
136}
137
138pub(crate) struct AuthorizationPlugin {
139 require_authentication: bool,
140}
141
142impl AuthorizationPlugin {
143 pub(crate) fn enable_directives(
144 configuration: &Configuration,
145 schema: &Schema,
146 ) -> Result<bool, ServiceBuildError> {
147 let has_config = configuration
148 .apollo_plugins
149 .plugins
150 .iter()
151 .find(|(s, _)| s.as_str() == "authorization")
152 .and_then(|(_, v)| v.get("directives").and_then(|v| v.as_object()))
153 .and_then(|v| v.get("enabled").and_then(|v| v.as_bool()));
154
155 let has_authorization_directives = schema.has_spec(
156 AUTHENTICATED_SPEC_BASE_URL,
157 AUTHENTICATED_SPEC_VERSION_RANGE,
158 ) || schema.has_spec(
159 REQUIRES_SCOPES_SPEC_BASE_URL,
160 REQUIRES_SCOPES_SPEC_VERSION_RANGE,
161 ) || schema
162 .has_spec(POLICY_SPEC_BASE_URL, POLICY_SPEC_VERSION_RANGE);
163
164 Ok(has_config.unwrap_or(true) && has_authorization_directives)
165 }
166
167 pub(crate) fn log_errors(configuration: &Configuration) -> ErrorConfig {
168 configuration
169 .apollo_plugins
170 .plugins
171 .iter()
172 .find(|(s, _)| s.as_str() == "authorization")
173 .and_then(|(_, v)| v.get("directives").and_then(|v| v.as_object()))
174 .and_then(|v| {
175 v.get("errors")
176 .and_then(|v| serde_json::from_value(v.clone()).ok())
177 })
178 .unwrap_or_default()
179 }
180
181 pub(crate) fn query_analysis(
182 doc: &ParsedDocumentInner,
183 operation_name: Option<&str>,
184 schema: &Schema,
185 context: &Context,
186 ) {
187 let CacheKeyMetadata {
188 is_authenticated,
189 scopes,
190 policies,
191 } = Self::generate_cache_metadata(
192 &doc.executable,
193 operation_name,
194 schema.supergraph_schema(),
195 false,
196 );
197 if is_authenticated {
198 context.insert(AUTHENTICATION_REQUIRED_KEY, true).unwrap();
199 }
200
201 if !scopes.is_empty() {
202 context.insert(REQUIRED_SCOPES_KEY, scopes).unwrap();
203 }
204
205 if !policies.is_empty() {
206 let policies: HashMap<String, Option<bool>> =
207 policies.into_iter().map(|policy| (policy, None)).collect();
208 context.insert(REQUIRED_POLICIES_KEY, policies).unwrap();
209 }
210 }
211
212 pub(crate) fn generate_cache_metadata(
213 document: &ExecutableDocument,
214 operation_name: Option<&str>,
215 schema: &apollo_compiler::Schema,
216 entity_query: bool,
217 ) -> CacheKeyMetadata {
218 let mut is_authenticated = false;
219 if let Some(mut visitor) = AuthenticatedCheckVisitor::new(schema, document, entity_query) {
220 if traverse::document(&mut visitor, document, operation_name).is_ok() && visitor.found {
224 is_authenticated = true;
225 }
226 }
227
228 let mut scopes = Vec::new();
229 if let Some(mut visitor) = ScopeExtractionVisitor::new(schema, document, entity_query) {
230 if traverse::document(&mut visitor, document, operation_name).is_ok() {
234 scopes = visitor.extracted_scopes.into_iter().collect();
235 }
236 }
237
238 let mut policies: Vec<String> = Vec::new();
239 if let Some(mut visitor) = PolicyExtractionVisitor::new(schema, document, entity_query) {
240 if traverse::document(&mut visitor, document, operation_name).is_ok() {
244 policies = visitor.extracted_policies.into_iter().collect();
245 }
246 }
247
248 CacheKeyMetadata {
249 is_authenticated,
250 scopes,
251 policies,
252 }
253 }
254
255 pub(crate) fn update_cache_key(context: &Context) {
256 let is_authenticated = context.contains_key(APOLLO_AUTHENTICATION_JWT_CLAIMS);
257
258 let request_scopes = context
259 .get_json_value(APOLLO_AUTHENTICATION_JWT_CLAIMS)
260 .and_then(|value| {
261 value.as_object().and_then(|object| {
262 object.get("scope").and_then(|v| {
263 v.as_str()
264 .map(|s| s.split(' ').map(|s| s.to_string()).collect::<HashSet<_>>())
265 })
266 })
267 });
268 let query_scopes = context.get_json_value(REQUIRED_SCOPES_KEY).and_then(|v| {
269 v.as_array().map(|v| {
270 v.iter()
271 .filter_map(|s| s.as_str().map(|s| s.to_string()))
272 .collect::<HashSet<_>>()
273 })
274 });
275
276 let mut scopes = match (request_scopes, query_scopes) {
277 (None, _) => vec![],
278 (_, None) => vec![],
279 (Some(req), Some(query)) => req.intersection(&query).cloned().collect(),
280 };
281 scopes.sort();
282
283 let mut policies = context
284 .get_json_value(REQUIRED_POLICIES_KEY)
285 .and_then(|v| {
286 v.as_object().map(|v| {
287 v.iter()
288 .filter_map(|(policy, result)| match result {
289 Value::Bool(true) => Some(policy.as_str().to_string()),
290 _ => None,
291 })
292 .collect::<Vec<String>>()
293 })
294 })
295 .unwrap_or_default();
296 policies.sort();
297
298 context.extensions().with_lock(|lock| {
299 lock.insert(CacheKeyMetadata {
300 is_authenticated,
301 scopes,
302 policies,
303 })
304 });
305 }
306
307 pub(crate) fn intersect_cache_keys_subgraph(
308 left: &CacheKeyMetadata,
309 right: &CacheKeyMetadata,
310 ) -> CacheKeyMetadata {
311 CacheKeyMetadata {
312 is_authenticated: left.is_authenticated && right.is_authenticated,
313 scopes: left
314 .scopes
315 .iter()
316 .collect::<HashSet<_>>()
317 .intersection(&right.scopes.iter().collect::<HashSet<_>>())
318 .map(|s| s.to_string())
319 .collect(),
320 policies: left
321 .policies
322 .iter()
323 .collect::<HashSet<_>>()
324 .intersection(&right.policies.iter().collect::<HashSet<_>>())
325 .map(|s| s.to_string())
326 .collect(),
327 }
328 }
329
330 pub(crate) fn filter_query(
331 configuration: &Configuration,
332 key: &QueryKey,
333 schema: &Schema,
334 ) -> Result<Option<FilteredQuery>, QueryPlannerError> {
335 let (reject_unauthorized, dry_run) = configuration
336 .apollo_plugins
337 .plugins
338 .iter()
339 .find(|(s, _)| s.as_str() == "authorization")
340 .and_then(|(_, v)| v.get("directives").and_then(|v| v.as_object()))
341 .map(|config| {
342 (
343 config
344 .get("reject_unauthorized")
345 .and_then(|v| v.as_bool())
346 .unwrap_or(false),
347 config
348 .get("dry_run")
349 .and_then(|v| v.as_bool())
350 .unwrap_or(false),
351 )
352 })
353 .unwrap_or((false, false));
354
355 let doc = ast::Document::parse(&key.filtered_query, "filtered_query")
361 .unwrap_or_else(|invalid| invalid.partial);
363
364 let is_authenticated = key.metadata.is_authenticated;
365 let scopes = &key.metadata.scopes;
366 let policies = &key.metadata.policies;
367
368 let mut is_filtered = false;
369 let mut unauthorized_paths: Vec<Path> = vec![];
370
371 let filter_res = Self::authenticated_filter_query(schema, dry_run, &doc, is_authenticated)?;
372
373 let doc = match filter_res {
374 None => doc,
375 Some((filtered_doc, paths)) => {
376 unauthorized_paths.extend(paths);
377
378 if filtered_doc.definitions.is_empty() {
380 return Err(QueryPlannerError::Unauthorized(unauthorized_paths));
381 }
382
383 is_filtered = true;
384
385 filtered_doc
386 }
387 };
388
389 let filter_res = Self::scopes_filter_query(schema, dry_run, &doc, scopes)?;
390
391 let doc = match filter_res {
392 None => doc,
393 Some((filtered_doc, paths)) => {
394 unauthorized_paths.extend(paths);
395
396 if filtered_doc.definitions.is_empty() {
398 return Err(QueryPlannerError::Unauthorized(unauthorized_paths));
399 }
400
401 is_filtered = true;
402
403 filtered_doc
404 }
405 };
406
407 let filter_res = Self::policies_filter_query(schema, dry_run, &doc, policies)?;
408
409 let doc = match filter_res {
410 None => doc,
411 Some((filtered_doc, paths)) => {
412 unauthorized_paths.extend(paths);
413
414 if filtered_doc.definitions.is_empty() {
416 return Err(QueryPlannerError::Unauthorized(unauthorized_paths));
417 }
418
419 is_filtered = true;
420
421 filtered_doc
422 }
423 };
424
425 if reject_unauthorized && !unauthorized_paths.is_empty() {
426 return Err(QueryPlannerError::Unauthorized(unauthorized_paths));
427 }
428
429 if is_filtered {
430 Ok(Some((unauthorized_paths, doc)))
431 } else {
432 Ok(None)
433 }
434 }
435
436 fn authenticated_filter_query(
437 schema: &Schema,
438 dry_run: bool,
439 doc: &ast::Document,
440 is_authenticated: bool,
441 ) -> Result<Option<(ast::Document, Vec<Path>)>, QueryPlannerError> {
442 if let Some(mut visitor) = AuthenticatedVisitor::new(
443 schema.supergraph_schema(),
444 &schema.implementers_map,
445 dry_run,
446 ) {
447 let modified_query = transform::document(&mut visitor, doc)
448 .map_err(|e| SpecError::TransformError(e.to_string()))?;
449
450 if visitor.query_requires_authentication {
451 if is_authenticated {
452 tracing::debug!(
453 "the query contains @authenticated, the request is authenticated, keeping the query"
454 );
455 Ok(None)
456 } else {
457 tracing::debug!(
458 "the query contains @authenticated, modified query:\n{modified_query}\nunauthorized paths: {:?}",
459 visitor
460 .unauthorized_paths
461 .iter()
462 .map(|path| path.to_string())
463 .collect::<Vec<_>>()
464 );
465
466 Ok(Some((modified_query, visitor.unauthorized_paths)))
467 }
468 } else {
469 tracing::debug!("the query does not contain @authenticated");
470 Ok(None)
471 }
472 } else {
473 tracing::debug!("the schema does not contain @authenticated");
474 Ok(None)
475 }
476 }
477
478 fn scopes_filter_query(
479 schema: &Schema,
480 dry_run: bool,
481 doc: &ast::Document,
482 scopes: &[String],
483 ) -> Result<Option<(ast::Document, Vec<Path>)>, QueryPlannerError> {
484 if let Some(mut visitor) = ScopeFilteringVisitor::new(
485 schema.supergraph_schema(),
486 &schema.implementers_map,
487 scopes.iter().cloned().collect(),
488 dry_run,
489 ) {
490 let modified_query = transform::document(&mut visitor, doc)
491 .map_err(|e| SpecError::TransformError(e.to_string()))?;
492 if visitor.query_requires_scopes {
493 tracing::debug!(
494 "the query required scopes, the requests present scopes: {scopes:?}, modified query:\n{modified_query}\nunauthorized paths: {:?}",
495 visitor
496 .unauthorized_paths
497 .iter()
498 .map(|path| path.to_string())
499 .collect::<Vec<_>>()
500 );
501 Ok(Some((modified_query, visitor.unauthorized_paths)))
502 } else {
503 tracing::debug!("the query does not require scopes");
504 Ok(None)
505 }
506 } else {
507 tracing::debug!("the schema does not contain @requiresScopes");
508 Ok(None)
509 }
510 }
511
512 fn policies_filter_query(
513 schema: &Schema,
514 dry_run: bool,
515
516 doc: &ast::Document,
517 policies: &[String],
518 ) -> Result<Option<(ast::Document, Vec<Path>)>, QueryPlannerError> {
519 if let Some(mut visitor) = PolicyFilteringVisitor::new(
520 schema.supergraph_schema(),
521 &schema.implementers_map,
522 policies.iter().cloned().collect(),
523 dry_run,
524 ) {
525 let modified_query = transform::document(&mut visitor, doc)
526 .map_err(|e| SpecError::TransformError(e.to_string()))?;
527
528 if visitor.query_requires_policies {
529 tracing::debug!(
530 "the query required policies, the requests present policies: {policies:?}, modified query:\n{modified_query}\nunauthorized paths: {:?}",
531 visitor
532 .unauthorized_paths
533 .iter()
534 .map(|path| path.to_string())
535 .collect::<Vec<_>>()
536 );
537 Ok(Some((modified_query, visitor.unauthorized_paths)))
538 } else {
539 tracing::debug!("the query does not require policies");
540 Ok(None)
541 }
542 } else {
543 tracing::debug!("the schema does not contain @policy");
544 Ok(None)
545 }
546 }
547}
548
549#[async_trait::async_trait]
550impl Plugin for AuthorizationPlugin {
551 type Config = Conf;
552
553 async fn new(init: PluginInit<Self::Config>) -> Result<Self, BoxError> {
554 Ok(AuthorizationPlugin {
555 require_authentication: init.config.require_authentication,
556 })
557 }
558
559 fn supergraph_service(&self, service: supergraph::BoxService) -> supergraph::BoxService {
560 if self.require_authentication {
561 ServiceBuilder::new()
562 .checkpoint(move |request: supergraph::Request| {
563 if request
566 .context
567 .contains_key(APOLLO_AUTHENTICATION_JWT_CLAIMS)
568 {
569 Ok(ControlFlow::Continue(request))
570 } else {
571 tracing::error!("rejecting unauthenticated request");
572 let response = supergraph::Response::error_builder()
573 .error(
574 graphql::Error::builder()
575 .message("unauthenticated".to_string())
576 .extension_code("AUTH_ERROR")
577 .build(),
578 )
579 .status_code(StatusCode::UNAUTHORIZED)
580 .context(request.context)
581 .build()?;
582 Ok(ControlFlow::Break(response))
583 }
584 })
585 .service(service)
586 .boxed()
587 } else {
588 service
589 }
590 }
591
592 fn execution_service(&self, service: execution::BoxService) -> execution::BoxService {
593 ServiceBuilder::new()
594 .map_request(|request: execution::Request| {
595 let filtered = !request.query_plan.query.unauthorized.paths.is_empty();
596 let needs_authenticated = request.context.contains_key(AUTHENTICATION_REQUIRED_KEY);
597 let needs_requires_scopes = request.context.contains_key(REQUIRED_SCOPES_KEY);
598
599 if needs_authenticated || needs_requires_scopes {
600 u64_counter!(
601 "apollo.router.operations.authorization",
602 "Number of subgraph requests requiring authorization",
603 1,
604 authorization.filtered = filtered,
605 authorization.needs_authenticated = needs_authenticated,
606 authorization.needs_requires_scopes = needs_requires_scopes
607 );
608 }
609
610 request
611 })
612 .service(service)
613 .boxed()
614 }
615}
616
617register_plugin!("apollo", "authorization", AuthorizationPlugin);
623
624#[cfg(test)]
625mod tests;