Skip to content

Commit

Permalink
Add ./gradlew push${MainService}ApolloOperations (#3403)
Browse files Browse the repository at this point in the history
* add RegisterOperations

* add ApolloRegisterOperationsTask

* add a test case

* fix bad copy/paste

* update metalava signatures

* add some doc

* Update docs/source/advanced/operation-safelisting.mdx

Co-authored-by: Benoit Lubek <BoD@JRAF.org>

* Update docs/source/advanced/operation-safelisting.mdx

Co-authored-by: Benoit Lubek <BoD@JRAF.org>

* add the link to the operation registry server documentation

Co-authored-by: Benoit Lubek <BoD@JRAF.org>
  • Loading branch information
martinbonnin and BoD committed Oct 8, 2021
1 parent e48d5f8 commit 696c1af
Show file tree
Hide file tree
Showing 15 changed files with 288 additions and 2 deletions.
10 changes: 10 additions & 0 deletions apollo-gradle-plugin/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,21 @@ package com.apollographql.apollo.gradle.api {
property public abstract org.gradle.api.provider.Property<java.lang.String> sourceSetName;
}

public interface RegisterOperationsConfig {
method public org.gradle.api.provider.Property<java.lang.String> getGraph();
method public org.gradle.api.provider.Property<java.lang.String> getGraphVariant();
method public org.gradle.api.provider.Property<java.lang.String> getKey();
property public abstract org.gradle.api.provider.Property<java.lang.String> graph;
property public abstract org.gradle.api.provider.Property<java.lang.String> graphVariant;
property public abstract org.gradle.api.provider.Property<java.lang.String> key;
}

public interface Service extends com.apollographql.apollo.gradle.api.CompilerParams {
method public org.gradle.api.provider.ListProperty<java.lang.String> getExclude();
method public org.gradle.api.provider.Property<java.lang.String> getSchemaPath();
method public org.gradle.api.provider.Property<java.lang.String> getSourceFolder();
method public void introspection(org.gradle.api.Action<? super com.apollographql.apollo.gradle.api.Introspection> configure);
method public void registerOperations(org.gradle.api.Action<? super com.apollographql.apollo.gradle.api.RegisterOperationsConfig> configure);
property public abstract org.gradle.api.provider.ListProperty<java.lang.String> exclude;
property public abstract org.gradle.api.provider.Property<java.lang.String> schemaPath;
property public abstract org.gradle.api.provider.Property<java.lang.String> sourceFolder;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.apollographql.apollo.gradle.api

import org.gradle.api.provider.Property

interface RegisterOperationsConfig {
val key: Property<String>

val graph: Property<String>

val graphVariant: Property<String>
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.apollographql.apollo.gradle.api

import com.apollographql.apollo.gradle.internal.RegisterOperations
import org.gradle.api.Action
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
Expand All @@ -16,6 +17,11 @@ interface Service : CompilerParams {
*/
fun introspection(configure: Action<in Introspection>)

/**
* Configures the [Introspection]
*/
fun registerOperations(configure: Action<in RegisterOperationsConfig>)

/**
* path to the folder containing the graphql files relative to the current source set
* (src/$foo/graphql/$sourceFolder). The plugin will compile all graphql files accross all source sets
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.ConfigurationContainer
import org.gradle.api.attributes.Usage
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.TaskProvider
import org.gradle.util.GradleVersion
import java.net.URLDecoder
Expand Down Expand Up @@ -135,6 +133,8 @@ open class ApolloPlugin : Plugin<Project> {
else -> JvmTaskConfigurator.registerGeneratedDirectory(project, compilationUnit, codegenProvider)
}

maybeRegisterRegisterOperationsTasks(project, compilationUnit, codegenProvider)

}

rootProvider.configure {
Expand Down Expand Up @@ -265,6 +265,20 @@ open class ApolloPlugin : Plugin<Project> {
}
}

private fun maybeRegisterRegisterOperationsTasks(project: Project, compilationUnit: DefaultCompilationUnit, codegenProvider: TaskProvider<ApolloGenerateSourcesTask>) {
val registerOperationsConfig = compilationUnit.service.registerOperationsConfig
if (registerOperationsConfig != null) {
project.tasks.register(ModelNames.registerOperations(compilationUnit), ApolloRegisterOperationsTask::class.java) { task ->
task.group = TASK_GROUP

task.graph.set(registerOperationsConfig.graph)
task.graphVariant.set(registerOperationsConfig.graphVariant)
task.key.set(registerOperationsConfig.key)
task.operationOutput.set(codegenProvider.flatMap { it.operationOutputFile })
}
}
}

fun toMap(s: String): Map<String, String> {
return s.split("&")
.map {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.apollographql.apollo.gradle.internal

import com.apollographql.apollo.compiler.operationoutput.OperationOutput
import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.TaskAction

abstract class ApolloRegisterOperationsTask: DefaultTask() {
@get:InputFile
abstract val operationOutput: RegularFileProperty

@get:Input
abstract val key: Property<String>

@get:Input
abstract val graph: Property<String>

@get:Input
abstract val graphVariant: Property<String>

@TaskAction
fun taskAction() {
RegisterOperations.registerOperations(
key = key.get() ?: error("key is required to register operations"),
graphID = graph.get() ?: error("graphID is required to register operations"),
graphVariant = graphVariant.get() ?: error("graphVariant is required to register operations"),
operationOutput = OperationOutput(operationOutput.get().asFile)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.apollographql.apollo.gradle.internal

import com.apollographql.apollo.gradle.api.RegisterOperationsConfig
import org.gradle.api.provider.Property

abstract class DefaultRegisterOperationsConfig: RegisterOperationsConfig {
abstract override val key: Property<String>
abstract override val graph: Property<String>
abstract override val graphVariant: Property<String>
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.apollographql.apollo.gradle.internal

import com.apollographql.apollo.gradle.api.CompilerParams
import com.apollographql.apollo.gradle.api.Introspection
import com.apollographql.apollo.gradle.api.RegisterOperationsConfig
import com.apollographql.apollo.gradle.api.Service
import org.gradle.api.Action
import org.gradle.api.model.ObjectFactory
Expand All @@ -17,6 +18,7 @@ open class DefaultService @Inject constructor(val objects: ObjectFactory, val na
override val exclude = objects.listProperty(String::class.java)

var introspection: DefaultIntrospection? = null
var registerOperationsConfig: DefaultRegisterOperationsConfig? = null

override fun introspection(configure: Action<in Introspection>) {
val introspection = objects.newInstance(DefaultIntrospection::class.java, objects)
Expand All @@ -33,4 +35,18 @@ open class DefaultService @Inject constructor(val objects: ObjectFactory, val na

this.introspection = introspection
}

override fun registerOperations(configure: Action<in RegisterOperationsConfig>) {
generateOperationOutput.set(true)

val registerOperationsConfig = objects.newInstance(DefaultRegisterOperationsConfig::class.java)

if (this.registerOperationsConfig != null) {
throw IllegalArgumentException("there must be only one registerOperations block")
}

configure.execute(registerOperationsConfig)

this.registerOperationsConfig = registerOperationsConfig
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ object ModelNames {
fun downloadApolloSchema() = camelCase("downloadApolloSchema")
fun pushApolloSchema() = camelCase("pushApolloSchema")
fun downloadApolloSchema(service: DefaultService) = camelCase("download", service.name, "ApolloSchema")
fun registerOperations(compilationUnit: DefaultCompilationUnit) = camelCase("register", compilationUnit.variantName, compilationUnit.serviceName, "ApolloOperations")
fun checkApolloVersions() = "checkApolloVersions"
fun checkApolloDuplicates(compilationUnit: DefaultCompilationUnit)= camelCase("check", compilationUnit.variantName, compilationUnit.serviceName, "ApolloDuplicates")
fun convertApolloSchema() = "convertApolloSchema"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.apollographql.apollo.gradle.internal

import com.apollographql.apollo.compiler.fromJson
import com.apollographql.apollo.compiler.operationoutput.OperationOutput
import com.apollographql.apollo.gradle.internal.SchemaDownloader.cast
import org.jetbrains.kotlin.gradle.utils.`is`

object RegisterOperations {
private val mutation = """
mutation RegisterOperations(
${'$'}id : ID!
${'$'}clientIdentity : RegisteredClientIdentityInput!
${'$'}operations : [RegisteredOperationInput!]!
${'$'}manifestVersion : Int!
${'$'}graphVariant : String
) {
service(id: ${'$'}id ) {
registerOperationsWithResponse(
clientIdentity: ${'$'}clientIdentity
operations: ${'$'}operations
manifestVersion: ${'$'}manifestVersion
graphVariant: ${'$'}graphVariant
) {
invalidOperations {
errors {
message
}
signature
}
newOperations {
signature
}
registrationSuccess
}
}
}
""".trimIndent()

fun registerOperations(
key: String,
graphID: String,
graphVariant: String,
operationOutput: OperationOutput
) {
val variables = mapOf(
"id" to graphID,
"clientIdentity" to mapOf(
"name" to "apollo-android",
"identifier" to "apollo-android",
"version" to com.apollographql.apollo.compiler.VERSION,
),
"operations" to operationOutput.entries.map {
mapOf(
"signature" to it.key,
"document" to it.value.source
)
},
"manifestVersion" to 2,
"graphVariant" to graphVariant
)

val response = SchemaHelper.executeQuery(mutation, variables, "https://graphql.api.apollographql.com/api/graphql", mapOf("x-api-key" to key))

check(response.isSuccessful)

val responseString = response.body.use { it?.string() }

val errors = responseString
?.fromJson<Map<String, *>>()
?.get("data").cast<Map<String, *>>()
?.get("service").cast<Map<String, *>>()
?.get("registerOperationsWithResponse").cast<Map<String, *>>()
?.get("invalidOperations").cast<List<Map<String, *>>>()
?.flatMap {
it.get("errors").cast<List<String>>() ?: emptyList()
} ?: emptyList()

check(errors.isEmpty()) {
"Cannot push operations: ${errors.joinToString("\n")}"
}

val success = responseString
?.fromJson<Map<String, *>>()
?.get("data").cast<Map<String, *>>()
?.get("service").cast<Map<String, *>>()
?.get("registerOperationsWithResponse").cast<Map<String, *>>()
?.get("registrationSuccess").cast<Boolean>()
?: false

check(success) {
"Cannot push operations: $responseString"
}

println("Operations pushed successfully")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import com.apollographql.apollo.gradle.api.ApolloExtension
import com.apollographql.apollo.compiler.operationoutput.OperationOutput
import com.apollographql.apollo.compiler.operationoutput.OperationDescriptor
import com.apollographql.apollo.compiler.OperationOutputGenerator

buildscript {
apply(from = "../../../gradle/dependencies.gradle")

repositories {
maven {
url = uri("../../../build/localMaven")
}
mavenCentral()
}
dependencies {
classpath(groovy.util.Eval.x(project, "x.dep.kotlin.plugin"))
classpath(groovy.util.Eval.x(project, "x.dep.apollo.plugin"))
}
}

apply(plugin = "org.jetbrains.kotlin.jvm")
apply(plugin = "com.apollographql.apollo")

repositories {
maven {
url = uri("../../../build/localMaven")
}
mavenCentral()
}

configure<ApolloExtension> {
service("service") {
registerOperations {
key.set(System.getenv("APOLLO_KEY"))
graph.set(System.getenv("APOLLO_GRAPH"))
graphVariant.set("current")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
rootProject.name="registerOperations"

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
query Greeting {
greeting
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

type Query {
greeting: String
}
1 change: 1 addition & 0 deletions docs/gatsby-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ module.exports = {
'essentials/fragments',
'essentials/inline-fragments',
'essentials/migration',
'advanced/operation-safelisting',
],
}
}
Expand Down
40 changes: 40 additions & 0 deletions docs/source/advanced/operation-safelisting.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
title: Operation safelisting
sidebar_title: Operation safelisting (enterprise only)
description: Secure your graph by enforcing a safelist of registered operations
---

import { ExpansionPanel } from 'gatsby-theme-apollo-docs';

## Overview

> **Operation safelisting requires an Apollo Studio [Enterprise plan](https://www.apollographql.com/plans/).** To enable this feature, please contact Apollo.
If you enabled operation safelisting on your backend (see [here](https://www.apollographql.com/docs/studio/operation-registry/) for more information about how to do this), you can use Apollo Android Gradle plugin to register your operations automatically. Apollo Android might transform the GraphQL files you write to include `__typename` (for polymorphic types) or trim whitespaces (to save some space). Registering your operations through the Gradle plugin ensures the transformed versions are registered so that there is an exact match between what is registered and what is sent by your app.

Add this to your Gradle configuration:

```kotlin
apollo {
service("$serviceName") {

// Configure operation safelisting
registerOperations {
// You can get a key at https://studio.apollographql.com/graph/$graphId/settings
key.set(System.getenv("APOLLO_KEY"))
// Configure your graph.
graph.set(System.getenv("APOLLO_GRAPH"))
// Configure your variant.
graphVariant.set("current")
}
}
}
```

When your operations are stable and you want to safelist them, execute the `registerMain${serviceName}ApolloOperations` task to push all your operation to the registry.

```kotlin
./gradlew registerMainServiceApolloOperations
```

If everything goes well, your queries are now safelisted and safe to use in your mobile app.

0 comments on commit 696c1af

Please sign in to comment.