diff --git a/.changesets/breaking_glasser_pq_metric_attribute_consistency.md b/.changesets/breaking_glasser_pq_metric_attribute_consistency.md new file mode 100644 index 0000000000..87c320e4ad --- /dev/null +++ b/.changesets/breaking_glasser_pq_metric_attribute_consistency.md @@ -0,0 +1,9 @@ +### More consistent attributes on `apollo.router.operations.persisted_queries` metric ([PR #6403](https://github.com/apollographql/router/pull/6403)) + +Version 1.28.1 added several *unstable* metrics, including `apollo.router.operations.persisted_queries`. + +When an operation is rejected, Router includes a `persisted_queries.safelist.rejected.unknown` attribute on the metric. Previously, this attribute had the value `true` if the operation is logged (via `log_unknown`), and `false` if the operation is not logged. (The attribute is not included at all if the operation is not rejected.) This appears to have been a mistake, as you can also tell whether it is logged via the `persisted_queries.logged` attribute. + +Router now only sets this attribute to true, and never to false. This may be a breaking change for your use of metrics; note that these metrics should be treated as unstable and may change in the future. + +By [@glasser](https://github.com/glasser) in https://github.com/apollographql/router/pull/6403 diff --git a/.changesets/feat_glasser_pq_safelist_override.md b/.changesets/feat_glasser_pq_safelist_override.md new file mode 100644 index 0000000000..316febbca9 --- /dev/null +++ b/.changesets/feat_glasser_pq_safelist_override.md @@ -0,0 +1,10 @@ +### Ability to skip Persisted Query List safelisting enforcement via plugin ([PR #6403](https://github.com/apollographql/router/pull/6403)) + +If safelisting is enabled, a `router_service` plugin can skip enforcement of the safelist (including the `require_id` check) by adding the key `apollo_persisted_queries::safelist::skip_enforcement` with value `true` to the request context. + +(This does not affect the logging of unknown operations by the `persisted_queries.log_unknown` option.) + +In cases where an operation would have been denied but is allowed due to the context key existing, the attribute +`persisted_queries.safelist.enforcement_skipped` is set on the `apollo.router.operations.persisted_queries` metric with value true. + +By [@glasser](https://github.com/glasser) in https://github.com/apollographql/router/pull/6403 \ No newline at end of file diff --git a/apollo-router/src/metrics/mod.rs b/apollo-router/src/metrics/mod.rs index 4b2aa0b888..1cb90bc2ae 100644 --- a/apollo-router/src/metrics/mod.rs +++ b/apollo-router/src/metrics/mod.rs @@ -525,9 +525,10 @@ pub(crate) fn meter_provider() -> AggregateMeterProvider { /// frobbles.color = "blue" /// ); /// // Count a thing with dynamic attributes: -/// let attributes = [ -/// opentelemetry::KeyValue::new("frobbles.color".to_string(), "blue".into()), -/// ]; +/// let attributes = vec![]; +/// if (frobbled) { +/// attributes.push(opentelemetry::KeyValue::new("frobbles.color".to_string(), "blue".into())); +/// } /// u64_counter!( /// "apollo.router.operations.frobbles", /// "The amount of frobbles we've operated on", @@ -939,6 +940,11 @@ macro_rules! assert_counter { assert_metric!(result, $name, Some($value.into()), None, &attributes); }; + ($name:literal, $value: expr, $attributes: expr) => { + let result = crate::metrics::collect_metrics().assert($name, crate::metrics::test_utils::MetricType::Counter, $value, $attributes); + assert_metric!(result, $name, Some($value.into()), None, &$attributes); + }; + ($name:literal, $value: expr) => { let result = crate::metrics::collect_metrics().assert($name, crate::metrics::test_utils::MetricType::Counter, $value, &[]); assert_metric!(result, $name, Some($value.into()), None, &[]); @@ -1209,6 +1215,7 @@ mod test { let attributes = vec![KeyValue::new("attr", "val")]; u64_counter!("test", "test description", 1, attributes); assert_counter!("test", 1, "attr" = "val"); + assert_counter!("test", 1, &attributes); } #[test] diff --git a/apollo-router/src/services/layers/persisted_queries/manifest_poller.rs b/apollo-router/src/services/layers/persisted_queries/manifest_poller.rs index 496c98d0a7..611c68b832 100644 --- a/apollo-router/src/services/layers/persisted_queries/manifest_poller.rs +++ b/apollo-router/src/services/layers/persisted_queries/manifest_poller.rs @@ -34,6 +34,13 @@ pub struct FullPersistedQueryOperationId { /// An in memory cache of persisted queries. pub type PersistedQueryManifest = HashMap; +/// Describes whether the router should allow or deny a given request. +/// with an error, or allow it but log the operation as unknown. +pub(crate) struct FreeformGraphQLAction { + pub(crate) should_allow: bool, + pub(crate) should_log: bool, +} + /// How the router should respond to requests that are not resolved as the IDs /// of an operation in the manifest. (For the most part this means "requests /// sent as freeform GraphQL", though it also includes requests sent as an ID @@ -58,49 +65,43 @@ pub(crate) enum FreeformGraphQLBehavior { }, } -/// Describes what the router should do for a given request: allow it, deny it -/// with an error, or allow it but log the operation as unknown. -pub(crate) enum FreeformGraphQLAction { - Allow, - Deny, - AllowAndLog, - DenyAndLog, -} - impl FreeformGraphQLBehavior { fn action_for_freeform_graphql( &self, ast: Result<&ast::Document, &str>, ) -> FreeformGraphQLAction { match self { - FreeformGraphQLBehavior::AllowAll { .. } => FreeformGraphQLAction::Allow, + FreeformGraphQLBehavior::AllowAll { .. } => FreeformGraphQLAction { + should_allow: true, + should_log: false, + }, // Note that this branch doesn't get called in practice, because we catch // DenyAll at an earlier phase with never_allows_freeform_graphql. - FreeformGraphQLBehavior::DenyAll { log_unknown, .. } => { - if *log_unknown { - FreeformGraphQLAction::DenyAndLog - } else { - FreeformGraphQLAction::Deny - } - } + FreeformGraphQLBehavior::DenyAll { log_unknown, .. } => FreeformGraphQLAction { + should_allow: false, + should_log: *log_unknown, + }, FreeformGraphQLBehavior::AllowIfInSafelist { safelist, log_unknown, .. } => { if safelist.is_allowed(ast) { - FreeformGraphQLAction::Allow - } else if *log_unknown { - FreeformGraphQLAction::DenyAndLog + FreeformGraphQLAction { + should_allow: true, + should_log: false, + } } else { - FreeformGraphQLAction::Deny + FreeformGraphQLAction { + should_allow: false, + should_log: *log_unknown, + } } } FreeformGraphQLBehavior::LogUnlessInSafelist { safelist, .. } => { - if safelist.is_allowed(ast) { - FreeformGraphQLAction::Allow - } else { - FreeformGraphQLAction::AllowAndLog + FreeformGraphQLAction { + should_allow: true, + should_log: !safelist.is_allowed(ast), } } } diff --git a/apollo-router/src/services/layers/persisted_queries/mod.rs b/apollo-router/src/services/layers/persisted_queries/mod.rs index 0208b2f878..ae849a6170 100644 --- a/apollo-router/src/services/layers/persisted_queries/mod.rs +++ b/apollo-router/src/services/layers/persisted_queries/mod.rs @@ -13,7 +13,6 @@ pub use manifest_poller::PersistedQueryManifest; pub(crate) use manifest_poller::PersistedQueryManifestPoller; use tower::BoxError; -use self::manifest_poller::FreeformGraphQLAction; use super::query_analysis::ParsedDocument; use crate::graphql::Error as GraphQLError; use crate::plugins::telemetry::CLIENT_NAME; @@ -23,6 +22,8 @@ use crate::Configuration; const DONT_CACHE_RESPONSE_VALUE: &str = "private, no-cache, must-revalidate"; const PERSISTED_QUERIES_CLIENT_NAME_CONTEXT_KEY: &str = "apollo_persisted_queries::client_name"; +const PERSISTED_QUERIES_SAFELIST_SKIP_ENFORCEMENT_CONTEXT_KEY: &str = + "apollo_persisted_queries::safelist::skip_enforcement"; struct UsedQueryIdFromManifest; @@ -34,6 +35,14 @@ pub(crate) struct PersistedQueryLayer { introspection_enabled: bool, } +fn skip_enforcement(request: &SupergraphRequest) -> bool { + request + .context + .get(PERSISTED_QUERIES_SAFELIST_SKIP_ENFORCEMENT_CONTEXT_KEY) + .unwrap_or_default() + .unwrap_or(false) +} + impl PersistedQueryLayer { /// Create a new [`PersistedQueryLayer`] from CLI options, YAML configuration, /// and optionally, an existing persisted query manifest poller. @@ -69,6 +78,9 @@ impl PersistedQueryLayer { manifest_poller, &persisted_query_id, ) + } else if skip_enforcement(&request) { + // A plugin told us to allow this, so let's skip to require_id check. + Ok(request) } else if let Some(log_unknown) = manifest_poller.never_allows_freeform_graphql() { // If we don't have an ID and we require an ID, return an error immediately, if log_unknown { @@ -229,46 +241,39 @@ impl PersistedQueryLayer { return Ok(request); } - match manifest_poller.action_for_freeform_graphql(Ok(&doc.ast)) { - FreeformGraphQLAction::Allow => { - u64_counter!( - "apollo.router.operations.persisted_queries", - "Total requests with persisted queries enabled", - 1 - ); - Ok(request) - } - FreeformGraphQLAction::Deny => { - u64_counter!( - "apollo.router.operations.persisted_queries", - "Total requests with persisted queries enabled", - 1, - persisted_queries.safelist.rejected.unknown = false - ); - Err(supergraph_err_operation_not_in_safelist(request)) - } - // Note that this might even include complaining about an operation that came via APQs. - FreeformGraphQLAction::AllowAndLog => { - u64_counter!( - "apollo.router.operations.persisted_queries", - "Total requests with persisted queries enabled", - 1, - persisted_queries.logged = true - ); - log_unknown_operation(operation_body); - Ok(request) - } - FreeformGraphQLAction::DenyAndLog => { - u64_counter!( - "apollo.router.operations.persisted_queries", - "Total requests with persisted queries enabled", - 1, - persisted_queries.safelist.rejected.unknown = true, - persisted_queries.logged = true - ); - log_unknown_operation(operation_body); - Err(supergraph_err_operation_not_in_safelist(request)) - } + let mut metric_attributes = vec![]; + let freeform_graphql_action = manifest_poller.action_for_freeform_graphql(Ok(&doc.ast)); + let skip_enforcement = skip_enforcement(&request); + let allow = skip_enforcement || freeform_graphql_action.should_allow; + if !allow { + metric_attributes.push(opentelemetry::KeyValue::new( + "persisted_queries.safelist.rejected.unknown".to_string(), + true, + )); + } else if !freeform_graphql_action.should_allow { + metric_attributes.push(opentelemetry::KeyValue::new( + "persisted_queries.safelist.enforcement_skipped".to_string(), + true, + )); + } + if freeform_graphql_action.should_log { + log_unknown_operation(operation_body); + metric_attributes.push(opentelemetry::KeyValue::new( + "persisted_queries.logged".to_string(), + true, + )); + } + u64_counter!( + "apollo.router.operations.persisted_queries", + "Total requests with persisted queries enabled", + 1, + metric_attributes + ); + + if allow { + Ok(request) + } else { + Err(supergraph_err_operation_not_in_safelist(request)) } } @@ -370,9 +375,10 @@ fn supergraph_err_operation_not_in_safelist(request: SupergraphRequest) -> Super } fn graphql_err_pq_id_required() -> GraphQLError { - graphql_err("PERSISTED_QUERY_ID_REQUIRED", + graphql_err( + "PERSISTED_QUERY_ID_REQUIRED", "This endpoint does not allow freeform GraphQL requests; operations must be sent by ID in the persisted queries GraphQL extension.", - ) + ) } fn supergraph_err_pq_id_required(request: SupergraphRequest) -> SupergraphResponse { @@ -407,12 +413,15 @@ mod tests { use maplit::hashmap; use serde_json::json; + use tracing::instrument::WithSubscriber; use super::*; + use crate::assert_snapshot_subscriber; use crate::configuration::Apq; use crate::configuration::PersistedQueries; use crate::configuration::PersistedQueriesSafelist; use crate::configuration::Supergraph; + use crate::metrics::FutureMetricsExt; use crate::services::layers::persisted_queries::manifest_poller::FreeformGraphQLBehavior; use crate::services::layers::query_analysis::QueryAnalysisLayer; use crate::spec::Schema; @@ -722,9 +731,21 @@ mod tests { pq_layer: &PersistedQueryLayer, query_analysis_layer: &QueryAnalysisLayer, body: &str, + skip_enforcement: bool, ) -> SupergraphRequest { + let context = Context::new(); + if skip_enforcement { + context + .insert( + PERSISTED_QUERIES_SAFELIST_SKIP_ENFORCEMENT_CONTEXT_KEY, + true, + ) + .unwrap(); + } + let incoming_request = SupergraphRequest::fake_builder() .query(body) + .context(context) .build() .unwrap(); @@ -747,9 +768,11 @@ mod tests { pq_layer: &PersistedQueryLayer, query_analysis_layer: &QueryAnalysisLayer, body: &str, + log_unknown: bool, + counter_value: u64, ) { let request_with_analyzed_query = - run_first_two_layers(pq_layer, query_analysis_layer, body).await; + run_first_two_layers(pq_layer, query_analysis_layer, body, false).await; let mut supergraph_response = pq_layer .supergraph_request_with_analyzed_query(request_with_analyzed_query) @@ -766,119 +789,208 @@ mod tests { response.errors, vec![graphql_err_operation_not_in_safelist()] ); + let mut metric_attributes = vec![opentelemetry::KeyValue::new( + "persisted_queries.safelist.rejected.unknown".to_string(), + true, + )]; + if log_unknown { + metric_attributes.push(opentelemetry::KeyValue::new( + "persisted_queries.logged".to_string(), + true, + )); + } + assert_counter!( + "apollo.router.operations.persisted_queries", + counter_value, + &metric_attributes + ); } async fn allowed_by_safelist( pq_layer: &PersistedQueryLayer, query_analysis_layer: &QueryAnalysisLayer, body: &str, + log_unknown: bool, + skip_enforcement: bool, + counter_value: u64, ) { let request_with_analyzed_query = - run_first_two_layers(pq_layer, query_analysis_layer, body).await; + run_first_two_layers(pq_layer, query_analysis_layer, body, skip_enforcement).await; pq_layer .supergraph_request_with_analyzed_query(request_with_analyzed_query) .await .ok() .expect("pq layer second hook returned error response instead of returning a request"); - } - #[tokio::test(flavor = "multi_thread")] - async fn pq_layer_freeform_graphql_with_safelist() { - let manifest = HashMap::from([( - FullPersistedQueryOperationId { - operation_id: "valid-syntax".to_string(), - client_name: None, - }, - "fragment A on Query { me { id } } query SomeOp { ...A ...B } fragment,,, B on Query{me{name,username} } # yeah" - .to_string(), - ), ( - FullPersistedQueryOperationId { - operation_id: "invalid-syntax".to_string(), - client_name: None, - }, - "}}}".to_string()), - ]); + let mut metric_attributes = vec![]; + if skip_enforcement { + metric_attributes.push(opentelemetry::KeyValue::new( + "persisted_queries.safelist.enforcement_skipped".to_string(), + true, + )); + if log_unknown { + metric_attributes.push(opentelemetry::KeyValue::new( + "persisted_queries.logged".to_string(), + true, + )); + } + } - let (_mock_guard, uplink_config) = mock_pq_uplink(&manifest).await; + assert_counter!( + "apollo.router.operations.persisted_queries", + counter_value, + &metric_attributes + ); + } - let config = Configuration::fake_builder() - .persisted_query( - PersistedQueries::builder() - .enabled(true) - .safelist(PersistedQueriesSafelist::builder().enabled(true).build()) - .build(), - ) - .uplink(uplink_config) - .apq(Apq::fake_builder().enabled(false).build()) - .supergraph(Supergraph::fake_builder().introspection(true).build()) - .build() - .unwrap(); + async fn pq_layer_freeform_graphql_with_safelist(log_unknown: bool) { + async move { + let manifest = HashMap::from([ + ( + FullPersistedQueryOperationId { + operation_id: "valid-syntax".to_string(), + client_name: None, + }, + "fragment A on Query { me { id } } query SomeOp { ...A ...B } fragment,,, B on Query{me{name,username} } # yeah" + .to_string(), + ), + ( + FullPersistedQueryOperationId { + operation_id: "invalid-syntax".to_string(), + client_name: None, + }, + "}}}".to_string(), + ), + ]); - let pq_layer = PersistedQueryLayer::new(&config).await.unwrap(); + let (_mock_guard, uplink_config) = mock_pq_uplink(&manifest).await; - let schema = Arc::new( - Schema::parse( - include_str!("../../../testdata/supergraph.graphql"), - &Default::default(), - ) - .unwrap(), - ); + let config = Configuration::fake_builder() + .persisted_query( + PersistedQueries::builder() + .enabled(true) + .safelist(PersistedQueriesSafelist::builder().enabled(true).build()) + .log_unknown(log_unknown) + .build(), + ) + .uplink(uplink_config) + .apq(Apq::fake_builder().enabled(false).build()) + .supergraph(Supergraph::fake_builder().introspection(true).build()) + .build() + .unwrap(); - let query_analysis_layer = QueryAnalysisLayer::new(schema, Arc::new(config)).await; + let pq_layer = PersistedQueryLayer::new(&config).await.unwrap(); - // A random query is blocked. - denied_by_safelist( - &pq_layer, - &query_analysis_layer, - "query SomeQuery { me { id } }", - ) - .await; + let schema = Arc::new(Schema::parse(include_str!("../../../testdata/supergraph.graphql"), &Default::default()).unwrap()); - // The exact string from the manifest is allowed. - allowed_by_safelist( - &pq_layer, - &query_analysis_layer, - "fragment A on Query { me { id } } query SomeOp { ...A ...B } fragment,,, B on Query{me{name,username} } # yeah", - ) - .await; + let query_analysis_layer = QueryAnalysisLayer::new(schema, Arc::new(config)).await; - // Reordering definitions and reformatting a bit matches. - allowed_by_safelist( + // A random query is blocked. + denied_by_safelist( &pq_layer, &query_analysis_layer, - "#comment\n fragment, B on Query , { me{name username} } query SomeOp { ...A ...B } fragment \nA on Query { me{ id} }" + "query SomeQuery { me { id } }", + log_unknown, + 1, ).await; - // Reordering fields does not match! - denied_by_safelist( + // But it is allowed with skip_enforcement set. + allowed_by_safelist( &pq_layer, &query_analysis_layer, - "fragment A on Query { me { id } } query SomeOp { ...A ...B } fragment,,, B on Query{me{username,name} } # yeah" + "query SomeQuery { me { id } }", + log_unknown, + true, + 1, ).await; - // Introspection queries are allowed (even using fragments and aliases), because - // introspection is enabled. - allowed_by_safelist( - &pq_layer, - &query_analysis_layer, - r#"fragment F on Query { __typename foo: __schema { __typename } } query Q { __type(name: "foo") { name } ...F }"#, - ).await; - - // Multiple spreads of the same fragment are also allowed - // (https://github.com/apollographql/apollo-rs/issues/613) - allowed_by_safelist( - &pq_layer, - &query_analysis_layer, - r#"fragment F on Query { __typename foo: __schema { __typename } } query Q { __type(name: "foo") { name } ...F ...F }"#, - ).await; - - // But adding any top-level non-introspection field is enough to make it not count as introspection. - denied_by_safelist( - &pq_layer, - &query_analysis_layer, - r#"fragment F on Query { __typename foo: __schema { __typename } me { id } } query Q { __type(name: "foo") { name } ...F }"#, - ).await; + // The exact string from the manifest is allowed. + allowed_by_safelist( + &pq_layer, + &query_analysis_layer, + "fragment A on Query { me { id } } query SomeOp { ...A ...B } fragment,,, B on Query{me{name,username} } # yeah", + log_unknown, + false, + 1, + ) + .await; + + // Reordering definitions and reformatting a bit matches. + allowed_by_safelist( + &pq_layer, + &query_analysis_layer, + "#comment\n fragment, B on Query , { me{name username} } query SomeOp { ...A ...B } fragment \nA on Query { me{ id} }", + log_unknown, + false, + 2, + ) + .await; + + // Reordering fields does not match! + denied_by_safelist( + &pq_layer, + &query_analysis_layer, + "fragment A on Query { me { id } } query SomeOp { ...A ...B } fragment,,, B on Query{me{username,name} } # yeah", + log_unknown, + 2, + ) + .await; + + // Introspection queries are allowed (even using fragments and aliases), because + // introspection is enabled. + allowed_by_safelist( + &pq_layer, + &query_analysis_layer, + r#"fragment F on Query { __typename foo: __schema { __typename } } query Q { __type(name: "foo") { name } ...F }"#, + log_unknown, + false, + // Note that introspection queries don't actually interact with the PQ machinery enough + // to update this metric, for better or for worse. + 2, + ) + .await; + + // Multiple spreads of the same fragment are also allowed + // (https://github.com/apollographql/apollo-rs/issues/613) + allowed_by_safelist( + &pq_layer, + &query_analysis_layer, + r#"fragment F on Query { __typename foo: __schema { __typename } } query Q { __type(name: "foo") { name } ...F ...F }"#, + log_unknown, + false, + // Note that introspection queries don't actually interact with the PQ machinery enough + // to update this metric, for better or for worse. + 2, + ) + .await; + + // But adding any top-level non-introspection field is enough to make it not count as introspection. + denied_by_safelist( + &pq_layer, + &query_analysis_layer, + r#"fragment F on Query { __typename foo: __schema { __typename } me { id } } query Q { __type(name: "foo") { name } ...F }"#, + log_unknown, + 3, + ) + .await; + } + .with_metrics() + .await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn pq_layer_freeform_graphql_with_safelist_log_unknown_false() { + pq_layer_freeform_graphql_with_safelist(false).await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn pq_layer_freeform_graphql_with_safelist_log_unknown_true() { + async { + pq_layer_freeform_graphql_with_safelist(true).await; + } + .with_subscriber(assert_snapshot_subscriber!()) + .await } #[tokio::test(flavor = "multi_thread")] @@ -1042,6 +1154,22 @@ mod tests { .await .expect("could not get response from pq layer"); assert_eq!(response.errors, vec![graphql_err_pq_id_required()]); + + // Try again skipping enforcement. + let context = Context::new(); + context + .insert( + PERSISTED_QUERIES_SAFELIST_SKIP_ENFORCEMENT_CONTEXT_KEY, + true, + ) + .unwrap(); + let incoming_request = SupergraphRequest::fake_builder() + .query("query { typename }") + .context(context) + .build() + .unwrap(); + assert!(incoming_request.supergraph_request.body().query.is_some()); + assert!(pq_layer.supergraph_request(incoming_request).is_ok()); } #[tokio::test(flavor = "multi_thread")] diff --git a/apollo-router/src/services/layers/persisted_queries/snapshots/apollo_router__services__layers__persisted_queries__tests__pq_layer_freeform_graphql_with_safelist_log_unknown_true@logs.snap b/apollo-router/src/services/layers/persisted_queries/snapshots/apollo_router__services__layers__persisted_queries__tests__pq_layer_freeform_graphql_with_safelist_log_unknown_true@logs.snap new file mode 100644 index 0000000000..f9a850f1c8 --- /dev/null +++ b/apollo-router/src/services/layers/persisted_queries/snapshots/apollo_router__services__layers__persisted_queries__tests__pq_layer_freeform_graphql_with_safelist_log_unknown_true@logs.snap @@ -0,0 +1,21 @@ +--- +source: apollo-router/src/services/layers/persisted_queries/mod.rs +expression: yaml +snapshot_kind: text +--- +- fields: + operation_body: "query SomeQuery { me { id } }" + level: WARN + message: unknown operation +- fields: + operation_body: "query SomeQuery { me { id } }" + level: WARN + message: unknown operation +- fields: + operation_body: "fragment A on Query { me { id } } query SomeOp { ...A ...B } fragment,,, B on Query{me{username,name} } # yeah" + level: WARN + message: unknown operation +- fields: + operation_body: "fragment F on Query { __typename foo: __schema { __typename } me { id } } query Q { __type(name: \"foo\") { name } ...F }" + level: WARN + message: unknown operation diff --git a/dev-docs/logging.md b/dev-docs/logging.md index abf7ef32c1..bb2517d74b 100644 --- a/dev-docs/logging.md +++ b/dev-docs/logging.md @@ -106,7 +106,7 @@ expression: yaml - fields: alg: ES256 reason: "invalid type: string \"Hmm\", expected a sequence" - index: 5 + index: 5 level: WARN message: "ignoring a key since it is not valid, enable debug logs to full content" ``` @@ -130,7 +130,7 @@ Use `with_subscriber` to attach a subscriber to an async block. ```rust #[tokio::test] async fn test_async() { - async{...}.with_subscriber(assert_snapshot_subscriber!()) + async{...}.with_subscriber(assert_snapshot_subscriber!()).await } ``` diff --git a/dev-docs/metrics.md b/dev-docs/metrics.md index 34530201ef..37b8431d71 100644 --- a/dev-docs/metrics.md +++ b/dev-docs/metrics.md @@ -136,7 +136,7 @@ Make sure to use `.with_metrics()` method on the async block to ensure that the // Multi-threaded runtime needs to use a tokio task local to avoid tests interfering with each other async { u64_counter!("test", "test description", 1, "attr" => "val"); - assert_counter!("test", 1, "attr" => "val"); + assert_counter!("test", 1, "attr" = "val"); } .with_metrics() .await; @@ -147,7 +147,7 @@ Make sure to use `.with_metrics()` method on the async block to ensure that the async { // It's a single threaded tokio runtime, so we can still use a thread local u64_counter!("test", "test description", 1, "attr" => "val"); - assert_counter!("test", 1, "attr" => "val"); + assert_counter!("test", 1, "attr" = "val"); } .with_metrics() .await; diff --git a/docs/source/routing/security/persisted-queries.mdx b/docs/source/routing/security/persisted-queries.mdx index 845e0aa75d..ea7d1e92b4 100644 --- a/docs/source/routing/security/persisted-queries.mdx +++ b/docs/source/routing/security/persisted-queries.mdx @@ -64,7 +64,7 @@ persisted_queries: log_unknown: true ``` -If used with the [`safelist`](#safelist) option, the router logs unregistered and rejected operations. With [`safelist.required_id`](#require_id) off, the only rejected operations are unregistered ones. If [`safelist.required_id`](#require_id) is turned on, operations can be rejected even when registered because they use operation IDs rather than operation strings. +If used with the [`safelist`](#safelist) option, the router logs unregistered and rejected operations. With [`safelist.require_id`](#require_id) off, the only rejected operations are unregistered ones. If [`safelist.require_id`](#require_id) is turned on, operations can be rejected even when registered because they use operation IDs rather than operation strings. #### `experimental_prewarm_query_plan_cache` @@ -114,7 +114,7 @@ To enable safelisting, you _must_ turn off [automatic persisted queries](/router -By default, the [`require_id`](#required_id) suboption is `false`, meaning the router accepts both operation IDs and operation strings as long as the operation is registered. +By default, the [`require_id`](#require_id) suboption is `false`, meaning the router accepts both operation IDs and operation strings as long as the operation is registered. #### `require_id` @@ -152,6 +152,16 @@ If this context value is not set by a customization, your router will use the sa If your request specifies an ID and a client name but there is no operation in the PQL with that ID and client name, your router will look to see if there is an operation with that ID and no client name specified, and use that if it finds it. +#### `apollo_persisted_queries::safelist::skip_enforcement` + +If safelisting is enabled, you can still opt out of safelist enforcement on a per-request basis. + +Your customization (Rhai script, coprocessor, etc) can examine a request during the [Router Service stage](/graphos/routing/customization/overview#request-path) of the request path and set the `apollo_persisted_queries::safelist::skip_enforcement` value in the request context to the boolean value `true`. + +For any request where you set this value, Router will skip safelist enforcement: requests with a full operation string will be allowed even if they are not in the safelist, and even if [`safelist.required_id`](#require_id) is enabled. + +This does not affect the behavior of the [`log_unknown` option](#log_unknown): unknown operations will still be logged if that option is set. + ## Limitations * **Unsupported with offline license**. An GraphOS Router using an [offline Enterprise license](/router/enterprise-features/#offline-enterprise-license) cannot use safelisting with persisted queries. The feature relies on Apollo Uplink to fetch persisted query manifests, so it doesn't work as designed when the router is disconnected from Uplink.