/// <summary> /// Called by the runtime to invoke the middleware logic. In most cases, the middleware component should return the task /// generated by calling the next delegate in the request chain with the provided context. /// </summary> /// <param name="context">The invocation request governing data in this pipeline run.</param> /// <param name="next">The next delegate in the chain to be invoked.</param> /// <param name="cancelToken">The cancel token.</param> /// <returns>Task.</returns> public async Task InvokeAsync(GraphFieldExecutionContext context, GraphMiddlewareInvocationDelegate <GraphFieldExecutionContext> next, CancellationToken cancelToken) { FieldAuthorizationResult result = FieldAuthorizationResult.Default(); if (context.IsValid) { // execute the authorization pipeline var authRequest = new GraphFieldAuthorizationRequest(context.Request); var authContext = new GraphFieldAuthorizationContext(context, authRequest); await _authPipeline.InvokeAsync(authContext, cancelToken).ConfigureAwait(false); result = authContext.Result ?? FieldAuthorizationResult.Default(); // by default, deny any stati not explicitly declared as "successful" by this component. if (!result.Status.IsAuthorized()) { context.Messages.Critical( $"Access Denied to field {context.Field.Route.Path}", Constants.ErrorCodes.ACCESS_DENIED, context.Request.Origin); } } if (!result.Status.IsAuthorized()) { context.ResolvedSourceItems.AddRange(context.Request.DataSource.Items); context.ResolvedSourceItems.ForEach(x => x.Fail()); } await next(context, cancelToken).ConfigureAwait(false); }
/// <summary> /// Iterates over every secure field in the operation on the context, attempting to authorize the /// user to each one. /// </summary> /// <param name="context">The primary query context.</param> /// <param name="cancelToken">The cancel token.</param> /// <returns><c>true</c> if authorization was successful, otherwise false.</returns> private async Task <bool> AuthorizeOperation(GraphQueryExecutionContext context, CancellationToken cancelToken) { var authTasks = new List <Task>(); bool anyFieldFailed = false; foreach (var fieldContext in context.QueryOperation.SecureFieldContexts) { var authRequest = new GraphFieldAuthorizationRequest(fieldContext); var authContext = new GraphFieldAuthorizationContext(context, authRequest); var pipelineTask = _authPipeline.InvokeAsync(authContext, cancelToken) .ContinueWith( (_) => { var authResult = authContext.Result ?? FieldAuthorizationResult.Default(); // fake the path elements from the field route. since we don't have a full resolution chain // when doing query level authorization (no indexers on potential child fields since // nothing is actually resolved yet) if (!authResult.Status.IsAuthorized()) { context.Messages.Critical( $"Access Denied to field {fieldContext.Field.Route.Path}", Constants.ErrorCodes.ACCESS_DENIED, fieldContext.Origin); anyFieldFailed = true; } }, cancelToken); authTasks.Add(pipelineTask); } await Task.WhenAll(authTasks).ConfigureAwait(false); return(anyFieldFailed); }
/// <inheritdoc /> public async Task <IGraphOperationResult> ExecuteRequest( GraphQueryExecutionContext context, CancellationToken cancelToken = default) { Validation.ThrowIfNull(context, nameof(context)); // ******************************* // Primary query execution // ******************************* await _pipeline.InvokeAsync(context, cancelToken).ConfigureAwait(false); // ******************************* // Response Generation // ******************************* var queryResponse = context.Result; if (queryResponse == null) { queryResponse = new GraphOperationResult(context.Request); queryResponse.Messages.Add(GraphMessageSeverity.Critical, ERROR_NO_RESPONSE, Constants.ErrorCodes.GENERAL_ERROR); } return(queryResponse); }
/// <summary> /// Submits the GraphQL query for processing. /// </summary> /// <param name="queryData">The query data.</param> /// <returns>Task<IActionResult>.</returns> public virtual async Task SubmitGraphQLQuery(GraphQueryData queryData) { // ensure data was received if (queryData == null || string.IsNullOrWhiteSpace(queryData.Query)) { await this.WriteStatusCodeResponse(HttpStatusCode.BadRequest, ERROR_NO_QUERY_PROVIDED).ConfigureAwait(false); return; } using (var cancelSource = new CancellationTokenSource()) { try { // ******************************* // Setup // ******************************* this.GraphQLRequest = this.CreateRequest(queryData); if (this.GraphQLRequest == null) { await this.WriteStatusCodeResponse(HttpStatusCode.InternalServerError, ERROR_NO_REQUEST_CREATED).ConfigureAwait(false); return; } // ******************************* // Primary query execution // ******************************* var metricPackage = this.EnableMetrics ? _metricsFactory.CreateMetricsPackage() : null; var context = new GraphQueryExecutionContext( this.GraphQLRequest, this.HttpContext.RequestServices, this.HttpContext.User, metricPackage, _logger); await _queryPipeline.InvokeAsync(context, cancelSource.Token).ConfigureAwait(false); // ******************************* // Response Generation // ******************************* var queryResponse = context.Result; if (queryResponse == null) { queryResponse = this.ErrorMessageAsGraphQLResponse(ERROR_NO_RESPONSE); } // if any metrics were populated in the execution, allow a child class to process them if (context.Metrics != null) { this.HandleQueryMetrics(context.Metrics); } // all done, finalize and return queryResponse = this.FinalizeResult(queryResponse); await this.WriteResponse(queryResponse).ConfigureAwait(false); } catch (Exception ex) { var exceptionResult = this.HandleQueryException(ex); if (exceptionResult == null) { // no one was able to handle hte exception. Log it if able and just fail out to the caller _logger?.UnhandledExceptionEvent(ex); await this.WriteStatusCodeResponse(HttpStatusCode.InternalServerError, ERROR_INTERNAL_SERVER_ISSUE).ConfigureAwait(false); } else { await this.WriteResponse(exceptionResult).ConfigureAwait(false); } } } }
private async Task ExecuteOperation(GraphQueryExecutionContext context) { // create a cancelation sourc irrespective of the required timeout for exeucting this operation // this allows for indication of why a task was canceled (timeout or other user driven reason) // vs just "it was canceled" which allows for tighter error messages in the response. var operation = context.QueryOperation; var fieldInvocations = new List <FieldPipelineInvocation>(); // Convert the supplied variable values to usable objects of the type expression // of the chosen operation var variableResolver = new ResolvedVariableGenerator(_schema, operation); var variableData = variableResolver.Resolve(context.Request.VariableData); var cancelSource = new CancellationTokenSource(); try { // begin a field execution pipeline for each top level field foreach (var invocationContext in operation.FieldContexts) { var path = new SourcePath(); path.AddFieldName(invocationContext.Name); object dataSourceValue; // fetch the source data value to use for the field invocation // attempt to retrieve from the master context if it was supplied by the pipeline // invoker, otherwise generate a root source if (!context.DefaultFieldSources.TryRetrieveSource(invocationContext.Field, out dataSourceValue)) { dataSourceValue = this.GenerateRootSourceData(operation.OperationType); } var topLevelDataItem = new GraphDataItem(invocationContext, dataSourceValue, path); var sourceData = new GraphFieldDataSource(dataSourceValue, path, topLevelDataItem); var fieldRequest = new GraphFieldRequest( invocationContext, sourceData, new SourceOrigin(invocationContext.Origin.Location, path), context.Items); var fieldContext = new GraphFieldExecutionContext( context, fieldRequest, variableData, context.DefaultFieldSources); var fieldTask = _fieldExecutionPipeline.InvokeAsync(fieldContext, cancelSource.Token); var pipelineInvocation = new FieldPipelineInvocation() { Task = fieldTask, DataItem = topLevelDataItem, FieldContext = fieldContext, }; fieldInvocations.Add(pipelineInvocation); // top level mutation operatons must be executed in sequential order // https://graphql.github.io/graphql-spec/June2018/#sec-Mutation if (_awaitEachTask || operation.OperationType == GraphCollection.Mutation) { await fieldTask.ConfigureAwait(false); } } // await all the outstanding tasks or a configured timeout var fieldPipelineTasksWrapper = Task.WhenAll(fieldInvocations.Select(x => x.Task)); var timeOutTask = Task.Delay(_timeoutMs, cancelSource.Token); var completedTask = await Task.WhenAny(fieldPipelineTasksWrapper, timeOutTask).ConfigureAwait(false); var isTimedOut = completedTask == timeOutTask; var cancelationWasRequested = cancelSource.IsCancellationRequested; if (!isTimedOut) { // Field resolutions completed within the timeout period. // Consider that the task may have faulted or been canceled causing them to complete incorrectly. // "re-await" so that any exceptions/cancellation are rethrown correctly. // and not aggregated under the `WhenAll/WhenAny` task from above // https://stackoverflow.com/questions/4238345/asynchronously-wait-for-taskt-to-complete-with-timeout foreach (var invocation in fieldInvocations) { await invocation.Task.ConfigureAwait(false); // load the reslts of each field (in order) to the context // for further processing context.FieldResults.Add(invocation.DataItem); context.Messages.AddRange(invocation.FieldContext.Messages); } } else { // when the timeout finishes first, process the cancel token in case any outstanding tasks are running // helps in cases where the timeout finished first but any of the field resolutions are perhaps stuck open // instruct all outstanding tasks to clean them selves up at the earlest possible point if (!cancelationWasRequested) { cancelSource.Cancel(); } } if (cancelationWasRequested) { context.Messages.Critical("The execution was canceled prior to completion of the requested query.", Constants.ErrorCodes.OPERATION_CANCELED); } else if (isTimedOut) { context.Messages.Critical($"The execution timed out prior to completion of the requested query. (Total Time: {_timeoutMs}ms)", Constants.ErrorCodes.OPERATION_CANCELED); } } finally { cancelSource.Dispose(); } }
/// <summary> /// For any resolved, non-leaf items assigned to the result, pass each through the resolution pipeline /// and await their individual results. /// </summary> /// <param name="context">The "parent" context supplying data for downstream reslts.</param> /// <param name="cancelToken">The cancel token.</param> /// <returns>Task.</returns> private async Task ProcessDownStreamFieldContexts(GraphFieldExecutionContext context, CancellationToken cancelToken) { if (context.InvocationContext.ChildContexts.Count == 0) { return; } // items resolved on this active context become the source for any downstream fields // --- // can never extract child fields from a null value (even if its valid for the item) // or one that isnt read for it List <GraphDataItem> allSourceItems = context .ResolvedSourceItems .SelectMany(x => x.FlattenListItemTree()) .Where(x => x.ResultData != null && x.Status == FieldItemResolutionStatus.NeedsChildResolution) .ToList(); if (allSourceItems.Count == 0) { return; } // find a reference to the graph type for the field var graphType = _schema.KnownTypes.FindGraphType(context.Field.TypeExpression.TypeName); // theoretically it can't not be found, but you never know if (graphType == null) { var msg = $"Internal Server Error. When processing the results of '{context.Field.Route.Path}' no graph type on the target schema " + $"could be found for the type name '{context.Field.TypeExpression.TypeName}'. " + $"Unable to process the {allSourceItems.Count} item(s) generated."; context.Messages.Add( GraphMessageSeverity.Critical, Constants.ErrorCodes.EXECUTION_ERROR, msg, context.Request.Origin); context.Cancel(); return; } var pipelines = new List <Task>(); // Step 0 // ----------------------------------------------------------------------- // create a lookup of source items by concrete type known to the schema, for easy seperation to the individual // downstream child contexts var sourceItemLookup = this.MapExpectedConcreteTypeFromSourceItem(allSourceItems, graphType); foreach (var childInvocationContext in context.InvocationContext.ChildContexts) { // Step 1 // ---------------------------- // figure out which child items need to be processed through it IEnumerable <GraphDataItem> sourceItemsToInclude; if (childInvocationContext.ExpectedSourceType == null) { sourceItemsToInclude = allSourceItems; } else { // if no children match the required type of the children present, then skip it // this can happen quite often in the case of a union or an interface where multiple invocation contexts // are added to a plan for the same child field in case a parent returns a member of the union or an // implementer of the interface if (!sourceItemLookup.ContainsKey(childInvocationContext.ExpectedSourceType)) { continue; } sourceItemsToInclude = sourceItemLookup[childInvocationContext.ExpectedSourceType]; } // Step 1B // For any source items replace any virtual objects with defaults found // on the context for the field in question if (context.DefaultFieldSources.TryRetrieveSource(childInvocationContext.Field, out var defaultSource)) { sourceItemsToInclude = sourceItemsToInclude.Select((currentValue) => { if (currentValue.ResultData is VirtualResolvedObject) { currentValue.AssignResult(defaultSource); } return(currentValue); }); } // Step 2 // ---------------------------- // when the invocation is as a batch, create one execution context for all children // when its "per source" create a context for each child individually IEnumerable <GraphFieldExecutionContext> childContexts = this.CreateChildExecutionContexts( context, childInvocationContext, sourceItemsToInclude); // Step 3 // -------------------- // Fire off the contexts through the pipeline foreach (var childContext in childContexts) { var task = _fieldExecutionPipeline.InvokeAsync(childContext, cancelToken) .ContinueWith(invokeTask => { this.CaptureChildFieldExecutionResults(context, childContext, invokeTask); }); pipelines.Add(task); if (_awaitEachPipeline) { await task.ConfigureAwait(false); } } } // wait for every pipeline to finish await Task.WhenAll(pipelines).ConfigureAwait(false); // reawait to allow for unwrapping and throwing of internal exceptions if (!_awaitEachPipeline) { foreach (var task in pipelines.Where(x => x.IsFaulted)) { await task.ConfigureAwait(false); } } }
/// <summary> /// For any resolved, non-leaf items assigned to the result, pass each through the resolution pipeline /// and await their individual results. /// </summary> /// <param name="context">The "parent" context supplying data for downstream reslts.</param> /// <param name="cancelToken">The cancel token.</param> /// <returns>Task.</returns> private async Task ProcessDownStreamFieldContexts(GraphFieldExecutionContext context, CancellationToken cancelToken) { if (context.InvocationContext.ChildContexts.Count == 0) { return; } var pipelines = new List <Task>(); // items resolved on this active context become the source for any downstream fields // --- // can never extract child fields from a null value (even if its valid for the item) // or one that isnt read for it IEnumerable <GraphDataItem> allSourceItems = context .ResolvedSourceItems .SelectMany(x => x.FlattenListItemTree()) .Where(x => x.ResultData != null && x.Status == FieldItemResolutionStatus.NeedsChildResolution); if (!allSourceItems.Any()) { return; } // create a lookup of source item by result type for easy seperation to the individual // downstream child contexts var sourceItemLookup = allSourceItems.ToLookup(x => x.ResultData.GetType()); IEnumerable <GraphFieldExecutionContext> childContexts = null; foreach (var childInvocationContext in context.InvocationContext.ChildContexts) { // Step 1 // ---------------------------- // figure out which child items need to be processed through it IEnumerable <GraphDataItem> sourceItemsToInclude; if (childInvocationContext.ExpectedSourceType == null) { sourceItemsToInclude = allSourceItems; } else { // if no children match the required type of the children present, then skip it // this can happen quite often in the case of a union or an interface where multiple invocation contexts // are added to a plan for the same child field in case a parent returns a member of the union or an // implementer of the interface if (!sourceItemLookup.Contains(childInvocationContext.ExpectedSourceType)) { continue; } sourceItemsToInclude = sourceItemLookup[childInvocationContext.ExpectedSourceType]; } // Step 2 // ---------------------------- // when the invocation is as a batch, create one execution context for all children // when its "per source" create a context for each child individually childContexts = this.CreateChildExecutionContexts(context, childInvocationContext, sourceItemsToInclude); // Step 3 // -------------------- // Fire off the contexts through the pipeline foreach (var childContext in childContexts) { var task = _fieldExecutionPipeline.InvokeAsync(childContext, cancelToken) .ContinueWith(invokeTask => { context.Messages.AddRange(childContext.Messages); }); pipelines.Add(task); if (_awaitEachPipeline) { await task.ConfigureAwait(false); } } } // wait for every pipeline to finish await Task.WhenAll(pipelines).ConfigureAwait(false); // reawait to allow for unwrapping and throwing of internal exceptions if (!_awaitEachPipeline) { foreach (var task in pipelines.Where(x => x.IsFaulted)) { await task.ConfigureAwait(false); } } }