All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.stargate.graphql.web.resources.GraphqlResourceBase Maven / Gradle / Ivy

There is a newer version: 2.0.31
Show newest version
/*
 * Copyright The Stargate Authors
 *
 * 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
 *
 * http://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 io.stargate.graphql.web.resources;

import com.datastax.oss.driver.shaded.guava.common.base.MoreObjects;
import com.datastax.oss.driver.shaded.guava.common.base.Splitter;
import com.datastax.oss.driver.shaded.guava.common.base.Strings;
import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList;
import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import graphql.ExecutionInput;
import graphql.GraphQL;
import graphql.GraphqlErrorException;
import io.stargate.auth.AuthenticationSubject;
import io.stargate.auth.AuthorizationService;
import io.stargate.auth.SourceAPI;
import io.stargate.auth.UnauthorizedException;
import io.stargate.auth.entity.ResourceKind;
import io.stargate.db.Persistence;
import io.stargate.graphql.web.StargateGraphqlContext;
import io.stargate.graphql.web.models.GraphqlJsonBody;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Produces;
import javax.ws.rs.container.AsyncResponse;
import javax.ws.rs.container.Suspended;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.glassfish.jersey.media.multipart.FormDataBodyPart;
import org.glassfish.jersey.media.multipart.FormDataMultiPart;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Factors common logic to handle GraphQL queries from JAX-RS resources.
 *
 * @see Serving (GraphQL) over HTTP
 */
@Produces(MediaType.APPLICATION_JSON)
public class GraphqlResourceBase {

  private static final Logger LOG = LoggerFactory.getLogger(DdlResource.class);
  protected static final String APPLICATION_GRAPHQL = "application/graphql";
  private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
  private static final TypeReference>> FILES_MAPPING_TYPE =
      new TypeReference>>() {};
  private static final Splitter PATH_SPLITTER = Splitter.on(".");

  @Inject protected AuthorizationService authorizationService;
  @Inject protected Persistence persistence;
  @Inject protected GraphqlCache graphqlCache;

