-
Notifications
You must be signed in to change notification settings - Fork 353
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: support Apollo Federation 2.5 (#1839)
### 📝 Description Adds support for [Apollo Federation v2.5](https://www.apollographql.com/docs/federation/federation-versions#v25). Adds new `willApplyDirective`/`didApplyDirective` hooks that can be used to customize transformation of directive definition to applied directive. JVM does not support nested arrays as annotation arguments so we need to apply custom hooks to generate valid `@requiresScopes` directive. New hooks can also be used to filter out default arguments (#1830). New federation directives - `@authenticated` - indicates that target element is only accessible to the authenticated supergraph users - `@requiresScopes` - indicates that target element is only accessible to the authenticated supergraph users with the appropriate JWT scopes Note: due to the potential conflict on directive names we will no longer auto import federation directives. New directives will be auto-namespaced to the target spec. For backwards compatibility, we will continue auto-importing directives up to Federation version 2.3. ### 🔗 Related Issues #1830
- Loading branch information
1 parent
ff88dad
commit b173ea0
Showing
25 changed files
with
559 additions
and
51 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
54 changes: 54 additions & 0 deletions
54
...kotlin/com/expediagroup/graphql/generator/federation/directives/AuthenticatedDirective.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
/* | ||
* Copyright 2023 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 graphql.introspection.Introspection | ||
|
||
/** | ||
* ```graphql | ||
* directive @authenticated on | ||
* ENUM | ||
* | FIELD_DEFINITION | ||
* | INTERFACE | ||
* | OBJECT | ||
* | SCALAR | ||
* ``` | ||
* | ||
* Directive that is used to indicate that the target element is accessible only to the authenticated supergraph users. For more granular access control, see the @requiresScopes directive usage. | ||
* Refer to the <a href="https://www.apollographql.com/docs/router/configuration/authorization#authenticated">Apollo Router article</a> for additional details. | ||
* | ||
* @see <a href="https://www.apollographql.com/docs/federation/federated-types/federated-directives#authenticated">@authenticated definition</a> | ||
* @see <a href="https://www.apollographql.com/docs/router/configuration/authorization#authenticated">Apollo Router @authenticated documentation</a> | ||
*/ | ||
@LinkedSpec(FEDERATION_SPEC) | ||
@Repeatable | ||
@GraphQLDirective( | ||
name = AUTHENTICATED_DIRECTIVE_NAME, | ||
description = AUTHENTICATED_DIRECTIVE_DESCRIPTION, | ||
locations = [ | ||
Introspection.DirectiveLocation.ENUM, | ||
Introspection.DirectiveLocation.FIELD_DEFINITION, | ||
Introspection.DirectiveLocation.INTERFACE, | ||
Introspection.DirectiveLocation.OBJECT, | ||
Introspection.DirectiveLocation.SCALAR, | ||
] | ||
) | ||
annotation class AuthenticatedDirective | ||
|
||
internal const val AUTHENTICATED_DIRECTIVE_NAME = "requiresScopes" | ||
private const val AUTHENTICATED_DIRECTIVE_DESCRIPTION = "Indicates to composition that the target element is accessible only to the authenticated supergraph users" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
110 changes: 110 additions & 0 deletions
110
...otlin/com/expediagroup/graphql/generator/federation/directives/RequiresScopesDirective.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
/* | ||
* Copyright 2023 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 @requiresScopes(scopes: [[Scope!]!]!) on | ||
* ENUM | ||
* | FIELD_DEFINITION | ||
* | INTERFACE | ||
* | OBJECT | ||
* | SCALAR | ||
* ``` | ||
* | ||
* Directive that is used to indicate that the target element is accessible only to the authenticated supergraph users with the appropriate JWT scopes. Refer to the | ||
* <a href="https://www.apollographql.com/docs/router/configuration/authorization#requiresscopes">Apollo Router article</a> for additional details. | ||
* | ||
* @see <a href="https://www.apollographql.com/docs/federation/federated-types/federated-directives#requiresscopes">@requiresScope definition</a> | ||
* @see <a href="https://www.apollographql.com/docs/router/configuration/authorization#requiresscopes">Apollo Router @requiresScope documentation</a> | ||
*/ | ||
@LinkedSpec(FEDERATION_SPEC) | ||
@Repeatable | ||
@GraphQLDirective( | ||
name = REQUIRES_SCOPE_DIRECTIVE_NAME, | ||
description = REQUIRES_SCOPE_DIRECTIVE_DESCRIPTION, | ||
locations = [ | ||
Introspection.DirectiveLocation.ENUM, | ||
Introspection.DirectiveLocation.FIELD_DEFINITION, | ||
Introspection.DirectiveLocation.INTERFACE, | ||
Introspection.DirectiveLocation.OBJECT, | ||
Introspection.DirectiveLocation.SCALAR, | ||
] | ||
) | ||
annotation class RequiresScopesDirective(val scopes: Array<Scopes>) | ||
|
||
internal const val REQUIRES_SCOPE_DIRECTIVE_NAME = "requiresScopes" | ||
private const val REQUIRES_SCOPE_DIRECTIVE_DESCRIPTION = "Indicates to composition that the target element is accessible only to the authenticated supergraph users with the appropriate JWT scopes" | ||
private const val SCOPES_ARGUMENT = "scopes" | ||
|
||
internal fun requiresScopesDirectiveType(scopes: GraphQLScalarType): graphql.schema.GraphQLDirective = graphql.schema.GraphQLDirective.newDirective() | ||
.name(REQUIRES_SCOPE_DIRECTIVE_NAME) | ||
.description(REQUIRES_SCOPE_DIRECTIVE_DESCRIPTION) | ||
.validLocations( | ||
Introspection.DirectiveLocation.ENUM, | ||
Introspection.DirectiveLocation.FIELD_DEFINITION, | ||
Introspection.DirectiveLocation.INTERFACE, | ||
Introspection.DirectiveLocation.OBJECT, | ||
Introspection.DirectiveLocation.SCALAR | ||
) | ||
.argument( | ||
GraphQLArgument.newArgument() | ||
.name("scopes") | ||
.type( | ||
GraphQLNonNull.nonNull( | ||
GraphQLList.list( | ||
GraphQLNonNull( | ||
GraphQLList.list( | ||
scopes | ||
) | ||
) | ||
) | ||
) | ||
) | ||
) | ||
.build() | ||
|
||
@Suppress("UNCHECKED_CAST") | ||
internal fun graphql.schema.GraphQLDirective.toAppliedRequiresScopesDirective(directiveInfo: DirectiveMetaInformation): GraphQLAppliedDirective { | ||
// we need to manually transform @requiresScopes directive definition as JVM does not support nested array as annotation arguments | ||
val annotationScopes = directiveInfo.directive.annotationClass.memberProperties | ||
.first { it.name == SCOPES_ARGUMENT } | ||
.call(directiveInfo.directive) as? Array<Scopes> ?: emptyArray() | ||
val scopes = annotationScopes.map { scopesAnnotation -> scopesAnnotation.value.toList() } | ||
|
||
return this.toAppliedDirective() | ||
.transform { appliedDirectiveBuilder -> | ||
this.getArgument(SCOPES_ARGUMENT) | ||
.toAppliedArgument() | ||
.transform { argumentBuilder -> | ||
argumentBuilder.valueProgrammatic(scopes) | ||
} | ||
.let { | ||
appliedDirectiveBuilder.argument(it) | ||
} | ||
} | ||
} |
32 changes: 32 additions & 0 deletions
32
...eration/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/Scope.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
/* | ||
* Copyright 2023 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 JWT scope scalar type that is used by the `@requiresScope directive. | ||
* | ||
* @param value required JWT scope | ||
* @see [com.expediagroup.graphql.generator.federation.types.SCOPE_SCALAR_TYPE] | ||
*/ | ||
@LinkedSpec(FEDERATION_SPEC) | ||
annotation class Scope(val value: String) | ||
|
||
// this is a workaround for JVM lack of support nested arrays as annotation values | ||
@GraphQLIgnore | ||
annotation class Scopes(val value: Array<Scope>) |
74 changes: 74 additions & 0 deletions
74
...n-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/types/Scope.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
/* | ||
* Copyright 2023 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.Scope | ||
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 SCOPE_SCALAR_NAME = "Scope" | ||
|
||
/** | ||
* Custom scalar type that is used to represent a valid JWT scope which serializes as a String. | ||
*/ | ||
internal val SCOPE_SCALAR_TYPE: GraphQLScalarType = GraphQLScalarType.newScalar(Scalars.GraphQLString) | ||
.name(SCOPE_SCALAR_NAME) | ||
.description("Federation type representing a JWT scope") | ||
.coercing(ScopeCoercing) | ||
.build() | ||
|
||
private object ScopeCoercing : Coercing<Scope, String> { | ||
override fun serialize(dataFetcherResult: Any, graphQLContext: GraphQLContext, locale: Locale): String = | ||
when (dataFetcherResult) { | ||
is Scope -> dataFetcherResult.value | ||
else -> throw CoercingSerializeException( | ||
"Cannot serialize $dataFetcherResult. Expected type 'Scope' but was '${dataFetcherResult.javaClass.simpleName}'." | ||
) | ||
} | ||
|
||
override fun parseValue(input: Any, graphQLContext: GraphQLContext, locale: Locale): Scope = | ||
when (input) { | ||
is Scope -> input | ||
is StringValue -> Scope::class.constructors.first().call(input.value) | ||
else -> throw CoercingParseLiteralException( | ||
"Cannot parse $input to Scope. Expected AST type 'StringValue' but was '${input.javaClass.simpleName}'." | ||
) | ||
} | ||
|
||
override fun parseLiteral(input: Value<*>, variables: CoercedVariables, graphQLContext: GraphQLContext, locale: Locale): Scope = | ||
when (input) { | ||
is StringValue -> Scope::class.constructors.first().call(input.value) | ||
else -> throw CoercingParseLiteralException( | ||
"Cannot parse $input to Scope. Expected AST type 'StringValue' but was '${input.javaClass.simpleName}'." | ||
) | ||
} | ||
|
||
override fun valueToLiteral(input: Any, graphQLContext: GraphQLContext, locale: Locale): Value<*> = | ||
when (input) { | ||
is Scope -> StringValue.newStringValue(input.value).build() | ||
else -> throw CoercingValueToLiteralException(Scope::class, input) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.