diff --git a/apollo-router/src/state_machine.rs b/apollo-router/src/state_machine.rs index 9093e52e93..138a233ab3 100644 --- a/apollo-router/src/state_machine.rs +++ b/apollo-router/src/state_machine.rs @@ -334,7 +334,7 @@ impl State { .map_err(|e| ServiceCreationError(e.to_string().into()))?, ); // Check the license - let report = LicenseEnforcementReport::build(&configuration, &schema); + let report = LicenseEnforcementReport::build(&configuration, &schema, &license); let license_limits = match license { LicenseState::Licensed { ref limits } => { diff --git a/apollo-router/src/uplink/license_enforcement.rs b/apollo-router/src/uplink/license_enforcement.rs index b66f1f08be..c466a0bd2e 100644 --- a/apollo-router/src/uplink/license_enforcement.rs +++ b/apollo-router/src/uplink/license_enforcement.rs @@ -110,13 +110,17 @@ impl LicenseEnforcementReport { pub(crate) fn build( configuration: &Configuration, schema: &Schema, + license: &LicenseState, ) -> LicenseEnforcementReport { LicenseEnforcementReport { restricted_config_in_use: Self::validate_configuration( configuration, - &Self::configuration_restrictions(), + &Self::configuration_restrictions(license), + ), + restricted_schema_in_use: Self::validate_schema( + schema, + &Self::schema_restrictions(license), ), - restricted_schema_in_use: Self::validate_schema(schema, &Self::schema_restrictions()), } } @@ -282,21 +286,13 @@ impl LicenseEnforcementReport { schema_violations } - fn configuration_restrictions() -> Vec { - vec![ + fn configuration_restrictions(license: &LicenseState) -> Vec { + let mut configuration_restrictions = vec![ ConfigurationRestriction::builder() .path("$.plugins.['experimental.restricted'].enabled") .value(true) .name("Restricted") .build(), - ConfigurationRestriction::builder() - .path("$.authentication.router") - .name("Authentication plugin") - .build(), - ConfigurationRestriction::builder() - .path("$.authorization.directives") - .name("Authorization directives") - .build(), ConfigurationRestriction::builder() .path("$.coprocessor") .name("Coprocessor plugin") @@ -305,20 +301,6 @@ impl LicenseEnforcementReport { .path("$.supergraph.query_planning.cache.redis") .name("Query plan caching") .build(), - ConfigurationRestriction::builder() - .path("$.apq.router.cache.redis") - .name("APQ caching") - .build(), - ConfigurationRestriction::builder() - .path("$.preview_entity_cache.enabled") - .value(true) - .name("Subgraph entity caching") - .build(), - ConfigurationRestriction::builder() - .path("$.subscription.enabled") - .value(true) - .name("Federated subscriptions") - .build(), // Per-operation limits are restricted but parser limits like `parser_max_recursion` // where the Router only configures apollo-rs are not. ConfigurationRestriction::builder() @@ -337,10 +319,6 @@ impl LicenseEnforcementReport { .path("$.limits.max_aliases") .name("Operation aliases limiting") .build(), - ConfigurationRestriction::builder() - .path("$.persisted_queries") - .name("Persisted queries") - .build(), ConfigurationRestriction::builder() .path("$.telemetry..spans.router") .name("Advanced telemetry") @@ -365,28 +343,141 @@ impl LicenseEnforcementReport { .path("$.telemetry..graphql") .name("Advanced telemetry") .build(), - ConfigurationRestriction::builder() - .path("$.preview_file_uploads") - .name("File uploads plugin") - .build(), - ConfigurationRestriction::builder() - .path("$.batching") - .name("Batching support") - .build(), - ConfigurationRestriction::builder() - .path("$.demand_control") - .name("Demand control plugin") - .build(), ConfigurationRestriction::builder() .path("$.telemetry.apollo.metrics_reference_mode") .value("extended") .name("Apollo metrics extended references") .build(), - ] + ]; + + // If the license has an allowed_features claim, we know we're using a pricing + // plan with a subset of allowed features + // Check if the following features are in the licenses' allowed_features claim + if let Some(allowed_features) = license.get_allowed_features() { + if !allowed_features.contains(&AllowedFeature::APQ) { + configuration_restrictions.push( + ConfigurationRestriction::builder() + .path("$.apq.router.cache.redis") + .name("APQ caching") + .build(), + ) + } + if !allowed_features.contains(&AllowedFeature::Authentication) { + configuration_restrictions.push( + ConfigurationRestriction::builder() + .path("$.authentication.router") + .name("Authentication plugin") + .build(), + ); + } + if !allowed_features.contains(&AllowedFeature::Authorization) { + configuration_restrictions.push( + ConfigurationRestriction::builder() + .path("$.authorization.directives") + .name("Authorization directives") + .build(), + ); + } + if !allowed_features.contains(&AllowedFeature::Batching) { + configuration_restrictions.push( + ConfigurationRestriction::builder() + .path("$.batching") + .name("Batching support") + .build(), + ); + } + if !allowed_features.contains(&AllowedFeature::DemandControl) { + configuration_restrictions.push( + ConfigurationRestriction::builder() + .path("$.demand_control") + .name("Demand control plugin") + .build(), + ); + } + if !allowed_features.contains(&AllowedFeature::EntityCaching) { + configuration_restrictions.push( + ConfigurationRestriction::builder() + .path("$.preview_entity_cache.enabled") + .value(true) + .name("Subgraph entity caching") + .build(), + ); + } + if !allowed_features.contains(&AllowedFeature::FileUploads) { + configuration_restrictions.push( + ConfigurationRestriction::builder() + .path("$.preview_file_uploads") + .name("File uploads plugin") + .build(), + ); + } + if !allowed_features.contains(&AllowedFeature::PersistedQueries) { + configuration_restrictions.push( + ConfigurationRestriction::builder() + .path("$.persisted_queries") + .name("Persisted queries") + .build(), + ); + } + if !allowed_features.contains(&AllowedFeature::Subscriptions) { + configuration_restrictions.push( + ConfigurationRestriction::builder() + .path("$.subscription.enabled") + .value(true) + .name("Federated subscriptions") + .build(), + ); + } + // If the license has no allowed_features claim, we're using a pricing plan + // that should have the plugin enabled regardless + } else { + configuration_restrictions.extend(vec![ + ConfigurationRestriction::builder() + .path("$.apq.router.cache.redis") + .name("APQ caching") + .build(), + ConfigurationRestriction::builder() + .path("$.authentication.router") + .name("Authentication plugin") + .build(), + ConfigurationRestriction::builder() + .path("$.authorization.directives") + .name("Authorization directives") + .build(), + ConfigurationRestriction::builder() + .path("$.batching") + .name("Batching support") + .build(), + ConfigurationRestriction::builder() + .path("$.demand_control") + .name("Demand control plugin") + .build(), + ConfigurationRestriction::builder() + .path("$.preview_entity_cache.enabled") + .value(true) + .name("Subgraph entity caching") + .build(), + ConfigurationRestriction::builder() + .path("$.preview_file_uploads") + .name("File uploads plugin") + .build(), + ConfigurationRestriction::builder() + .path("$.persisted_queries") + .name("Persisted queries") + .build(), + ConfigurationRestriction::builder() + .path("$.subscription.enabled") + .value(true) + .name("Federated subscriptions") + .build(), + ]); + } + + configuration_restrictions } - fn schema_restrictions() -> Vec { - vec![ + fn schema_restrictions(license: &LicenseState) -> Vec { + let mut schema_restrictions = vec![ SchemaRestriction::Spec { name: "authenticated".to_string(), spec_url: "https://specs.apollo.dev/authenticated".to_string(), @@ -400,13 +491,6 @@ impl LicenseEnforcementReport { }], }, }, - SchemaRestriction::SpecInJoinDirective { - name: "connect".to_string(), - spec_url: "https://specs.apollo.dev/connect".to_string(), - version_req: semver::VersionReq { - comparators: vec![], // all versions - }, - }, SchemaRestriction::Spec { name: "context".to_string(), spec_url: "https://specs.apollo.dev/context".to_string(), @@ -463,7 +547,34 @@ impl LicenseEnforcementReport { }, explanation: "The `contextArguments` argument on the join spec's @field directive is restricted to Enterprise users. This argument exists in your supergraph as a result of using the `@fromContext` directive in one or more of your subgraphs.".to_string() }, - ] + ]; + + // If the license has an allowed_features claim, we know we're using a pricing + // plan with a subset of allowed features + // Check if the following features are in the licenses' allowed_features claim + if let Some(allowed_features) = license.get_allowed_features() { + if !allowed_features.contains(&AllowedFeature::RestConnectors) { + schema_restrictions.push(SchemaRestriction::SpecInJoinDirective { + name: "connect".to_string(), + spec_url: "https://specs.apollo.dev/connect".to_string(), + version_req: semver::VersionReq { + comparators: vec![], // all versions + }, + }) + } + // If the license has no allowed_features claim, we're using a pricing plan + // that should have the plugin enabled regardless + } else { + schema_restrictions.push(SchemaRestriction::SpecInJoinDirective { + name: "connect".to_string(), + spec_url: "https://specs.apollo.dev/connect".to_string(), + version_req: semver::VersionReq { + comparators: vec![], // all versions + }, + }) + } + + schema_restrictions } } @@ -767,6 +878,7 @@ impl License { #[cfg(test)] mod test { + use std::collections::HashSet; use std::str::FromStr; use std::time::Duration; use std::time::UNIX_EPOCH; @@ -774,21 +886,29 @@ mod test { use insta::assert_snapshot; use serde_json::json; + use crate::AllowedFeature; use crate::Configuration; use crate::spec::Schema; use crate::uplink::license_enforcement::Audience; use crate::uplink::license_enforcement::Claims; use crate::uplink::license_enforcement::License; use crate::uplink::license_enforcement::LicenseEnforcementReport; + use crate::uplink::license_enforcement::LicenseLimits; + use crate::uplink::license_enforcement::LicenseState; use crate::uplink::license_enforcement::OneOrMany; use crate::uplink::license_enforcement::SchemaViolation; #[track_caller] - fn check(router_yaml: &str, supergraph_schema: &str) -> LicenseEnforcementReport { + fn check( + router_yaml: &str, + supergraph_schema: &str, + license: LicenseState, + ) -> LicenseEnforcementReport { let config = Configuration::from_str(router_yaml).expect("router config must be valid"); let schema = Schema::parse(supergraph_schema, &config).expect("supergraph schema must be valid"); - LicenseEnforcementReport::build(&config, &schema) + + LicenseEnforcementReport::build(&config, &schema, &license) } #[test] @@ -796,6 +916,7 @@ mod test { let report = check( include_str!("testdata/oss.router.yaml"), include_str!("testdata/oss.graphql"), + LicenseState::default(), ); assert!( @@ -809,6 +930,40 @@ mod test { let report = check( include_str!("testdata/restricted.router.yaml"), include_str!("testdata/oss.graphql"), + LicenseState::default(), + ); + + assert!( + !report.restricted_config_in_use.is_empty(), + "should have found restricted features" + ); + assert_snapshot!(report.to_string()); + } + + #[test] + fn test_restricted_features_via_config_with_subset_of_allowed_features_not_containing_subscriptions() + { + // This config includes subscriptions but the license's allowed_features claim + // does not include subscriptions + let report = check( + include_str!("testdata/restricted.router.yaml"), + include_str!("testdata/oss.graphql"), + LicenseState::Licensed { + limits: Some(LicenseLimits { + tps: None, + allowed_features: Some(HashSet::from_iter(vec![ + AllowedFeature::APQ, + AllowedFeature::Authentication, + AllowedFeature::Authorization, + AllowedFeature::Batching, + AllowedFeature::DemandControl, + AllowedFeature::EntityCaching, + AllowedFeature::FileUploads, + AllowedFeature::PersistedQueries, + AllowedFeature::FileUploads, + ])), + }), + }, ); assert!( @@ -823,6 +978,7 @@ mod test { let report = check( include_str!("testdata/oss.router.yaml"), include_str!("testdata/authorization.graphql"), + LicenseState::default(), ); assert!( @@ -838,6 +994,7 @@ mod test { let report = check( include_str!("testdata/oss.router.yaml"), include_str!("testdata/unix_socket.graphql"), + LicenseState::default(), ); assert!( @@ -919,6 +1076,7 @@ mod test { let report = check( include_str!("testdata/oss.router.yaml"), include_str!("testdata/progressive_override.graphql"), + LicenseState::default(), ); assert!( @@ -933,6 +1091,7 @@ mod test { let report = check( include_str!("testdata/oss.router.yaml"), include_str!("testdata/set_context.graphql"), + LicenseState::default(), ); assert!( @@ -947,6 +1106,7 @@ mod test { let report = check( include_str!("testdata/oss.router.yaml"), include_str!("testdata/progressive_override_renamed_join.graphql"), + LicenseState::default(), ); assert!( @@ -961,6 +1121,7 @@ mod test { let report = check( include_str!("testdata/oss.router.yaml"), include_str!("testdata/schema_enforcement_spec_version_in_range.graphql"), + LicenseState::default(), ); assert!( @@ -975,6 +1136,7 @@ mod test { let report = check( include_str!("testdata/oss.router.yaml"), include_str!("testdata/schema_enforcement_spec_version_out_of_range.graphql"), + LicenseState::default(), ); assert!( @@ -988,6 +1150,7 @@ mod test { let report = check( include_str!("testdata/oss.router.yaml"), include_str!("testdata/schema_enforcement_directive_arg_version_in_range.graphql"), + LicenseState::default(), ); assert!( @@ -1002,6 +1165,7 @@ mod test { let report = check( include_str!("testdata/oss.router.yaml"), include_str!("testdata/schema_enforcement_directive_arg_version_out_of_range.graphql"), + LicenseState::default(), ); assert!( @@ -1015,6 +1179,7 @@ mod test { let report = check( include_str!("testdata/oss.router.yaml"), include_str!("testdata/schema_enforcement_connectors.graphql"), + LicenseState::default(), ); assert_eq!( diff --git a/apollo-router/src/uplink/snapshots/apollo_router__uplink__license_enforcement__test__restricted_features_via_config.snap b/apollo-router/src/uplink/snapshots/apollo_router__uplink__license_enforcement__test__restricted_features_via_config.snap index cf517de1b5..6307bee18e 100644 --- a/apollo-router/src/uplink/snapshots/apollo_router__uplink__license_enforcement__test__restricted_features_via_config.snap +++ b/apollo-router/src/uplink/snapshots/apollo_router__uplink__license_enforcement__test__restricted_features_via_config.snap @@ -6,24 +6,12 @@ Configuration yaml: * Restricted .plugins.['experimental.restricted'].enabled -* Authentication plugin - .authentication.router - * Coprocessor plugin .coprocessor * Query plan caching .supergraph.query_planning.cache.redis -* APQ caching - .apq.router.cache.redis - -* Subgraph entity caching - .preview_entity_cache.enabled - -* Federated subscriptions - .subscription.enabled - * Operation depth limiting .limits.max_depth @@ -51,11 +39,23 @@ Configuration yaml: * Advanced telemetry .telemetry..graphql -* File uploads plugin - .preview_file_uploads +* Apollo metrics extended references + .telemetry.apollo.metrics_reference_mode + +* APQ caching + .apq.router.cache.redis + +* Authentication plugin + .authentication.router * Demand control plugin .demand_control -* Apollo metrics extended references - .telemetry.apollo.metrics_reference_mode +* Subgraph entity caching + .preview_entity_cache.enabled + +* File uploads plugin + .preview_file_uploads + +* Federated subscriptions + .subscription.enabled diff --git a/apollo-router/src/uplink/snapshots/apollo_router__uplink__license_enforcement__test__restricted_features_via_config_with_subset_of_allowed_features_not_containing_the_feature.snap b/apollo-router/src/uplink/snapshots/apollo_router__uplink__license_enforcement__test__restricted_features_via_config_with_subset_of_allowed_features_not_containing_the_feature.snap new file mode 100644 index 0000000000..021ccbec81 --- /dev/null +++ b/apollo-router/src/uplink/snapshots/apollo_router__uplink__license_enforcement__test__restricted_features_via_config_with_subset_of_allowed_features_not_containing_the_feature.snap @@ -0,0 +1,46 @@ +--- +source: apollo-router/src/uplink/license_enforcement.rs +expression: report.to_string() +--- +Configuration yaml: +* Restricted + .plugins.['experimental.restricted'].enabled + +* Coprocessor plugin + .coprocessor + +* Query plan caching + .supergraph.query_planning.cache.redis + +* Operation depth limiting + .limits.max_depth + +* Operation height limiting + .limits.max_height + +* Operation root fields limiting + .limits.max_root_fields + +* Operation aliases limiting + .limits.max_aliases + +* Advanced telemetry + .telemetry..spans.router + +* Advanced telemetry + .telemetry..spans.supergraph + +* Advanced telemetry + .telemetry..spans.subgraph + +* Advanced telemetry + .telemetry..instruments + +* Advanced telemetry + .telemetry..graphql + +* Apollo metrics extended references + .telemetry.apollo.metrics_reference_mode + +* Federated subscriptions + .subscription.enabled