  /**
   * Handles a GraphQL GET request.
   *
   * 

The payload is provided via URL parameters. */ protected void get( String query, String operationName, String variables, GraphQL graphql, HttpServletRequest httpRequest, AsyncResponse asyncResponse) { if (Strings.isNullOrEmpty(query)) { replyWithGraphqlError( Status.BAD_REQUEST, "You must provide a GraphQL query as a URL parameter", asyncResponse); return; } try { ExecutionInput.Builder input = ExecutionInput.newExecutionInput(query) .operationName(operationName) .context( new StargateGraphqlContext( httpRequest, authorizationService, persistence, graphqlCache)); if (!Strings.isNullOrEmpty(variables)) { @SuppressWarnings("unchecked") Map parsedVariables = OBJECT_MAPPER.readValue(variables, Map.class); input = input.variables(parsedVariables); } executeAsync(input.build(), graphql, asyncResponse); } catch (IOException e) { replyWithGraphqlError( Status.BAD_REQUEST, "Could not parse variables: " + e.getMessage(), asyncResponse); } } /** * Handles a GraphQL POST request that uses the {@link MediaType#APPLICATION_JSON} content type. * *

Such a request normally comprises a JSON-encoded body, but the spec also allows the query to * be passed as a URL parameter. */ protected void postJson( GraphqlJsonBody jsonBody, String queryFromUrl, GraphQL graphql, HttpServletRequest httpRequest, AsyncResponse asyncResponse) { queryFromUrl = Strings.emptyToNull(queryFromUrl); String queryFromBody = (jsonBody == null) ? null : Strings.emptyToNull(jsonBody.getQuery()); String operationName = (jsonBody == null) ? null : Strings.emptyToNull(jsonBody.getOperationName()); Map variables = (jsonBody == null) ? null : jsonBody.getVariables(); if (queryFromBody == null && queryFromUrl == null) { replyWithGraphqlError( Status.BAD_REQUEST, "You must provide a GraphQL query, either as a query parameter or in the request body", asyncResponse); return; } if (queryFromBody != null && queryFromUrl != null) { // The GraphQL spec doesn't specify what to do in this case, but it's probably better to error // out rather than pick one arbitrarily. replyWithGraphqlError( Status.BAD_REQUEST, "You can't provide a GraphQL query both as a query parameter and in the request body", asyncResponse); return; } String query = MoreObjects.firstNonNull(queryFromBody, queryFromUrl); ExecutionInput.Builder input = ExecutionInput.newExecutionInput(query) .operationName(operationName) .context( new StargateGraphqlContext( httpRequest, authorizationService, persistence, graphqlCache)); if (variables != null) { input = input.variables(variables); } executeAsync(input.build(), graphql, asyncResponse); } /** * Handles a GraphQL POST request that uses the {@link MediaType#MULTIPART_FORM_DATA} content * type, allowing file arguments. * *

It follows the GraphQL multipart request * specification. * *

Example cURL call: * *

   * curl http://host:port/path/to/graphql \
   *   -F operations='{ "query": "query ($file: Upload!) { someQuery(file: $file) }", "variables": { "file": null } };type=application/json'
   *   -F map='{ "filePart": ["variables.file"] }'
   *   -FfilePart=@/path/to/file.txt
   * 
* * The first part MUST declare a JSON content type ("type=application/json" in the example above). * *

This method assumes that its argument come from properly annotated arguments in a Jersey * resource; see the existing callers for an example. * * @param jsonBody the JSON payload containing the GraphQL "operations object" to execute (eg * {query=..., variables=...}). It's parsed from the operations * part in the request. * @param allParts the whole multipart request, consisting of: the operations part * (ignored since we've already parsed it above), a map part containing a map * that specifies which GraphQL variable each file corresponds to, and an arbitrary number of * file parts named after the keys of the files map. * @param graphql the GraphQL schema to use for execution. */ protected void postMultipartJson( GraphqlJsonBody jsonBody, FormDataMultiPart allParts, GraphQL graphql, HttpServletRequest httpRequest, AsyncResponse asyncResponse) { if (jsonBody == null) { replyWithGraphqlError( Status.BAD_REQUEST, "Could not find GraphQL operations object. " + "Make sure your multipart request includes an 'operations' part with MIME type " + MediaType.APPLICATION_JSON, asyncResponse); return; } if (bindFilesToVariables(jsonBody, allParts, asyncResponse)) { postJson( jsonBody, // We don't allow passing the query as a URL param for this variant. The spec does not // preclude it explicitly, but it's unlikely that someone would try to do that. null, graphql, httpRequest, asyncResponse); } } /** * Given: * *

    *
  • a GraphQL query such as: *
       *       { "query": "...", "variables": { "file1": , "file2":  } }
       *       
    *
  • a 'map' part such as *
       *       "part1": [ "variables.file1" ], "part2": [ "variables.file2" ] }
       *       
    *
  • two parts 'part1' and 'part2' with the contents of the corresponding files. *
* *

We want to read each file part as an {@link InputStream}, and inject it in {@link * GraphqlJsonBody#getVariables()} at the corresponding position (overriding whatever was there). * * @return true if the process succeeded. Otherwise an error response has already been written. */ private static boolean bindFilesToVariables( GraphqlJsonBody jsonBody, FormDataMultiPart allParts, AsyncResponse asyncResponse) { Map variables = jsonBody.getVariables(); FormDataBodyPart filesMappingPart = allParts.getField("map"); if (filesMappingPart != null) { if (variables == null || variables.isEmpty()) { // We could just ignore the files but this is likely a user mistake replyWithGraphqlError( Status.BAD_REQUEST, "Found a 'map' part but the GraphQL query has no variables", asyncResponse); return false; } Map> filesMapping; try { filesMapping = OBJECT_MAPPER.readValue(filesMappingPart.getValue(), FILES_MAPPING_TYPE); } catch (JsonProcessingException e) { replyWithGraphqlError( Status.BAD_REQUEST, "Could not parse map part: " + e.getMessage(), asyncResponse); return false; } for (Map.Entry> entry : filesMapping.entrySet()) { String partName = entry.getKey(); List variablePaths = entry.getValue(); FormDataBodyPart part = allParts.getField(partName); if (part == null) { replyWithGraphqlError( Status.BAD_REQUEST, String.format( "The 'map' part references '%s', but found no part with that name", partName), asyncResponse); return false; } if (variablePaths == null || variablePaths.size() != 1) { // The spec allows more than one variable, but we won't use that feature and it would // complicate things with InputStream. replyWithGraphqlError( Status.BAD_REQUEST, String.format( "This implementation only allows file parts to reference exactly one variable " + "(offending part: '%s' with %d variables)", partName, variablePaths == null ? 0 : variablePaths.size()), asyncResponse); return false; } String variablePath = variablePaths.get(0); List pathElements = PATH_SPLITTER.splitToList(variablePath); if (pathElements.size() != 2 && !"variables".equals(pathElements.get(0))) { // Again, the spec allows more complicated cases like nested variables or arrays, but we // won't need that so let's keep it simple for now. replyWithGraphqlError( Status.BAD_REQUEST, String.format( "This implementation only allows simple variable references like 'variables.x' " + "(offending reference: '%s')", variablePath), asyncResponse); return false; } String variableName = pathElements.get(1); variables.put(variableName, part.getEntityAs(InputStream.class)); } } return true; } /** * Handles a GraphQL POST request that uses the "application/graphql" content type. * *

The request body is the GraphQL query directly. */ protected void postGraphql( String query, GraphQL graphql, HttpServletRequest httpRequest, AsyncResponse asyncResponse) { if (Strings.isNullOrEmpty(query)) { replyWithGraphqlError( Status.BAD_REQUEST, "You must provide a GraphQL query in the request body", asyncResponse); return; } ExecutionInput input = ExecutionInput.newExecutionInput(query) .context( new StargateGraphqlContext( httpRequest, authorizationService, persistence, graphqlCache)) .build(); executeAsync(input, graphql, asyncResponse); } protected static void executeAsync( ExecutionInput input, GraphQL graphql, @Suspended AsyncResponse asyncResponse) { graphql .executeAsync(input) .whenComplete( (result, error) -> { if (error != null) { LOG.error("Unexpected error while processing GraphQL request", error); replyWithGraphqlError( Status.INTERNAL_SERVER_ERROR, "Internal server error", asyncResponse); } else { StargateGraphqlContext context = (StargateGraphqlContext) input.getContext(); if (context.isOverloaded()) { replyWithGraphqlError( Status.TOO_MANY_REQUESTS, "Database is overloaded", asyncResponse); } else { asyncResponse.resume(result.toSpecification()); } } }); } protected boolean isAuthorized(HttpServletRequest httpRequest, String keyspaceName) { AuthenticationSubject subject = (AuthenticationSubject) httpRequest.getAttribute(AuthenticationFilter.SUBJECT_KEY); try { authorizationService.authorizeSchemaRead( subject, Collections.singletonList(keyspaceName), Collections.emptyList(), SourceAPI.GRAPHQL, ResourceKind.KEYSPACE); return true; } catch (UnauthorizedException e) { return false; } } protected static void replyWithGraphqlError( Status status, String message, @Suspended AsyncResponse asyncResponse) { asyncResponse.resume( Response.status(status) .entity( ImmutableMap.of("errors", ImmutableList.of(ImmutableMap.of("message", message)))) .build()); } protected static void replyWithGraphqlError( Status status, GraphqlErrorException error, @Suspended AsyncResponse asyncResponse) { asyncResponse.resume( Response.status(status) .entity(ImmutableMap.of("errors", ImmutableList.of(error.toSpecification()))) .build()); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy