Skip to content

Commit

Permalink
Merge pull request #37136 from jmartisk/2.13-graphql-websocket-2
Browse files Browse the repository at this point in the history
(2.13) graphql websocket fixes
  • Loading branch information
gsmet authored Nov 16, 2023
2 parents 22c9319 + 452167f commit c8e69bd
Show file tree
Hide file tree
Showing 19 changed files with 927 additions and 5 deletions.
4 changes: 2 additions & 2 deletions .github/native-tests.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@
},
{
"category": "Misc3",
"timeout": 65,
"test-modules": "kubernetes-client, openshift-client, kubernetes-service-binding-jdbc, smallrye-config, smallrye-graphql, smallrye-graphql-client, smallrye-metrics, smallrye-opentracing",
"timeout": 80,
"test-modules": "kubernetes-client, openshift-client, kubernetes-service-binding-jdbc, smallrye-config, smallrye-graphql, smallrye-graphql-client, smallrye-graphql-client-keycloak, smallrye-metrics, smallrye-opentracing",
"os-name": "ubuntu-latest"
},
{
Expand Down
2 changes: 1 addition & 1 deletion bom/application/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
<smallrye-health.version>3.3.0</smallrye-health.version>
<smallrye-metrics.version>3.0.5</smallrye-metrics.version>
<smallrye-open-api.version>2.2.1</smallrye-open-api.version>
<smallrye-graphql.version>1.7.2</smallrye-graphql.version> <!-- keep in sync with graphql-java -->
<smallrye-graphql.version>1.7.3</smallrye-graphql.version> <!-- keep in sync with graphql-java -->
<smallrye-opentracing.version>2.1.1</smallrye-opentracing.version>
<smallrye-fault-tolerance.version>5.5.0</smallrye-fault-tolerance.version>
<smallrye-jwt.version>3.5.4</smallrye-jwt.version>
Expand Down
10 changes: 10 additions & 0 deletions extensions/smallrye-graphql-client/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@
<artifactId>stork-service-discovery-static-list</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-elytron-security-deployment</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-elytron-security-properties-file-deployment</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package io.quarkus.smallrye.graphql.client.deployment;

import org.eclipse.microprofile.graphql.GraphQLApi;
import org.eclipse.microprofile.graphql.Query;
import org.jboss.shrinkwrap.api.asset.EmptyAsset;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;
import io.smallrye.graphql.api.Subscription;
import io.smallrye.graphql.client.Response;
import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient;
import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClientBuilder;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.helpers.test.AssertSubscriber;
import io.vertx.core.http.UpgradeRejectedException;

public class DynamicGraphQLClientWebSocketAuthenticationHttpPermissionsTest {

static String url = "http://" + System.getProperty("quarkus.http.host", "localhost") + ":" +
System.getProperty("quarkus.http.test-port", "8081") + "/graphql";

@RegisterExtension
static QuarkusUnitTest test = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(SecuredApi.class, Foo.class)
.addAsResource("application-secured-http-permissions.properties", "application.properties")
.addAsResource("users.properties")
.addAsResource("roles.properties")
.addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml"));

@Test
public void testUnauthenticatedForQueryWebSocket() throws Exception {
DynamicGraphQLClientBuilder clientBuilder = DynamicGraphQLClientBuilder.newBuilder()
.url(url)
.executeSingleOperationsOverWebsocket(true);
try (DynamicGraphQLClient client = clientBuilder.build()) {
try {
client.executeSync("{ baz { message} }");
Assertions.fail("WebSocket upgrade should fail");
} catch (UpgradeRejectedException e) {
// ok
}
}
}

@Test
public void testUnauthenticatedForSubscriptionWebSocket() throws Exception {
DynamicGraphQLClientBuilder clientBuilder = DynamicGraphQLClientBuilder.newBuilder()
.url(url);
try (DynamicGraphQLClient client = clientBuilder.build()) {
AssertSubscriber<Response> subscriber = new AssertSubscriber<>();
client.subscription("{ bazSub { message} }").subscribe().withSubscriber(subscriber);
subscriber.awaitFailure().assertFailedWith(UpgradeRejectedException.class);
}
}

public static class Foo {

private String message;

public Foo(String foo) {
this.message = foo;
}

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}

}

@GraphQLApi
public static class SecuredApi {

@Query
public Foo baz() {
return new Foo("baz");
}

@Subscription
public Multi<Foo> bazSub() {
return Multi.createFrom().emitter(emitter -> {
emitter.emit(new Foo("baz"));
emitter.complete();
});
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
package io.quarkus.smallrye.graphql.client.deployment;

import static org.awaitility.Awaitility.await;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.concurrent.atomic.AtomicBoolean;

import javax.annotation.security.RolesAllowed;
import javax.json.JsonValue;

import org.eclipse.microprofile.graphql.GraphQLApi;
import org.eclipse.microprofile.graphql.Query;
import org.jboss.shrinkwrap.api.asset.EmptyAsset;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;
import io.smallrye.common.annotation.NonBlocking;
import io.smallrye.graphql.api.Subscription;
import io.smallrye.graphql.client.Response;
import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient;
import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClientBuilder;
import io.smallrye.mutiny.Multi;

/**
* Due to the complexity of establishing a WebSocket, WebSocket/Subscription testing of the GraphQL server is done here,
* as the client framework comes in very useful for establishing the connection to the server.
* <br>
* This test establishes connections to the server, and ensures that the connected user has the necessary permissions to
* execute the operation.
*/
public class DynamicGraphQLClientWebSocketAuthenticationTest {

static String url = "http://" + System.getProperty("quarkus.http.host", "localhost") + ":" +
System.getProperty("quarkus.http.test-port", "8081") + "/graphql";

@RegisterExtension
static QuarkusUnitTest test = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(SecuredApi.class, Foo.class)
.addAsResource("application-secured.properties", "application.properties")
.addAsResource("users.properties")
.addAsResource("roles.properties")
.addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml"));

@Test
public void testAuthenticatedUserForSubscription() throws Exception {
DynamicGraphQLClientBuilder clientBuilder = DynamicGraphQLClientBuilder.newBuilder()
.url(url)
.header("Authorization", "Basic ZGF2aWQ6cXdlcnR5MTIz");
try (DynamicGraphQLClient client = clientBuilder.build()) {
Multi<Response> subscription = client
.subscription("subscription fooSub { fooSub { message } }");

assertNotNull(subscription);

AtomicBoolean hasData = new AtomicBoolean(false);
AtomicBoolean hasCompleted = new AtomicBoolean(false);

subscription.subscribe().with(item -> {
assertFalse(hasData.get());
assertTrue(item.hasData());
assertEquals(JsonValue.ValueType.OBJECT, item.getData().get("fooSub").getValueType());
assertEquals("foo", item.getData().getJsonObject("fooSub").getString("message"));
hasData.set(true);
}, Assertions::fail, () -> {
hasCompleted.set(true);
});

await().untilTrue(hasCompleted);
assertTrue(hasData.get());
}
}

@Test
public void testAuthenticatedUserForQueryWebSocket() throws Exception {
DynamicGraphQLClientBuilder clientBuilder = DynamicGraphQLClientBuilder.newBuilder()
.url(url)
.header("Authorization", "Basic ZGF2aWQ6cXdlcnR5MTIz")
.executeSingleOperationsOverWebsocket(true);
try (DynamicGraphQLClient client = clientBuilder.build()) {
Response response = client.executeSync("{ foo { message} }");
assertTrue(response.hasData());
assertEquals("foo", response.getData().getJsonObject("foo").getString("message"));
}
}

@Test
public void testAuthorizedAndUnauthorizedForQueryWebSocket() throws Exception {
DynamicGraphQLClientBuilder clientBuilder = DynamicGraphQLClientBuilder.newBuilder()
.url(url)
.header("Authorization", "Basic ZGF2aWQ6cXdlcnR5MTIz")
.executeSingleOperationsOverWebsocket(true);
try (DynamicGraphQLClient client = clientBuilder.build()) {
Response response = client.executeSync("{ foo { message} }");
assertTrue(response.hasData());
assertEquals("foo", response.getData().getJsonObject("foo").getString("message"));

// Run a second query with a different result to validate that the result of the first query isn't being cached at all.
response = client.executeSync("{ bar { message} }");
assertEquals(JsonValue.ValueType.NULL, response.getData().get("bar").getValueType());
}
}

@Test
public void testUnauthorizedUserForSubscription() throws Exception {
DynamicGraphQLClientBuilder clientBuilder = DynamicGraphQLClientBuilder.newBuilder()
.url(url)
.header("Authorization", "Basic ZGF2aWQ6cXdlcnR5MTIz");
try (DynamicGraphQLClient client = clientBuilder.build()) {
Multi<Response> subscription = client
.subscription("subscription barSub { barSub { message } }");

assertNotNull(subscription);

AtomicBoolean returned = new AtomicBoolean(false);

subscription.subscribe().with(item -> {
assertEquals(JsonValue.ValueType.NULL, item.getData().get("barSub").getValueType());
returned.set(true);
}, throwable -> Assertions.fail(throwable));

await().untilTrue(returned);
}
}

@Test
public void testUnauthorizedUserForQueryWebSocket() throws Exception {
DynamicGraphQLClientBuilder clientBuilder = DynamicGraphQLClientBuilder.newBuilder()
.url(url)
.header("Authorization", "Basic ZGF2aWQ6cXdlcnR5MTIz")
.executeSingleOperationsOverWebsocket(true);
try (DynamicGraphQLClient client = clientBuilder.build()) {
Response response = client.executeSync("{ bar { message } }");
assertEquals(JsonValue.ValueType.NULL, response.getData().get("bar").getValueType());
}
}

@Test
public void testUnauthenticatedForQueryWebSocket() throws Exception {
DynamicGraphQLClientBuilder clientBuilder = DynamicGraphQLClientBuilder.newBuilder()
.url(url)
.executeSingleOperationsOverWebsocket(true);
try (DynamicGraphQLClient client = clientBuilder.build()) {
Response response = client.executeSync("{ foo { message} }");
assertEquals(JsonValue.ValueType.NULL, response.getData().get("foo").getValueType());
}
}

public static class Foo {

private String message;

public Foo(String foo) {
this.message = foo;
}

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}

}

@GraphQLApi
public static class SecuredApi {

@Query
@RolesAllowed("fooRole")
@NonBlocking
public Foo foo() {
return new Foo("foo");
}

@Query
@RolesAllowed("barRole")
public Foo bar() {
return new Foo("bar");
}

@Subscription
@RolesAllowed("fooRole")
public Multi<Foo> fooSub() {
return Multi.createFrom().emitter(emitter -> {
emitter.emit(new Foo("foo"));
emitter.complete();
});
}

@Subscription
@RolesAllowed("barRole")
public Multi<Foo> barSub() {
return Multi.createFrom().emitter(emitter -> {
emitter.emit(new Foo("bar"));
emitter.complete();
});
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
quarkus.security.users.file.enabled=true
quarkus.security.users.file.plain-text=true
quarkus.security.users.file.users=users.properties
quarkus.security.users.file.roles=roles.properties
quarkus.http.auth.basic=true

quarkus.smallrye-graphql.log-payload=queryAndVariables
quarkus.smallrye-graphql.print-data-fetcher-exception=true
quarkus.smallrye-graphql.error-extension-fields=exception,classification,code,description,validationErrorType,queryPath

quarkus.http.auth.permission.authenticated.paths=/graphql
quarkus.http.auth.permission.authenticated.methods=GET,POST
quarkus.http.auth.permission.authenticated.policy=authenticated
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
quarkus.security.users.file.enabled=true
quarkus.security.users.file.plain-text=true
quarkus.security.users.file.users=users.properties
quarkus.security.users.file.roles=roles.properties
quarkus.http.auth.basic=true

quarkus.smallrye-graphql.log-payload=queryAndVariables
quarkus.smallrye-graphql.print-data-fetcher-exception=true
quarkus.smallrye-graphql.error-extension-fields=exception,classification,code,description,validationErrorType,queryPath
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
david=fooRole
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
david=qwerty123
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
import io.quarkus.smallrye.graphql.runtime.SmallRyeGraphQLRecorder;
import io.quarkus.smallrye.graphql.runtime.SmallRyeGraphQLRuntimeConfig;
import io.quarkus.vertx.http.deployment.BodyHandlerBuildItem;
import io.quarkus.vertx.http.deployment.FilterBuildItem;
import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem;
import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem;
import io.quarkus.vertx.http.deployment.RouteBuildItem;
Expand Down Expand Up @@ -130,6 +131,10 @@ public class SmallRyeGraphQLProcessor {
private static final List<String> SUPPORTED_WEBSOCKET_SUBPROTOCOLS = List.of(SUBPROTOCOL_GRAPHQL_WS,
SUBPROTOCOL_GRAPHQL_TRANSPORT_WS);

private static final int GRAPHQL_WEBSOCKET_HANDLER_ORDER = (-1 * FilterBuildItem.AUTHORIZATION) + 1;

private static final String GRAPHQL_MEDIA_TYPE = "application/graphql+json";

@BuildStep
void feature(BuildProducer<FeatureBuildItem> featureProducer) {
featureProducer.produce(new FeatureBuildItem(Feature.SMALLRYE_GRAPHQL));
Expand Down Expand Up @@ -332,7 +337,7 @@ void buildExecutionEndpoint(
runBlocking);

HttpRootPathBuildItem.Builder subscriptionsBuilder = httpRootPathBuildItem.routeBuilder()
.orderedRoute(graphQLConfig.rootPath, Integer.MIN_VALUE)
.orderedRoute(graphQLConfig.rootPath, GRAPHQL_WEBSOCKET_HANDLER_ORDER)
.handler(graphqlOverWebsocketHandler);
routeProducer.produce(subscriptionsBuilder.build());

Expand Down
Loading

0 comments on commit c8e69bd

Please sign in to comment.