Skip to content

Commit

Permalink
[ODS-5764] Profile usage is not enforced (v6.1) (#680)
Browse files Browse the repository at this point in the history
* [ODS-5764] Profile usage is not enforced (v5.1) (#672) (#676)

* 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]>

* IContextProvider<DataManagementResourceContext> dataManagementRequestContextProvider updated and 2 Profie Test CodeFiz

* ODS-5612 branch Ed-Fi ODS-API Profile Test Suite.postman_collection

* removed the Extensions tests from the Profiles test suite and Validation Message codefix

---------

Co-authored-by: Geoff McElhanon <[email protected]>
  • Loading branch information
semalaiappan and gmcelhanon authored Mar 18, 2023
1 parent a580850 commit 9b26411
Show file tree
Hide file tree
Showing 36 changed files with 6,596 additions and 1,858 deletions.
2 changes: 1 addition & 1 deletion Application/EdFi.Ods.Api/Constants/RouteConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public static string InstanceIdFromRoute
{
get => @"{instanceIdFromRoute:regex(^[[A-Za-z0-9-]]+$)}/";
}

public static string InstanceIdFromRouteForFilter
{
get => @"{instanceIdFromRoute:regex(^[A-Za-z0-9-]+$)}/";
Expand Down
20 changes: 16 additions & 4 deletions Application/EdFi.Ods.Api/Container/Modules/ApplicationModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,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 Down Expand Up @@ -59,14 +60,21 @@ protected override void Load(ContainerBuilder builder)
.As<IFilterMetadata>()
.SingleInstance();

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

builder.RegisterType<DataManagementRequestContextProvider>()
.As<IDataManagementRequestContextProvider>()
.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 @@ -279,6 +287,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 @@ -156,6 +156,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 @@ -204,6 +205,7 @@ public virtual async Task<IActionResult> GetAll(
}

[HttpGet("{id:guid}")]
[ServiceFilter(typeof(EnforceAssignedProfileUsageFilter), IsReusable = true)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status412PreconditionFailed)]
public virtual async Task<IActionResult> Get(Guid id)
Expand All @@ -226,11 +228,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 @@ -275,6 +280,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
191 changes: 105 additions & 86 deletions Application/EdFi.Ods.Api/Filters/DataManagementRequestContextFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,123 +5,142 @@

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>
/// An action 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 : IActionFilter
namespace EdFi.Ods.Api.Filters
{
private readonly IDataManagementRequestContextProvider _contextProvider;
private readonly ApiSettings _apiSettings;

private readonly string[] _knownSchemas;

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

public DataManagementRequestContextFilter(
IResourceModelProvider resourceModelProvider,
IDataManagementRequestContextProvider contextProvider,
ApiSettings apiSettings)
/// <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
{
_resourceModelProvider = resourceModelProvider;
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;

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

_contextProvider = contextProvider;
_apiSettings = apiSettings;
}

public void OnActionExecuting(ActionExecutingContext context)
{
var attributeRouteInfo = context.ActionDescriptor.AttributeRouteInfo;
_apiSettings = apiSettings;
_templatePrefix = new Lazy<string>(GetTemplatePrefix);
}

if (attributeRouteInfo != null)
public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next)
{
string template = attributeRouteInfo.Template;
string templatePrefix = GetTemplatePrefix();
var attributeRouteInfo = context.ActionDescriptor.AttributeRouteInfo;

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

// Is this a data management route?
if (template?.StartsWith(templatePrefix) ?? false)
if (attributeRouteInfo != null)
{
var parts = template.Substring(templatePrefix.Length).Split('/');
string template = attributeRouteInfo.Template;

string schema, resourceCollection;

// If the schema segment is a templated route value...
if (parts[0] == "{schema}")
{
if (!context.RouteData.Values.TryGetValue("schema", out object schemaAsObject)
|| !context.RouteData.Values.TryGetValue("resource", out object resourceAsObject))
{
return;
}
// e.g. data/v3/ed-fi/gradebookEntries

schema = (string) schemaAsObject;
resourceCollection = (string) resourceAsObject;
}
else
// Is this a data management route?
if (template?.StartsWith(_templatePrefix.Value) ?? false)
{
// If this is NOT a known schema...
if (!_knownSchemas.Contains(parts[0]))
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}")
{
return;
}
if (!context.RouteData.Values.TryGetValue("schema", out object schemaAsObject)
|| !context.RouteData.Values.TryGetValue("resource", out object resourceAsObject))
{
await next();

schema = parts[0];
resourceCollection = parts[1];
}
return;
}

// Find and capture the associated resource to context
try
{
var resource = _resourceModelProvider.GetResourceModel()
.GetResourceByApiCollectionName(schema, resourceCollection);
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.SetResource(resource);
}
catch (Exception)
{
_logger.Debug(
$"Unable to find resource based on route template value '{template.Substring(RouteConstants.DataManagementRoutePrefix.Length + 1)}'...");
_contextProvider.Set(new DataManagementResourceContext(resource));
}
catch (Exception)
{
_logger.Debug(
$"Unable to find resource based on route template value '{template.Substring(RouteConstants.DataManagementRoutePrefix.Length + 1)}'...");
}
}
}
}
}

public void OnActionExecuted(ActionExecutedContext context) { }
await next();
}

private string GetTemplatePrefix()
{
string template = $"{RouteConstants.DataManagementRoutePrefix}/";
public void OnActionExecuted(ActionExecutedContext context) { }

if (_apiSettings.GetApiMode() == ApiMode.YearSpecific)
private string GetTemplatePrefix()
{
template += RouteConstants.SchoolYearFromRoute;
}
string template = $"{RouteConstants.DataManagementRoutePrefix}/";

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

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

return template;
}
}
}
Loading

0 comments on commit 9b26411

Please sign in to comment.