Skip to content

Commit

Permalink
feat(federation): federation v2.6 support (#1928)
Browse files Browse the repository at this point in the history
### 📝 Description

Adds Federation v2.6 support

```graphql
directive @Policy(policies: [[federation__Policy!]!]!) on
  | FIELD_DEFINITION
  | OBJECT
  | INTERFACE
  | SCALAR
  | ENUM

scalar federation__Policy
```

`@policy` directive indicates to composition that the target element is
restricted based on authorization policies that are evaluated in a Rhai
script or coprocessor. Refer to the [Apollo Router
article](https://www.apollographql.com/docs/router/configuration/authorization#policy)
for additional details.

### 🔗 Related Issues

N/A
  • Loading branch information
dariuszkuc committed Feb 22, 2024
1 parent 0aea13e commit 1172da1
Show file tree
Hide file tree
Showing 22 changed files with 387 additions and 27 deletions.
2 changes: 1 addition & 1 deletion examples/federation/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
services:
router:
image: ghcr.io/apollographql/router:v1.29.1
image: ghcr.io/apollographql/router:v1.40.0
volumes:
- ./router.yaml:/dist/config/router.yaml
- ./supergraph.graphql:/dist/config/supergraph.graphql
Expand Down
2 changes: 1 addition & 1 deletion examples/federation/supergraph.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
federation_version: =2.5.4
federation_version: =2.6.3
subgraphs:
products:
routing_url: http://products:8080/graphql
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023 Expedia, Inc
* Copyright 2024 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -43,6 +43,7 @@ import com.expediagroup.graphql.generator.federation.directives.LinkDirective
import com.expediagroup.graphql.generator.federation.directives.LinkImport
import com.expediagroup.graphql.generator.federation.directives.LinkedSpec
import com.expediagroup.graphql.generator.federation.directives.OVERRIDE_DIRECTIVE_NAME
import com.expediagroup.graphql.generator.federation.directives.POLICY_DIRECTIVE_NAME
import com.expediagroup.graphql.generator.federation.directives.PROVIDES_DIRECTIVE_NAME
import com.expediagroup.graphql.generator.federation.directives.PROVIDES_DIRECTIVE_TYPE
import com.expediagroup.graphql.generator.federation.directives.REQUIRES_DIRECTIVE_NAME
Expand All @@ -52,10 +53,12 @@ import com.expediagroup.graphql.generator.federation.directives.SHAREABLE_DIRECT
import com.expediagroup.graphql.generator.federation.directives.TAG_DIRECTIVE_NAME
import com.expediagroup.graphql.generator.federation.directives.keyDirectiveDefinition
import com.expediagroup.graphql.generator.federation.directives.linkDirectiveDefinition
import com.expediagroup.graphql.generator.federation.directives.policyDirectiveDefinition
import com.expediagroup.graphql.generator.federation.directives.providesDirectiveDefinition
import com.expediagroup.graphql.generator.federation.directives.requiresDirectiveDefinition
import com.expediagroup.graphql.generator.federation.directives.requiresScopesDirectiveType
import com.expediagroup.graphql.generator.federation.directives.toAppliedLinkDirective
import com.expediagroup.graphql.generator.federation.directives.toAppliedPolicyDirective
import com.expediagroup.graphql.generator.federation.directives.toAppliedRequiresScopesDirective
import com.expediagroup.graphql.generator.federation.exception.DuplicateSpecificationLinkImport
import com.expediagroup.graphql.generator.federation.exception.IncorrectFederatedDirectiveUsage
Expand All @@ -69,6 +72,7 @@ import com.expediagroup.graphql.generator.federation.types.FIELD_SET_SCALAR_NAME
import com.expediagroup.graphql.generator.federation.types.FIELD_SET_SCALAR_TYPE
import com.expediagroup.graphql.generator.federation.types.FieldSetTransformer
import com.expediagroup.graphql.generator.federation.types.LINK_IMPORT_SCALAR_TYPE
import com.expediagroup.graphql.generator.federation.types.POLICY_SCALAR_TYPE
import com.expediagroup.graphql.generator.federation.types.SCOPE_SCALAR_TYPE
import com.expediagroup.graphql.generator.federation.types.SERVICE_FIELD_DEFINITION
import com.expediagroup.graphql.generator.federation.types._Service
Expand Down Expand Up @@ -149,6 +153,18 @@ open class FederatedSchemaGeneratorHooks(
}
}
}
private val policiesScalar: GraphQLScalarType by lazy {
POLICY_SCALAR_TYPE.run {
val policyScalarName = namespacedTypeName(FEDERATION_SPEC, this.name)
if (policyScalarName != this.name) {
this.transform {
it.name(policyScalarName)
}
} else {
this
}
}
}
private val scopesScalar: GraphQLScalarType by lazy {
SCOPE_SCALAR_TYPE.run {
val scopesScalarName = namespacedTypeName(FEDERATION_SPEC, this.name)
Expand Down Expand Up @@ -252,17 +268,24 @@ open class FederatedSchemaGeneratorHooks(
EXTERNAL_DIRECTIVE_NAME -> EXTERNAL_DIRECTIVE_TYPE_V2
KEY_DIRECTIVE_NAME -> keyDirectiveDefinition(fieldSetScalar)
LINK_DIRECTIVE_NAME -> linkDirectiveDefinition(linkImportScalar)
POLICY_DIRECTIVE_NAME -> policyDirectiveDefinition(policiesScalar)
PROVIDES_DIRECTIVE_NAME -> providesDirectiveDefinition(fieldSetScalar)
REQUIRES_DIRECTIVE_NAME -> requiresDirectiveDefinition(fieldSetScalar)
REQUIRES_SCOPE_DIRECTIVE_NAME -> requiresScopesDirectiveType(scopesScalar)
else -> super.willGenerateDirective(directiveInfo)
}

override fun willApplyDirective(directiveInfo: DirectiveMetaInformation, directive: GraphQLDirective): GraphQLAppliedDirective? {
return if (directiveInfo.effectiveName == REQUIRES_SCOPE_DIRECTIVE_NAME) {
directive.toAppliedRequiresScopesDirective(directiveInfo)
} else {
super.willApplyDirective(directiveInfo, directive)
return when (directiveInfo.effectiveName) {
REQUIRES_SCOPE_DIRECTIVE_NAME -> {
directive.toAppliedRequiresScopesDirective(directiveInfo)
}
POLICY_DIRECTIVE_NAME -> {
directive.toAppliedPolicyDirective(directiveInfo)
}
else -> {
super.willApplyDirective(directiveInfo, directive)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023 Expedia, Inc
* Copyright 2024 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -32,7 +32,7 @@ const val LINK_SPEC_URL_PREFIX = "$APOLLO_SPEC_URL/$LINK_SPEC"
const val LINK_SPEC_LATEST_URL = "$LINK_SPEC_URL_PREFIX/v$LINK_SPEC_LATEST_VERSION"

const val FEDERATION_SPEC = "federation"
const val FEDERATION_SPEC_LATEST_VERSION = "2.5"
const val FEDERATION_SPEC_LATEST_VERSION = "2.6"
const val FEDERATION_SPEC_URL_PREFIX = "$APOLLO_SPEC_URL/$FEDERATION_SPEC"
const val FEDERATION_SPEC_LATEST_URL = "$FEDERATION_SPEC_URL_PREFIX/v$FEDERATION_SPEC_LATEST_VERSION"

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2024 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.expediagroup.graphql.generator.federation.directives

import com.expediagroup.graphql.generator.annotations.GraphQLIgnore

/**
* Annotation representing authorization policy scalar type that is used by the `@policy directive.
*
* @param value required authorization policy
* @see [com.expediagroup.graphql.generator.federation.types.POLICY_SCALAR_TYPE]
*/
@LinkedSpec(FEDERATION_SPEC)
annotation class Policy(val value: String)

// this is a workaround for JVM lack of support nested arrays as annotation values
@GraphQLIgnore
annotation class Policies(val value: Array<Policy>)
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* Copyright 2024 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.expediagroup.graphql.generator.federation.directives

import com.expediagroup.graphql.generator.annotations.GraphQLDirective
import com.expediagroup.graphql.generator.directives.DirectiveMetaInformation
import graphql.introspection.Introspection
import graphql.schema.GraphQLAppliedDirective
import graphql.schema.GraphQLArgument
import graphql.schema.GraphQLList
import graphql.schema.GraphQLNonNull
import graphql.schema.GraphQLScalarType
import kotlin.reflect.full.memberProperties

/**
* ```graphql
* directive @policy(scopes: [[Policy!]!]!) on
* ENUM
* | FIELD_DEFINITION
* | INTERFACE
* | OBJECT
* | SCALAR
* ```
*
*
* Directive that is used to indicate that the target element is restricted based on authorization policies that are evaluated in a Rhai script or coprocessor.
* Refer to the <a href="https://www.apollographql.com/docs/router/configuration/authorization#policy">Apollo Router article</a> for additional details.
*
* @see <a href="https://www.apollographql.com/docs/federation/federated-types/federated-directives#policy">@policy definition</a>
* @see <a href="https://www.apollographql.com/docs/router/configuration/authorization#policy">Apollo Router @policy documentation</a>
*/
@LinkedSpec(FEDERATION_SPEC)
@Repeatable
@GraphQLDirective(
name = POLICY_DIRECTIVE_NAME,
description = POLICY_DIRECTIVE_DESCRIPTION,
locations = [
Introspection.DirectiveLocation.ENUM,
Introspection.DirectiveLocation.FIELD_DEFINITION,
Introspection.DirectiveLocation.INTERFACE,
Introspection.DirectiveLocation.OBJECT,
Introspection.DirectiveLocation.SCALAR,
]
)
annotation class PolicyDirective(val policies: Array<Policies>)

internal const val POLICY_DIRECTIVE_NAME = "policy"
private const val POLICY_DIRECTIVE_DESCRIPTION = "Indicates to composition that the target element is restricted based on authorization policies that are evaluated in a Rhai script or coprocessor"
private const val POLICIES_ARGUMENT = "policies"

internal fun policyDirectiveDefinition(policies: GraphQLScalarType): graphql.schema.GraphQLDirective = graphql.schema.GraphQLDirective.newDirective()
.name(POLICY_DIRECTIVE_NAME)
.description(POLICY_DIRECTIVE_DESCRIPTION)
.validLocations(
Introspection.DirectiveLocation.ENUM,
Introspection.DirectiveLocation.FIELD_DEFINITION,
Introspection.DirectiveLocation.INTERFACE,
Introspection.DirectiveLocation.OBJECT,
Introspection.DirectiveLocation.SCALAR
)
.argument(
GraphQLArgument.newArgument()
.name(POLICIES_ARGUMENT)
.type(
GraphQLNonNull.nonNull(
GraphQLList.list(
GraphQLNonNull(
GraphQLList.list(
policies
)
)
)
)
)
)
.build()

@Suppress("UNCHECKED_CAST")
internal fun graphql.schema.GraphQLDirective.toAppliedPolicyDirective(directiveInfo: DirectiveMetaInformation): GraphQLAppliedDirective {
// we need to manually transform @policy directive definition as JVM does not support nested array as annotation arguments
val annotationPolicies = directiveInfo.directive.annotationClass.memberProperties
.first { it.name == POLICIES_ARGUMENT }
.call(directiveInfo.directive) as? Array<Policies> ?: emptyArray()
val policies = annotationPolicies.map { policiesAnnotation -> policiesAnnotation.value.toList() }

return this.toAppliedDirective()
.transform { appliedDirectiveBuilder ->
this.getArgument(POLICIES_ARGUMENT)
.toAppliedArgument()
.transform { argumentBuilder ->
argumentBuilder.valueProgrammatic(policies)
}
.let {
appliedDirectiveBuilder.argument(it)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023 Expedia, Inc
* Copyright 2024 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -73,7 +73,7 @@ internal fun requiresScopesDirectiveType(scopes: GraphQLScalarType): graphql.sch
)
.argument(
GraphQLArgument.newArgument()
.name("scopes")
.name(SCOPES_ARGUMENT)
.type(
GraphQLNonNull.nonNull(
GraphQLList.list(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright 2024 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.expediagroup.graphql.generator.federation.types

import com.expediagroup.graphql.generator.federation.directives.Policy
import com.expediagroup.graphql.generator.federation.exception.CoercingValueToLiteralException
import graphql.GraphQLContext
import graphql.Scalars
import graphql.execution.CoercedVariables
import graphql.language.StringValue
import graphql.language.Value
import graphql.schema.Coercing
import graphql.schema.CoercingParseLiteralException
import graphql.schema.CoercingSerializeException
import graphql.schema.GraphQLScalarType
import java.util.Locale

internal const val POLICY_SCALAR_NAME = "Policy"

/**
* Custom scalar type that is used to represent authentication policy which serializes as a String.
*/
internal val POLICY_SCALAR_TYPE: GraphQLScalarType = GraphQLScalarType.newScalar(Scalars.GraphQLString)
.name(POLICY_SCALAR_NAME)
.description("Federation type representing authorization policy")
.coercing(PolicyCoercing)
.build()

private object PolicyCoercing : Coercing<Policy, String> {
override fun serialize(dataFetcherResult: Any, graphQLContext: GraphQLContext, locale: Locale): String =
when (dataFetcherResult) {
is Policy -> dataFetcherResult.value
else -> throw CoercingSerializeException(
"Cannot serialize $dataFetcherResult. Expected type 'Policy' but was '${dataFetcherResult.javaClass.simpleName}'."
)
}

override fun parseValue(input: Any, graphQLContext: GraphQLContext, locale: Locale): Policy =
when (input) {
is Policy -> input
is StringValue -> Policy::class.constructors.first().call(input.value)
else -> throw CoercingParseLiteralException(
"Cannot parse $input to Policy. Expected AST type 'StringValue' but was '${input.javaClass.simpleName}'."
)
}

override fun parseLiteral(input: Value<*>, variables: CoercedVariables, graphQLContext: GraphQLContext, locale: Locale): Policy =
when (input) {
is StringValue -> Policy::class.constructors.first().call(input.value)
else -> throw CoercingParseLiteralException(
"Cannot parse $input to Policy. Expected AST type 'StringValue' but was '${input.javaClass.simpleName}'."
)
}

override fun valueToLiteral(input: Any, graphQLContext: GraphQLContext, locale: Locale): Value<*> =
when (input) {
is Policy -> StringValue.newStringValue(input.value).build()
else -> throw CoercingValueToLiteralException(Policy::class, input)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class FederatedSchemaV2GeneratorTest {
fun `verify can generate federated schema`() {
val expectedSchema =
"""
schema @link(import : ["@external", "@key", "@provides", "@requires", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.5"){
schema @link(import : ["@external", "@key", "@provides", "@requires", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.6"){
query: Query
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023 Expedia, Inc
* Copyright 2024 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -43,7 +43,7 @@ class ComposeDirectiveTest {
fun `verify we can generate valid schema with @composeDirective`() {
val expectedSchema =
"""
schema @composeDirective(name : "custom") @link(as : "myspec", import : ["@custom"], url : "https://www.myspecs.dev/myspec/v1.0") @link(import : ["@composeDirective", "@key", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.5"){
schema @composeDirective(name : "custom") @link(as : "myspec", import : ["@custom"], url : "https://www.myspecs.dev/myspec/v1.0") @link(import : ["@composeDirective", "@key", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.6"){
query: Query
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class ContactDirectiveTest {
fun `verify we can import federation spec using custom @link`() {
val expectedSchema =
"""
schema @contact(description : "Send emails to foo@myteamname.com", name : "My Team Name", url : "https://myteam.slack.com/room") @link(url : "https://specs.apollo.dev/federation/v2.5"){
schema @contact(description : "Send emails to foo@myteamname.com", name : "My Team Name", url : "https://myteam.slack.com/room") @link(url : "https://specs.apollo.dev/federation/v2.6"){
query: Query
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class LinkDirectiveTest {
fun `verify we can import federation spec using custom @link`() {
val expectedSchema =
"""
schema @link(as : "fed", import : [{name : "@key", as : "@myKey"}], url : "https://specs.apollo.dev/federation/v2.5"){
schema @link(as : "fed", import : [{name : "@key", as : "@myKey"}], url : "https://specs.apollo.dev/federation/v2.6"){
query: Query
}
Expand Down
Loading

0 comments on commit 1172da1

Please sign in to comment.