diff --git a/apollo-router/src/plugins/response_cache/plugin.rs b/apollo-router/src/plugins/response_cache/plugin.rs index 00bf67506d..63319a2b41 100644 --- a/apollo-router/src/plugins/response_cache/plugin.rs +++ b/apollo-router/src/plugins/response_cache/plugin.rs @@ -1267,10 +1267,7 @@ async fn cache_lookup_root( Ok(value) => { if value.control.can_use() { let control = value.control.clone(); - request - .context - .extensions() - .with_lock(|lock| lock.insert(control)); + update_cache_control(&request.context, &control); if debug { let root_operation_fields: Vec = request .executable_document @@ -1632,7 +1629,6 @@ fn update_cache_control(context: &Context, cache_control: &CacheControl) { if let Some(c) = lock.get_mut::() { *c = c.merge(cache_control); } else { - //FIXME: race condition. We need an Entry API for private entries lock.insert(cache_control.clone()); } }) diff --git a/apollo-router/src/plugins/response_cache/snapshots/apollo_router__plugins__response_cache__tests__private_and_public-3.snap b/apollo-router/src/plugins/response_cache/snapshots/apollo_router__plugins__response_cache__tests__private_and_public-3.snap new file mode 100644 index 0000000000..15d5f820ee --- /dev/null +++ b/apollo-router/src/plugins/response_cache/snapshots/apollo_router__plugins__response_cache__tests__private_and_public-3.snap @@ -0,0 +1,97 @@ +--- +source: apollo-router/src/plugins/response_cache/tests.rs +expression: cache_keys +--- +[ + { + "key": "private_and_public-version:1.0:subgraph:orga:type:Query:hash:d72fc5bd9cc1994bdd5bee5d0f62417a27385b44300212a56d332be2e064798e:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c:03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4", + "invalidationKeys": [], + "kind": { + "rootFields": [ + "orga" + ] + }, + "subgraphName": "orga", + "subgraphRequest": { + "query": "{ orga(id: \"2\") { name } }" + }, + "status": "cached", + "cacheControl": { + "created": 0, + "maxAge": 86400, + "private": true + }, + "data": { + "data": { + "orga": { + "name": "test_orga" + } + } + } + }, + { + "key": "private_and_public-version:1.0:subgraph:user:type:Query:hash:26ab4118dbabffe5dfbef4462513caa8b6c8941363cf2eafa874bf1d47a3c468:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", + "invalidationKeys": [ + "currentUser" + ], + "kind": { + "rootFields": [ + "currentUser" + ] + }, + "subgraphName": "user", + "subgraphRequest": { + "query": "{ currentUser { activeOrganization { __typename id } } }" + }, + "status": "cached", + "cacheControl": { + "created": 0, + "maxAge": 86400, + "public": true + }, + "data": { + "data": { + "currentUser": { + "activeOrganization": { + "__typename": "Organization", + "id": "1" + } + } + } + } + }, + { + "key": "version:1.0:subgraph:orga:type:Organization:entity:bcc0a4a9f8c595510c0ff8849bc36b402ac3f52506392d67107c623528ff11f4:representation::hash:a7ccf1576978c6598f6d10d8855fc683f71242533dc6d5a706743bd9bbfa554c:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c:03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4", + "invalidationKeys": [ + "organization", + "organization-1" + ], + "kind": { + "typename": "Organization", + "entityKey": { + "id": "1" + } + }, + "subgraphName": "orga", + "subgraphRequest": { + "query": "query($representations: [_Any!]!) { _entities(representations: $representations) { ... on Organization { creatorUser { __typename id } } } }", + "variables": { + "representations": [] + } + }, + "status": "cached", + "cacheControl": { + "created": 0, + "maxAge": 86400, + "private": true + }, + "data": { + "data": { + "creatorUser": { + "__typename": "User", + "id": 2 + } + } + } + } +] diff --git a/apollo-router/src/plugins/response_cache/snapshots/apollo_router__plugins__response_cache__tests__private_and_public-5.snap b/apollo-router/src/plugins/response_cache/snapshots/apollo_router__plugins__response_cache__tests__private_and_public-5.snap new file mode 100644 index 0000000000..0560124c45 --- /dev/null +++ b/apollo-router/src/plugins/response_cache/snapshots/apollo_router__plugins__response_cache__tests__private_and_public-5.snap @@ -0,0 +1,102 @@ +--- +source: apollo-router/src/plugins/response_cache/tests.rs +expression: cache_keys +--- +[ + { + "key": "version:1.0:subgraph:orga:type:Query:hash:d72fc5bd9cc1994bdd5bee5d0f62417a27385b44300212a56d332be2e064798e:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c:f8638b979b2f4f793ddb6dbd197e0ee25a7a6ea32b0ae22f5e3c5d119d839e75", + "invalidationKeys": [], + "kind": { + "rootFields": [ + "orga" + ] + }, + "subgraphName": "orga", + "subgraphRequest": { + "query": "{ orga(id: \"2\") { name } }" + }, + "status": "new", + "cacheControl": { + "created": 0, + "maxAge": 86400, + "private": true + }, + "data": { + "data": { + "orga": { + "name": "test_orga" + } + } + } + }, + { + "key": "private_and_public-version:1.0:subgraph:user:type:Query:hash:26ab4118dbabffe5dfbef4462513caa8b6c8941363cf2eafa874bf1d47a3c468:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", + "invalidationKeys": [ + "currentUser" + ], + "kind": { + "rootFields": [ + "currentUser" + ] + }, + "subgraphName": "user", + "subgraphRequest": { + "query": "{ currentUser { activeOrganization { __typename id } } }" + }, + "status": "cached", + "cacheControl": { + "created": 0, + "maxAge": 86400, + "public": true + }, + "data": { + "data": { + "currentUser": { + "activeOrganization": { + "__typename": "Organization", + "id": "1" + } + } + } + } + }, + { + "key": "version:1.0:subgraph:orga:type:Organization:entity:bcc0a4a9f8c595510c0ff8849bc36b402ac3f52506392d67107c623528ff11f4:representation::hash:a7ccf1576978c6598f6d10d8855fc683f71242533dc6d5a706743bd9bbfa554c:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c:f8638b979b2f4f793ddb6dbd197e0ee25a7a6ea32b0ae22f5e3c5d119d839e75", + "invalidationKeys": [ + "organization", + "organization-1" + ], + "kind": { + "typename": "Organization", + "entityKey": { + "id": "1" + } + }, + "subgraphName": "orga", + "subgraphRequest": { + "query": "query($representations: [_Any!]!) { _entities(representations: $representations) { ... on Organization { creatorUser { __typename id } } } }", + "variables": { + "representations": [ + { + "id": "1", + "__typename": "Organization" + } + ] + } + }, + "status": "new", + "cacheControl": { + "created": 0, + "maxAge": 86400, + "private": true + }, + "data": { + "data": { + "creatorUser": { + "__typename": "User", + "id": 2 + } + } + } + } +] diff --git a/apollo-router/src/plugins/response_cache/snapshots/apollo_router__plugins__response_cache__tests__private_and_public.snap b/apollo-router/src/plugins/response_cache/snapshots/apollo_router__plugins__response_cache__tests__private_and_public.snap new file mode 100644 index 0000000000..880a237392 --- /dev/null +++ b/apollo-router/src/plugins/response_cache/snapshots/apollo_router__plugins__response_cache__tests__private_and_public.snap @@ -0,0 +1,102 @@ +--- +source: apollo-router/src/plugins/response_cache/tests.rs +expression: cache_keys +--- +[ + { + "key": "version:1.0:subgraph:orga:type:Query:hash:d72fc5bd9cc1994bdd5bee5d0f62417a27385b44300212a56d332be2e064798e:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c:03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4", + "invalidationKeys": [], + "kind": { + "rootFields": [ + "orga" + ] + }, + "subgraphName": "orga", + "subgraphRequest": { + "query": "{ orga(id: \"2\") { name } }" + }, + "status": "new", + "cacheControl": { + "created": 0, + "maxAge": 86400, + "private": true + }, + "data": { + "data": { + "orga": { + "name": "test_orga" + } + } + } + }, + { + "key": "version:1.0:subgraph:user:type:Query:hash:26ab4118dbabffe5dfbef4462513caa8b6c8941363cf2eafa874bf1d47a3c468:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c", + "invalidationKeys": [ + "currentUser" + ], + "kind": { + "rootFields": [ + "currentUser" + ] + }, + "subgraphName": "user", + "subgraphRequest": { + "query": "{ currentUser { activeOrganization { __typename id } } }" + }, + "status": "new", + "cacheControl": { + "created": 0, + "maxAge": 86400, + "public": true + }, + "data": { + "data": { + "currentUser": { + "activeOrganization": { + "__typename": "Organization", + "id": "1" + } + } + } + } + }, + { + "key": "version:1.0:subgraph:orga:type:Organization:entity:bcc0a4a9f8c595510c0ff8849bc36b402ac3f52506392d67107c623528ff11f4:representation::hash:a7ccf1576978c6598f6d10d8855fc683f71242533dc6d5a706743bd9bbfa554c:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c:03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4", + "invalidationKeys": [ + "organization", + "organization-1" + ], + "kind": { + "typename": "Organization", + "entityKey": { + "id": "1" + } + }, + "subgraphName": "orga", + "subgraphRequest": { + "query": "query($representations: [_Any!]!) { _entities(representations: $representations) { ... on Organization { creatorUser { __typename id } } } }", + "variables": { + "representations": [ + { + "id": "1", + "__typename": "Organization" + } + ] + } + }, + "status": "new", + "cacheControl": { + "created": 0, + "maxAge": 86400, + "private": true + }, + "data": { + "data": { + "creatorUser": { + "__typename": "User", + "id": 2 + } + } + } + } +] diff --git a/apollo-router/src/plugins/response_cache/snapshots/apollo_router__plugins__response_cache__tests__private-3.snap b/apollo-router/src/plugins/response_cache/snapshots/apollo_router__plugins__response_cache__tests__private_only-3.snap similarity index 86% rename from apollo-router/src/plugins/response_cache/snapshots/apollo_router__plugins__response_cache__tests__private-3.snap rename to apollo-router/src/plugins/response_cache/snapshots/apollo_router__plugins__response_cache__tests__private_only-3.snap index 15b0713daa..9c70a59ea7 100644 --- a/apollo-router/src/plugins/response_cache/snapshots/apollo_router__plugins__response_cache__tests__private-3.snap +++ b/apollo-router/src/plugins/response_cache/snapshots/apollo_router__plugins__response_cache__tests__private_only-3.snap @@ -4,7 +4,7 @@ expression: cache_keys --- [ { - "key": "private-version:1.0:subgraph:user:type:Query:hash:26ab4118dbabffe5dfbef4462513caa8b6c8941363cf2eafa874bf1d47a3c468:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c:03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4", + "key": "private_only-version:1.0:subgraph:user:type:Query:hash:26ab4118dbabffe5dfbef4462513caa8b6c8941363cf2eafa874bf1d47a3c468:data:d9d84a3c7ffc27b0190a671212f3740e5b8478e84e23825830e97822e25cf05c:03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4", "invalidationKeys": [ "currentUser" ], diff --git a/apollo-router/src/plugins/response_cache/snapshots/apollo_router__plugins__response_cache__tests__private-5.snap b/apollo-router/src/plugins/response_cache/snapshots/apollo_router__plugins__response_cache__tests__private_only-5.snap similarity index 100% rename from apollo-router/src/plugins/response_cache/snapshots/apollo_router__plugins__response_cache__tests__private-5.snap rename to apollo-router/src/plugins/response_cache/snapshots/apollo_router__plugins__response_cache__tests__private_only-5.snap diff --git a/apollo-router/src/plugins/response_cache/snapshots/apollo_router__plugins__response_cache__tests__private.snap b/apollo-router/src/plugins/response_cache/snapshots/apollo_router__plugins__response_cache__tests__private_only.snap similarity index 100% rename from apollo-router/src/plugins/response_cache/snapshots/apollo_router__plugins__response_cache__tests__private.snap rename to apollo-router/src/plugins/response_cache/snapshots/apollo_router__plugins__response_cache__tests__private_only.snap diff --git a/apollo-router/src/plugins/response_cache/tests.rs b/apollo-router/src/plugins/response_cache/tests.rs index 9ac4e8a29d..3925c754aa 100644 --- a/apollo-router/src/plugins/response_cache/tests.rs +++ b/apollo-router/src/plugins/response_cache/tests.rs @@ -1319,7 +1319,7 @@ async fn no_store_from_request() { } #[tokio::test] -async fn private() { +async fn private_only() { let query = "query { currentUser { activeOrganization { id creatorUser { __typename id } } } }"; let valid_schema = Arc::new(Schema::parse_and_validate(SCHEMA, "test.graphql").unwrap()); @@ -1361,7 +1361,7 @@ async fn private() { required_to_start: true, pool_size: default_pool_size(), batch_size: default_batch_size(), - namespace: Some(String::from("private")), + namespace: Some(String::from("private_only")), }) .await .unwrap(); @@ -1463,6 +1463,217 @@ async fn private() { let context = Context::new(); context.insert_json_value("sub", "1234".into()); + let request = supergraph::Request::fake_builder() + .query(query) + .context(context) + .header( + HeaderName::from_static(CACHE_DEBUG_HEADER_NAME), + HeaderValue::from_static("true"), + ) + .build() + .unwrap(); + let mut response = service.ready().await.unwrap().call(request).await.unwrap(); + assert!( + response + .response + .headers() + .get(CACHE_CONTROL) + .unwrap() + .to_str() + .unwrap() + .contains("private") + ); + let mut cache_keys: CacheKeysContext = response + .context + .get(CONTEXT_DEBUG_CACHE_KEYS) + .unwrap() + .unwrap(); + cache_keys.iter_mut().for_each(|ck| { + ck.invalidation_keys.sort(); + ck.cache_control.created = 0; + }); + cache_keys.sort_by(|a, b| a.invalidation_keys.cmp(&b.invalidation_keys)); + insta::assert_json_snapshot!(cache_keys); + + let mut response = response.next_response().await.unwrap(); + assert!( + response + .extensions + .remove(CACHE_DEBUG_EXTENSIONS_KEY) + .is_some() + ); + + insta::assert_json_snapshot!(response, @r###" + { + "data": { + "currentUser": { + "activeOrganization": { + "id": "1", + "creatorUser": { + "__typename": "User", + "id": 2 + } + } + } + } + } + "###); + + let context = Context::new(); + context.insert_json_value("sub", "5678".into()); + let request = supergraph::Request::fake_builder() + .query(query) + .context(context) + .header( + HeaderName::from_static(CACHE_DEBUG_HEADER_NAME), + HeaderValue::from_static("true"), + ) + .build() + .unwrap(); + let mut response = service.ready().await.unwrap().call(request).await.unwrap(); + assert!( + response + .response + .headers() + .get(CACHE_CONTROL) + .unwrap() + .to_str() + .unwrap() + .contains("private") + ); + let mut cache_keys: CacheKeysContext = response + .context + .get(CONTEXT_DEBUG_CACHE_KEYS) + .unwrap() + .unwrap(); + cache_keys.iter_mut().for_each(|ck| { + ck.invalidation_keys.sort(); + ck.cache_control.created = 0; + }); + cache_keys.sort_by(|a, b| a.invalidation_keys.cmp(&b.invalidation_keys)); + insta::assert_json_snapshot!(cache_keys); + + let mut response = response.next_response().await.unwrap(); + assert!( + response + .extensions + .remove(CACHE_DEBUG_EXTENSIONS_KEY) + .is_some() + ); + + insta::assert_json_snapshot!(response, @r###" + { + "data": { + "currentUser": { + "activeOrganization": { + "id": "1", + "creatorUser": { + "__typename": "User", + "id": 2 + } + } + } + } + } + "###); +} + +// In this test we want to make sure when we have 2 root fields with both public and private data it still returns private +#[tokio::test] +async fn private_and_public() { + let query = "query { currentUser { activeOrganization { id creatorUser { __typename id } } } orga(id: \"2\") { name } }"; + let valid_schema = Arc::new(Schema::parse_and_validate(SCHEMA, "test.graphql").unwrap()); + + let subgraphs = serde_json::json!({ + "user": { + "query": { + "currentUser": { + "activeOrganization": { + "__typename": "Organization", + "id": "1", + } + } + }, + "headers": {"cache-control": "public"}, + }, + "orga": { + "query": { + "orga": { + "__typename": "Organization", + "id": "2", + "name": "test_orga" + } + }, + "entities": [ + { + "__typename": "Organization", + "id": "1", + "creatorUser": { + "__typename": "User", + "id": 2 + } + } + ], + "headers": {"cache-control": "private"}, + }, + }); + + let pg_cache = PostgresCacheStorage::new(&PostgresCacheConfig { + cleanup_interval: default_cleanup_interval(), + tls: Default::default(), + url: "postgres://127.0.0.1".parse().unwrap(), + username: None, + password: None, + idle_timeout: std::time::Duration::from_secs(5), + acquire_timeout: std::time::Duration::from_millis(50), + required_to_start: true, + pool_size: default_pool_size(), + batch_size: default_batch_size(), + namespace: Some(String::from("private_and_public")), + }) + .await + .unwrap(); + let map = [ + ( + "user".to_string(), + Subgraph { + postgres: None, + private_id: Some("sub".to_string()), + enabled: true.into(), + ttl: None, + ..Default::default() + }, + ), + ( + "orga".to_string(), + Subgraph { + postgres: None, + private_id: Some("sub".to_string()), + enabled: true.into(), + ttl: None, + ..Default::default() + }, + ), + ] + .into_iter() + .collect(); + let response_cache = + ResponseCache::for_test(pg_cache.clone(), map, valid_schema.clone(), true, false) + .await + .unwrap(); + + let mut service = TestHarness::builder() + .configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true }, "experimental_mock_subgraphs": subgraphs.clone() })) + .unwrap() + .schema(SCHEMA) + .extra_private_plugin(response_cache.clone()) + .build_supergraph() + .await + .unwrap(); + + let context = Context::new(); + context.insert_json_value("sub", "1234".into()); + let request = supergraph::Request::fake_builder() .query(query) .context(context) @@ -1485,6 +1696,76 @@ async fn private() { cache_keys.sort_by(|a, b| a.invalidation_keys.cmp(&b.invalidation_keys)); insta::assert_json_snapshot!(cache_keys); + let mut response = response.next_response().await.unwrap(); + assert!( + response + .extensions + .remove(CACHE_DEBUG_EXTENSIONS_KEY) + .is_some() + ); + insta::assert_json_snapshot!(response, @r###" + { + "data": { + "currentUser": { + "activeOrganization": { + "id": "1", + "creatorUser": { + "__typename": "User", + "id": 2 + } + } + }, + "orga": { + "name": "test_orga" + } + } + } + "###); + // Now testing without any mock subgraphs, all the data should come from the cache + let mut service = TestHarness::builder() + .configuration_json(serde_json::json!({"include_subgraph_errors": { "all": true }, "experimental_mock_subgraphs": subgraphs.clone() })) + .unwrap() + .schema(SCHEMA) + .extra_private_plugin(response_cache.clone()) + .build_supergraph() + .await + .unwrap(); + + let context = Context::new(); + context.insert_json_value("sub", "1234".into()); + + let request = supergraph::Request::fake_builder() + .query(query) + .context(context) + .header( + HeaderName::from_static(CACHE_DEBUG_HEADER_NAME), + HeaderValue::from_static("true"), + ) + .build() + .unwrap(); + let mut response = service.ready().await.unwrap().call(request).await.unwrap(); + assert!( + response + .response + .headers() + .get(CACHE_CONTROL) + .unwrap() + .to_str() + .unwrap() + .contains("private") + ); + let mut cache_keys: CacheKeysContext = response + .context + .get(CONTEXT_DEBUG_CACHE_KEYS) + .unwrap() + .unwrap(); + cache_keys.iter_mut().for_each(|ck| { + ck.invalidation_keys.sort(); + ck.cache_control.created = 0; + }); + cache_keys.sort_by(|a, b| a.invalidation_keys.cmp(&b.invalidation_keys)); + insta::assert_json_snapshot!(cache_keys); + let mut response = response.next_response().await.unwrap(); assert!( response @@ -1504,6 +1785,9 @@ async fn private() { "id": 2 } } + }, + "orga": { + "name": "test_orga" } } } @@ -1521,6 +1805,16 @@ async fn private() { .build() .unwrap(); let mut response = service.ready().await.unwrap().call(request).await.unwrap(); + assert!( + response + .response + .headers() + .get(CACHE_CONTROL) + .unwrap() + .to_str() + .unwrap() + .contains("private") + ); let mut cache_keys: CacheKeysContext = response .context .get(CONTEXT_DEBUG_CACHE_KEYS) @@ -1552,6 +1846,9 @@ async fn private() { "id": 2 } } + }, + "orga": { + "name": "test_orga" } } } @@ -1653,6 +1950,16 @@ async fn private_without_private_id() { .build() .unwrap(); let mut response = service.ready().await.unwrap().call(request).await.unwrap(); + assert!( + response + .response + .headers() + .get(CACHE_CONTROL) + .unwrap() + .to_str() + .unwrap() + .contains("private") + ); let mut cache_keys: CacheKeysContext = response .context .get(CONTEXT_DEBUG_CACHE_KEYS) @@ -1709,6 +2016,16 @@ async fn private_without_private_id() { .build() .unwrap(); let mut response = service.ready().await.unwrap().call(request).await.unwrap(); + assert!( + response + .response + .headers() + .get(CACHE_CONTROL) + .unwrap() + .to_str() + .unwrap() + .contains("private") + ); let mut cache_keys: CacheKeysContext = response .context .get(CONTEXT_DEBUG_CACHE_KEYS)