Skip to content

Commit

Permalink
[ODS-5764] Profile usage is not enforced (v5.1) (#672) (#676)
Browse files Browse the repository at this point in the history
* Initial commit of all perceived changes needed for Profiles enforcement.

* Fixed bug with GetById action method not returning the application/vnd.ed-fi. content types for Profile-based requests.

* Updated profile enforcement filter to match the needs of the v5.1.0 architecture (sans Dynamic Profiles), also adding some needed checks that are implemented as part of the v7.0 mapping contract logic.

* Added lazy loading to the newly added GetByApiCollectionName method to ensure that the source dictionary has been fully initialized before the URI-based lookup dictionary is prepared.

* using statement usage update for older version of C# language, and associated whitespace changes.

* Remove commented out code, and fix whitespace/indentation issues.

* Modified status and error message associated with GET requests using a writable content type (and vice-versa) to match behavior of pre-5.1.0 behavior.

* Changes for alignment with v5.1.1 Postman Profiles integration tests.

* Profiles boilerplate removal due to Postman export format update.

* Remove all access token usage from tests that don't cover assigned profile functionality.

* Fixed bad URLs in requests (removing extra slash).

* Alignment of test expectations to actual behavior with Profile enforcement fixes.

* Remove redundant Npgsql package reference.

* Remove redundant NUnit package reference.

* Removed unused access token for non-existing API client.

* Changed conditional check for Profiles feature to use literal true rather then string-value of "true".

* Provide a fix for the missing dependency for the EnforceAssignedProfileUsageFilter class when the Profiles feature is not enabled.

* Updated Postman test failing based on case sensitive string comparison to use RegExp class for case-insensitive comparison.

Co-authored-by: Geoff McElhanon <[email protected]>
  • Loading branch information
semalaiappan and gmcelhanon authored Mar 15, 2023
1 parent ef15092 commit f4f3174
Show file tree
Hide file tree
Showing 27 changed files with 1,071 additions and 908 deletions.
5 changes: 5 additions & 0 deletions Application/EdFi.Ods.Api/Constants/RouteConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,10 @@ public static string InstanceIdFromRoute
{
get => @"{instanceIdFromRoute:regex(^[[A-Za-z0-9-]]+$)}/";
}

public static string InstanceIdFromRouteForFilter
{
get => @"{instanceIdFromRoute:regex(^[A-Za-z0-9-]+$)}/";
}
}
}
16 changes: 16 additions & 0 deletions Application/EdFi.Ods.Api/Container/Modules/ApplicationModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
using EdFi.Ods.Common.Infrastructure.Extensibility;
using EdFi.Ods.Common.Infrastructure.Pipelines;
using EdFi.Ods.Common.IO;
using EdFi.Ods.Common.Metadata;
using EdFi.Ods.Common.Models;
using EdFi.Ods.Common.Models.Domain;
using EdFi.Ods.Common.Models.Resource;
Expand All @@ -54,6 +55,17 @@ protected override void Load(ContainerBuilder builder)
.As<IFilterMetadata>()
.SingleInstance();

builder.RegisterType<EnforceAssignedProfileUsageFilter>()
.SingleInstance();

builder.RegisterType<NullProfileMetadataProvider>()
.As<IProfileMetadataProvider>()
.SingleInstance();

builder.RegisterType<DataManagementRequestContextFilter>()
.As<IFilterMetadata>()
.SingleInstance();

builder.RegisterType<EnterpriseApiVersionProvider>()
.As<IApiVersionProvider>()
.SingleInstance();
Expand Down Expand Up @@ -234,6 +246,10 @@ protected override void Load(ContainerBuilder builder)
.PreserveExistingDefaults()
.SingleInstance();

builder.RegisterGeneric(typeof(ContextProvider<>))
.As(typeof(IContextProvider<>))
.SingleInstance();

RegisterPipeLineStepProviders();
RegisterModels();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ private IActionResult CreateActionResultFromException(
protected virtual string GetReadContentType() => MediaTypeNames.Application.Json;

[HttpGet]
[ServiceFilter(typeof(EnforceAssignedProfileUsageFilter), IsReusable = true)]
public virtual async Task<IActionResult> GetAll(
[FromQuery] UrlQueryParametersRequest urlQueryParametersRequest,
[FromQuery] TGetByExampleRequest request = default(TGetByExampleRequest))
Expand Down Expand Up @@ -215,6 +216,7 @@ public virtual async Task<IActionResult> GetAll(
}

[HttpGet("{id}")]
[ServiceFilter(typeof(EnforceAssignedProfileUsageFilter), IsReusable = true)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status412PreconditionFailed)]
public virtual async Task<IActionResult> Get(Guid id)
Expand All @@ -237,11 +239,14 @@ public virtual async Task<IActionResult> Get(Guid id)
// Add ETag header for the resource
Response.GetTypedHeaders().ETag = GetEtag(result.Resource.ETag);

Response.GetTypedHeaders().ContentType = new MediaTypeHeaderValue(GetReadContentType());

