/// <summary> /// Returns the OpenAPI representation of the given <see cref="ApiDataModel" />. /// </summary> /// <param name="httpContext">The <see cref="HttpContext" />.</param> /// <param name="openApiDocument">The <see cref="OpenApiDocument" />, as built by the <see cref="OpenApiDocumentBuilder" />.</param> /// <param name="options">The options to configure the OpenAPI document.</param> /// <returns>An OpenAPI representation.</returns> public PlainTextResult Invoke( HttpContext httpContext, OpenApiDocument openApiDocument, ApiOperationContext context, IOptions <OpenApiOptions> options) { var openApiOptions = options.Value; // If the document does not have any servers defined explicitly by client application we will push // a default one that is the currently running server if (openApiDocument.Servers.Count == 0) { var baseUri = httpContext.GetBlueprintBaseUri(); openApiDocument.Servers.Add(new OpenApiServer { Url = baseUri, }); } var httpRequest = httpContext.Request; // If we believe this is a hit from a browser then serve up the documentation using Refit. This can be overriden // by passing a json query string to force the JSON response. if (httpRequest.Headers["Accept"].ToString().Contains("text/html") && httpRequest.Query.ContainsKey("json") == false) { var baseUri = httpContext.GetBlueprintBaseUri(); var refitHtmlDocument = @$ "<!DOCTYPE html> <html> <head> <title>{openApiDocument.Info.Title}</title>
public ValueTask <object> Handle(IncrementCountCommand operation, ApiOperationContext apiOperationContext) { if (operation.Max != -1 && counter >= operation.Max) { logger.LogWarning("Reached max count"); return(default);
/// <summary> /// Pushes the given <see cref="IBackgroundTask" /> that has been wrapped in an <see cref="BackgroundTaskEnvelope" /> /// to a <see cref="IApiOperationExecutor" />. /// </summary> /// <param name="taskEnvelope">The task to be executed.</param> /// <param name="configureSpan">An action that will be called with an <see cref="IApmSpan" /> for the service-specific /// provider to add tags.</param> /// <param name="token">A cancellation token that indicates this method should be aborted.</param> /// <returns>A <see cref="Task" /> representing the execution of the given task.</returns> public async Task Execute( BackgroundTaskEnvelope taskEnvelope, Action <IApmSpan> configureSpan, CancellationToken token) { Guard.NotNull(nameof(taskEnvelope), taskEnvelope); using var nestedContainer = this._rootServiceProvider.CreateScope(); var apiContext = new ApiOperationContext( nestedContainer.ServiceProvider, this._apiOperationExecutor.DataModel, taskEnvelope.Task, token); using var span = this._apmTool.StartOperation( apiContext.Descriptor, SpanKinds.Consumer, taskEnvelope.ApmContext); configureSpan(span); apiContext.ApmSpan = span; var result = await this._apiOperationExecutor.ExecuteAsync(apiContext); if (result is UnhandledExceptionOperationResult unhandledExceptionOperationResult) { span.RecordException(unhandledExceptionOperationResult.Exception); unhandledExceptionOperationResult.Rethrow(); } }
public OkResult Handle(ApiOperationContext context, IDependency dependency) { Context = context; Dependency = dependency; return(new OkResult(nameof(InlineHandle))); }
private static async Task PopulateResourceEventData( IResourceEventRepository resourceEventRepository, ApiOperationContext context, ResourceEvent resourceEvent) { // Get the latest after creation or update (cannot, obviously, get for a deleted record) if (resourceEvent.ChangeType != ResourceEventChangeType.Deleted) { resourceEvent.Data = await GetByIdAsync(context, resourceEvent.SelfQuery); } if (resourceEvent.ChangeType == ResourceEventChangeType.Updated) { var previousResource = await resourceEventRepository.GetCurrentDataAsync(resourceEvent.Href, resourceEvent.ResourceType); // There are cases where the old resource will not exist because resources being saved was a new // introduction after being in prod for over a year if (previousResource != null) { resourceEvent.ChangedValues = ObjectComparer.GetPreviousValues( previousResource, resourceEvent.Data); } } }
public OkResult Handle(ApiOperationContext context, IDependency dependency, IUserAuthorisationContext user) { Context = context; Dependency = dependency; User = user; return(new OkResult(nameof(InlineHandle))); }
/// <inheritdoc /> public Task ExecuteAsync(ApiOperationContext context, ValidationFailedOperationResult result) { var validationProblemDetails = new ValidationProblemDetails(result.Errors); return(this._okResultOperationExecutor.WriteContentAsync( context.GetHttpContext(), validationProblemDetails.Status.Value, validationProblemDetails)); }
public static void WriteSuccess(IAuditor auditor, ApiOperationContext context) { auditor.Write(new AuditItem( Activity.Current?.Id ?? "no-activity-id", true, "Success", GetUserId(context), context.Operation)); }
public static void WriteFailure(IAuditor auditor, ApiOperationContext context, Exception e) { auditor.Write(new AuditItem( Activity.Current?.Id ?? "no-activity-id", false, e.Message, GetUserId(context), context.Operation)); }
public async Task RouteAsync(HttpContext httpContext) { var endpoint = httpContext.GetEndpoint(); var routeData = httpContext.GetRouteData(); var httpRequest = httpContext.Request; var operation = endpoint.Metadata.GetMetadata <ApiOperationDescriptor>(); // If we have an activity set the DisplayName to the operation type if (Activity.Current != null) { Activity.Current.DisplayName = operation.Name; } var httpFeatureData = operation.GetFeatureData <HttpOperationFeatureData>(); if (httpFeatureData.HttpMethod != httpRequest.Method) { this._logger.LogInformation( "Request {Method} {Url} does not match required HTTP method {RequiredMethod}", httpRequest.Method, httpRequest.GetDisplayUrl(), httpFeatureData.HttpMethod); httpContext.Response.StatusCode = (int)HttpStatusCode.MethodNotAllowed; return; } using var nestedContainer = this._rootServiceProvider.CreateScope(); var apiContext = new ApiOperationContext( nestedContainer.ServiceProvider, this._apiOperationExecutor.DataModel, operation, httpContext.RequestAborted) { ClaimsIdentity = httpContext.User.Identity as ClaimsIdentity, }; apiContext.SetHttpFeatureContext(new HttpFeatureContext { HttpContext = httpContext, RouteData = routeData, }); var request = httpContext.Request; var baseUri = $"{request.Scheme}://{this._httpOptions.Value.PublicHost ?? request.Host.Value}{request.PathBase}/{this._basePath}"; httpContext.SetBaseUri(baseUri); var result = await this._apiOperationExecutor.ExecuteAsync(apiContext); // We want to immediately execute the result to allow it to write to the HTTP response await result.ExecuteAsync(apiContext); }
public static async Task HandleAsync( IResourceEventRepository resourceEventRepository, IApiLinkGenerator apiLinkGenerator, ApiOperationContext context, OperationResult result) { if (result is OkResult okResult) { var innerResult = okResult.Content; if (innerResult is ResourceEvent resourceEvent) { var logger = context.ServiceProvider.GetRequiredService <ILogger <ResourceEventHandlerMiddlewareBuilder> >(); void AddMetadata(string k, object v) => resourceEvent.Metadata[k] = v; context.UserAuthorisationContext?.PopulateMetadata(AddMetadata); resourceEvent.CorrelationId = context.Activity?.Id; resourceEvent.Operation = context.Operation; var metadataProviders = context.ServiceProvider.GetServices <IContextMetadataProvider>(); foreach (var p in metadataProviders) { await p.PopulateMetadataAsync(context, AddMetadata); } // If we do not already have Data use the "SelfQuery" to populate using a nested query if possible if (resourceEvent.Data == null && resourceEvent.SelfQuery != null) { await TryPopulateResourceEventDataAsync(context, resourceEvent); } var selfLink = context.DataModel.GetLinkFor(resourceEvent.ResourceType, "self"); if (selfLink == null) { logger.LogWarning( "No self link exists. Href property of the resource event of type {ResourceType} will not be populated", resourceEvent.ResourceType); } else { resourceEvent.Href = apiLinkGenerator.CreateUrl(selfLink, resourceEvent.Data ?? resourceEvent.SelfQuery); } if (resourceEvent.Data != null) { await TryPopulateChangedValuesAsync(resourceEventRepository, resourceEvent); } await resourceEventRepository.AddAsync(resourceEvent); } } }
private static async Task TryPopulateResourceEventDataAsync( ApiOperationContext context, ResourceEvent resourceEvent) { // Get the latest after creation or update (cannot, obviously, get for a deleted record) if (resourceEvent.ChangeType != ResourceEventChangeType.Deleted) { resourceEvent.Data = await GetByIdAsync(context, resourceEvent.SelfQuery); } }
private static async ValueTask <object> AddResourceLinksAsync( ILogger <LinkGeneratorMiddlewareBuilder> logger, IApiLinkGenerator apiLinkGenerator, IEnumerable <IResourceLinkGenerator> generators, ApiOperationContext context, object resource) { if (resource is ILinkableResource linkableResource) { await AddLinksAsync(logger, apiLinkGenerator, generators, context, linkableResource); } var enumerableResult = resource as IEnumerable <object>; if (resource is IPagedApiResource pagedResult) { enumerableResult = pagedResult.GetEnumerable(); } if (enumerableResult != null) { if (enumerableResult is IQueryable <object> ) { // We need to ensure we are now dealing with a non-deferred result, else the // links will be added, but the deferred result is still returned and so // changes are lost. // // This needs to be what we then actually return from the middleware after the // links have been added enumerableResult = enumerableResult.ToList(); resource = enumerableResult; } foreach (var obj in enumerableResult) { if (obj is ILinkableResource apiResourceItem) { await AddLinksAsync(logger, apiLinkGenerator, generators, context, apiResourceItem); } else { // If we cannot add any links because not a `LinkableResource` then break early // as we assume all entries are the same type if (logger.IsEnabled(LogLevel.Trace)) { logger.LogTrace("Resource type is not a LinkableResource. resource_type={0}", obj.GetType().Name); } break; } } } return(resource); }
/// <summary> /// Creates a new instance of <see cref="BodyParserContext"/>. /// </summary> /// <param name="operationContext">The operation context.</param> /// <param name="httpContext"> /// The <see cref="Microsoft.AspNetCore.Http.HttpContext"/> for the current operation. /// </param> /// <param name="instance">The instance that needs to be populated.</param> /// <param name="bodyType">The type the body should be read as.</param> public BodyParserContext( ApiOperationContext operationContext, HttpContext httpContext, object instance, Type bodyType) { this.OperationContext = operationContext ?? throw new ArgumentNullException(nameof(httpContext)); this.HttpContext = httpContext ?? throw new ArgumentNullException(nameof(httpContext)); this.Instance = instance ?? throw new ArgumentNullException(nameof(instance)); this.BodyType = bodyType ?? throw new ArgumentNullException(nameof(bodyType)); }
/// <inheritdoc /> public async Task <OperationResult> ExecuteAsync(ApiOperationContext context) { var result = await this._executor.ExecuteAsync(context); if (result is UnhandledExceptionOperationResult e) { e.Rethrow(); } return(result); }
/// <summary> /// Gets the <see cref="RouteContext" /> that has been registered with this <see cref="ApiOperationContext" />. /// </summary> /// <param name="context">The context to load from.</param> /// <returns>The <see cref="RouteContext"/> registered.</returns> /// <exception cref="InvalidOperationException">If no <see cref="RouteContext"/> has been registered.</exception> /// <seealso cref="SetHttpFeatureContext" /> public static HttpFeatureContext GetHttpFeatureContext(this ApiOperationContext context) { if (context.Data.TryGetValue(nameof(HttpFeatureContext), out var value)) { return((HttpFeatureContext)value); } throw new InvalidOperationException( $"A HttpFeatureContext instance does not exist on this {nameof(ApiOperationContext)}. To use HTTP-specific features a HttpContext must exist, which " + $"can be accomplished by using {nameof(EndpointRouteBuilderExtensions.MapBlueprintApi)} in your ASP.NET Core web application"); }
public static async Task AddLinksAsync( IApiLinkGenerator apiLinkGenerator, IEnumerable <IResourceLinkGenerator> registeredGenerators, ApiOperationContext context, OperationResult result) { if (result is OkResult okResult) { var logger = context.ServiceProvider.GetRequiredService <ILogger <LinkGeneratorMiddlewareBuilder> >(); okResult.Content = await AddResourceLinksAsync(logger, apiLinkGenerator, registeredGenerators, context, okResult.Content); } }
public ValueTask <object> Handle(T operation, ApiOperationContext apiOperationContext) { WasCalled = true; OperationPassed = operation; ContextPassed = apiOperationContext; if (ToThrow != null) { throw ToThrow; } return(new ValueTask <object>(ResultToReturn)); }
private static Task <ExecutionAllowed> IsAuthorisedAsync(ApiOperationContext operationContext) { if (operationContext.UserAuthorisationContext == null) { return(_userUnauthenticated); } if (!operationContext.UserAuthorisationContext.IsActive) { return(_userInactive); } return(ExecutionAllowed.YesTask); }
public static async Task HandleAsync( IResourceEventRepository resourceEventRepository, IApiLinkGenerator apiLinkGenerator, ApiOperationContext context, OperationResult result) { if (result is OkResult okResult) { var innerResult = okResult.Content; if (innerResult is ResourceEvent resourceEvent) { var logger = context.ServiceProvider.GetRequiredService <ILogger <ResourceEventHandlerMiddlewareBuilder> >(); logger.LogDebug("ResourceEvent found. Loading resource. resource_type={0}", resourceEvent.ResourceType); void AddMetadata(string k, object v) => resourceEvent.Metadata[k] = v; context.UserAuthorisationContext?.PopulateMetadata(AddMetadata); resourceEvent.CorrelationId = Activity.Current?.Id; resourceEvent.Operation = context.Operation; var metadataProviders = context.ServiceProvider.GetServices <IContextMetadataProvider>(); foreach (var p in metadataProviders) { await p.PopulateMetadataAsync(context, AddMetadata); } var selfLink = context.DataModel.GetLinkFor(resourceEvent.ResourceType, "self"); if (selfLink == null) { logger.LogWarning( "No self link exists. Link and payload will not be populated. resource_type={0}", resourceEvent.ResourceType.Name); return; } resourceEvent.Href = apiLinkGenerator.CreateUrl(selfLink, resourceEvent.SelfQuery); await PopulateResourceEventData(resourceEventRepository, context, resourceEvent); await resourceEventRepository.AddAsync(resourceEvent); } } }
/// <inheritdoc /> public Task PopulateMetadataAsync(ApiOperationContext context, Action <string, object> add) { var request = context.GetHttpContext().Request; add("IpAddress", request.GetClientIpAddress()); if (request.Headers.ContainsKey("User-Agent")) { add( "UserAgent", string.Join(" ", request.Headers["User-Agent"].ToString())); } return(Task.CompletedTask); }
/// <inheritdoc /> public override async Task ExecuteAsync(ApiOperationContext context) { await base.ExecuteAsync(context); var httpContext = context.GetHttpContext(); var response = httpContext.Response; response.ContentType = this.ContentType; using var httpResponseStreamWriter = new HttpResponseStreamWriter(response.Body, Encoding.UTF8); await httpResponseStreamWriter.WriteAsync(this._content); await httpResponseStreamWriter.FlushAsync(); }
public Task AddClassValidationResultsAsync(object value, ApiOperationContext apiOperationContext, ValidationFailures results) { var validationContext = new ValidationContext(value, null, null); if (value is IValidatableObject validatableObject) { var validationResults = validatableObject.Validate(validationContext); foreach (var validationResult in validationResults) { results.AddFailure(validationResult); } } return(Task.CompletedTask); }
/// <inheritdoc /> public Task ExecuteAsync(ApiOperationContext context, UnhandledExceptionOperationResult result) { var httpContext = context.GetHttpContext(); var problemDetails = this.ToProblemDetails(result.Exception); if (context.Activity != null) { problemDetails.AddExtension("traceId", context.Activity?.TraceId.ToString()); problemDetails.AddExtension("traceParentId", context.Activity?.ParentId); } return(this._okResultOperationExecutor.WriteContentAsync( httpContext, problemDetails.Status, problemDetails)); }
private static async ValueTask AddLinksAsync( ILogger logger, IApiLinkGenerator apiLinkGenerator, IEnumerable <IResourceLinkGenerator> generators, ApiOperationContext context, ILinkableResource result) { foreach (var resourceLinkGenerator in generators) { if (logger.IsEnabled(LogLevel.Trace)) { logger.LogTrace("Generating links. generator={0} result_type={1}", resourceLinkGenerator.GetType().Name, result.GetType().Name); } await resourceLinkGenerator.AddLinksAsync(apiLinkGenerator, context, result); } }
/// <inheritdoc /> public Task ExecuteAsync(ApiOperationContext context, UnhandledExceptionOperationResult result) { var httpContext = context.GetHttpContext(); var problemDetails = this.ToProblemDetails(result.Exception); var traceId = context.ApmSpan?.TraceId; if (traceId != null) { problemDetails.AddExtension("traceId", traceId); } return(this._okResultOperationExecutor.WriteContentAsync( httpContext, problemDetails.Status.Value, problemDetails)); }
/// <summary> /// Sets the HTTP status code and adds any custom headers that have been added to <see cref="Headers" />. /// </summary> /// <param name="context">The context of the operation this result is for.</param> /// <returns>A <see cref="Task" /> representing the execution of this method.</returns> public override Task ExecuteAsync(ApiOperationContext context) { var httpContext = context.GetHttpContext(); var response = httpContext.Response; response.StatusCode = (int)this.StatusCode; if (this.Headers != null) { foreach (var h in this.Headers) { response.Headers.Add(h); } } return(Task.CompletedTask); }
private Task <ExecutionAllowed> IsAuthorisedAsync(ApiOperationContext operationContext, ApiOperationDescriptor descriptor, object resource) { if (operationContext.UserAuthorisationContext == null) { return(_userUnauthenticated); } if (!operationContext.UserAuthorisationContext.IsActive) { return(_userInactive); } if (!(operationContext.UserAuthorisationContext is IClaimsHolder userClaims)) { throw new InvalidOperationException($"Cannot apply {nameof(ClaimsRequiredApiAuthoriser)} to a user context that does not implement {nameof(IClaimsHolder)}"); } var claimRequiredAttribute = descriptor.TypeAttributes.OfType <ClaimRequiredAttribute>().Single(); var expansionState = ClaimExpansionState.RequiresExpansion; var requiredClaim = claimRequiredAttribute.GetClaim(resource); // Pre-expand key if it is a match var apiResource = resource as IHaveResourceKey; var expandedKey = apiResource?.ResourceKey; if (expandedKey != null && expandedKey.EndsWith(requiredClaim.Value)) { requiredClaim = new Claim(requiredClaim.Type, expandedKey, requiredClaim.ValueType); expansionState = ClaimExpansionState.AlreadyExpanded; } var result = this._claimInspector.IsDemandedClaimFulfilled(userClaims, requiredClaim, expansionState); if (!result) { return(Task.FromResult(ExecutionAllowed.No( $"User does not have required claim {requiredClaim.Type} {requiredClaim.ValueType} for {requiredClaim.Value}", "You do not have enough permissions to perform this action", ExecutionAllowedFailureType.Authorisation))); } return(ExecutionAllowed.YesTask); }
public async ValueTask <ExecutionAllowed> CanShowLinkAsync(ApiOperationContext operationContext, ApiOperationDescriptor descriptor, [CanBeNull] object resource) { var traceLogEnabled = this._logger.IsEnabled(LogLevel.Trace); foreach (var checker in this._apiAuthorisers) { if (!checker.AppliesTo(descriptor)) { continue; } var result = await checker.CanShowLinkAsync(operationContext, descriptor, resource); if (result.IsAllowed == false) { // Base links could have many, potentially hundreds, of failures, which are completely // normal, we will not log unless enabled if (traceLogEnabled) { this._logger.LogTrace( "Link check failed. type={0} resource_type={1} reason={2} authoriser={3}", descriptor.OperationType.Name, resource?.GetType().Name, result.Reason, checker.GetType()); } return(result); } } if (traceLogEnabled) { this._logger.LogTrace( "Link check succeeded. type={0} resource_type={1}", descriptor.OperationType.Name, resource?.GetType().Name); } return(ExecutionAllowed.Yes); }
/// <summary> /// Configures the <see cref="ApiOperationContext" /> with a HTTP context configured for the given URL. /// </summary> /// <remarks> /// In addition to adding the <see cref="RouteContext"/> on the descriptor this method will set the newly /// created <see cref="HttpContext" /> on the <see cref="HttpContextAccessor" /> that has been registered /// with the context's <see cref="IServiceProvider"/>. /// </remarks> /// <param name="context">The context to configure.</param> /// <param name="url">The URL to set for this context's request.</param> public static HttpContext ConfigureHttp(this ApiOperationContext context, string url) { var httpContext = new DefaultHttpContext(); httpContext.Connection.RemoteIpAddress = IPAddress.Loopback; httpContext.SetRequestUri(url); httpContext.Request.Method = context.Descriptor.GetFeatureData <HttpOperationFeatureData>().HttpMethod; httpContext.Request.Headers["Content-Type"] = "application/test-data"; context.SetHttpFeatureContext(new HttpFeatureContext { HttpContext = httpContext, RouteData = new RouteData(), }); httpContext.SetBaseUri("https://api.blueprint-testing.com/api/"); context.ServiceProvider.GetRequiredService <IHttpContextAccessor>().HttpContext = httpContext; return(httpContext); }