return Ok(result.Resource);
}

[CheckModelForNull]
[HttpPut("{id}")]
[ServiceFilter(typeof(EnforceAssignedProfileUsageFilter), IsReusable = true)]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
Expand Down Expand Up @@ -286,6 +291,7 @@ public virtual async Task<IActionResult> Put([FromBody] TPutRequest request, Gui

[CheckModelForNull]
[HttpPost]
[ServiceFilter(typeof(EnforceAssignedProfileUsageFilter), IsReusable = true)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
Expand Down
146 changes: 146 additions & 0 deletions Application/EdFi.Ods.Api/Filters/DataManagementRequestContextFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// SPDX-License-Identifier: Apache-2.0
// Licensed to the Ed-Fi Alliance under one or more agreements.
// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
// See the LICENSE and NOTICES files in the project root for more information.

using System;
using System.Linq;
using System.Threading.Tasks;
using EdFi.Common.Configuration;
using EdFi.Ods.Api.Constants;
using EdFi.Ods.Common.Configuration;
using EdFi.Ods.Common.Context;
using EdFi.Ods.Common.Extensions;
using EdFi.Ods.Common.Models;
using EdFi.Ods.Common.Models.Resource;
using EdFi.Ods.Common.Security.Claims;
using log4net;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Primitives;

namespace EdFi.Ods.Api.Filters
{
/// <summary>
/// A resource filter that inspects the action descriptor's AttributeRouteInfo to locate
/// the <see cref="Resource" /> associated with the current data management API request.
/// </summary>
public class DataManagementRequestContextFilter : IAsyncResourceFilter
{
private readonly IContextProvider<DataManagementResourceContext> _contextProvider;
private readonly ApiSettings _apiSettings;

private readonly ILog _logger = LogManager.GetLogger(typeof(DataManagementRequestContextFilter));
private readonly IResourceModelProvider _resourceModelProvider;

private readonly Lazy<string> _templatePrefix;
private readonly Lazy<string[]> _knownSchemaUriSegments;

public DataManagementRequestContextFilter(
IResourceModelProvider resourceModelProvider,
IContextProvider<DataManagementResourceContext> contextProvider,
ApiSettings apiSettings)
{
_resourceModelProvider = resourceModelProvider;
_contextProvider = contextProvider;

_knownSchemaUriSegments = new Lazy<string[]>(
() => _resourceModelProvider.GetResourceModel()
.SchemaNameMapProvider.GetSchemaNameMaps()
.Select(m => m.UriSegment)
.ToArray());

_apiSettings = apiSettings;
_templatePrefix = new Lazy<string>(GetTemplatePrefix);
}

public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next)
{
var attributeRouteInfo = context.ActionDescriptor.AttributeRouteInfo;

if (attributeRouteInfo != null)
{
string template = attributeRouteInfo.Template;

// e.g. data/v3/ed-fi/gradebookEntries

// Is this a data management route?
if (template?.StartsWith(_templatePrefix.Value) ?? false)
{
var templateSegment = new StringSegment(template);

var parts = templateSegment.Subsegment(_templatePrefix.Value.Length).Split(new[]{'/'});
using var partsEnumerator = parts.GetEnumerator();
partsEnumerator.MoveNext();

string schema, resourceCollection;

// If the schema segment is a templated route value...
if (partsEnumerator.Current == "{schema}")
{
if (!context.RouteData.Values.TryGetValue("schema", out object schemaAsObject)
|| !context.RouteData.Values.TryGetValue("resource", out object resourceAsObject))
{
await next();

return;
}

schema = (string) schemaAsObject;
resourceCollection = (string) resourceAsObject;
}
else
{
// If this is NOT a known schema URI segment...
if (!_knownSchemaUriSegments.Value.Any(s => partsEnumerator.Current.Equals(s)))
{
await next();

return;
}

schema = partsEnumerator.Current.Value;

partsEnumerator.MoveNext();
resourceCollection = partsEnumerator.Current.Value;
}

// Find and capture the associated resource to context
try
{
var resource = _resourceModelProvider.GetResourceModel()
.GetResourceByApiCollectionName(schema, resourceCollection);

_contextProvider.Set(new DataManagementResourceContext(resource));
}
catch (Exception)
{
_logger.Debug(
$"Unable to find resource based on route template value '{template.Substring(RouteConstants.DataManagementRoutePrefix.Length + 1)}'...");
}
}
}

await next();
}

public void OnActionExecuted(ActionExecutedContext context) { }

private string GetTemplatePrefix()
{
string template = $"{RouteConstants.DataManagementRoutePrefix}/";

if (_apiSettings.GetApiMode() == ApiMode.YearSpecific)
{
template += RouteConstants.SchoolYearFromRoute;
}

if (_apiSettings.GetApiMode() == ApiMode.InstanceYearSpecific)
{
template += RouteConstants.InstanceIdFromRouteForFilter;
template += RouteConstants.SchoolYearFromRoute;
}

return template;
}
}
}
Loading

0 comments on commit f4f3174

Please sign in to comment.