Skip to content

Commit

Permalink
[ODS-6425] Support extensibility for read request authorization filte…
Browse files Browse the repository at this point in the history
…ring based on custom database views (#1106)
  • Loading branch information
gmcelhanon authored Jul 31, 2024
1 parent 2d3ce56 commit 73a9791
Show file tree
Hide file tree
Showing 91 changed files with 2,752 additions and 1,946 deletions.
15 changes: 5 additions & 10 deletions Application/EdFi.Ods.Api/Container/Modules/ExtensionsModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,27 +64,22 @@ public override void ApplyConfigurationSpecificRegistrations(ContainerBuilder bu

var relationshipContextDataProviderTypes = assembly.GetTypes()
.Where(
t => !t.IsAbstract && typeof(IRelationshipsAuthorizationContextDataProvider<,>).IsAssignableFromGeneric(t))
t => !t.IsAbstract && typeof(IRelationshipsAuthorizationContextDataProvider<>).IsAssignableFromGeneric(t))
.ToList();

var contextDataType = typeof(RelationshipsAuthorizationContextData);
foreach (var providerType in relationshipContextDataProviderTypes)
{
var partiallyClosedInterfaceType =
providerType.GetInterfaces()
.SingleOrDefault(i => i.Name == typeof(IRelationshipsAuthorizationContextDataProvider<,>).Name);
.SingleOrDefault(i => i.Name == typeof(IRelationshipsAuthorizationContextDataProvider<>).Name);

var modelType = partiallyClosedInterfaceType?.GetGenericArguments()[0];

var closedInterfaceType =
typeof(IRelationshipsAuthorizationContextDataProvider<,>)
.MakeGenericType(modelType, contextDataType);
typeof(IRelationshipsAuthorizationContextDataProvider<>)
.MakeGenericType(modelType);

var closedServiceType =
providerType
.MakeGenericType(contextDataType);

builder.RegisterType(closedServiceType).As(closedInterfaceType)
builder.RegisterType(providerType).As(closedInterfaceType)
.SingleInstance();
}
});
Expand Down
2 changes: 1 addition & 1 deletion Application/EdFi.Ods.Api/EdFi.Ods.Api.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<PackageId>EdFi.Suite3.Ods.Api</PackageId>
<PackageLicenseFile>LICENSE.txt</PackageLicenseFile>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using EdFi.Common.Extensions;
using EdFi.Ods.Api.Security.AuthorizationStrategies.NamespaceBased;
using EdFi.Ods.Api.Security.AuthorizationStrategies;
using EdFi.Ods.Api.Security.Claims;
using EdFi.Ods.Common.Exceptions;
using EdFi.Ods.Common.Security.Authorization;
Expand All @@ -23,27 +22,26 @@ public class AuthorizationBasisMetadataSelector : IAuthorizationBasisMetadataSel

private readonly IResourceAuthorizationMetadataProvider _resourceAuthorizationMetadataProvider;
private readonly IClaimSetClaimsProvider _claimSetClaimsProvider;
private readonly Dictionary<string, IAuthorizationStrategy> _authorizationStrategyByName;
private readonly IAuthorizationStrategyProvider[] _authorizationStrategyProviders;

private readonly Lazy<Dictionary<string, int>> _bitValuesByAction;

private const string AuthorizationStrategyNameSuffix = "AuthorizationStrategy";

/// <summary>
/// Initializes a new instance of the <see cref="AuthorizationBasisMetadataSelector"/> class.
/// </summary>
/// <param name="resourceAuthorizationMetadataProvider">The component that will be used to supply the claims/strategies that can be used to authorize the resource.</param>
/// <param name="securityRepository"></param>
/// <param name="authorizationStrategies"></param>
/// <param name="claimSetClaimsProvider"></param>
/// <param name="authorizationStrategyProviders"></param>
public AuthorizationBasisMetadataSelector(
IResourceAuthorizationMetadataProvider resourceAuthorizationMetadataProvider,
ISecurityRepository securityRepository,
IAuthorizationStrategy[] authorizationStrategies,
IClaimSetClaimsProvider claimSetClaimsProvider)
IClaimSetClaimsProvider claimSetClaimsProvider,
IAuthorizationStrategyProvider[] authorizationStrategyProviders)
{
_resourceAuthorizationMetadataProvider = resourceAuthorizationMetadataProvider;
_claimSetClaimsProvider = claimSetClaimsProvider;
_authorizationStrategyProviders = authorizationStrategyProviders;

// Lazy initialization
_bitValuesByAction = new Lazy<Dictionary<string, int>>(
Expand All @@ -55,44 +53,6 @@ public AuthorizationBasisMetadataSelector(
{ securityRepository.GetActionByName("Delete").ActionUri, 0x8 },
{ securityRepository.GetActionByName("ReadChanges").ActionUri, 0x10 },
});

_authorizationStrategyByName = CreateAuthorizationStrategyByNameDictionary();

Dictionary<string, IAuthorizationStrategy> CreateAuthorizationStrategyByNameDictionary()
{
var strategyByName = new Dictionary<string, IAuthorizationStrategy>(StringComparer.OrdinalIgnoreCase);

foreach (var strategy in authorizationStrategies)
{
string strategyTypeName = GetStrategyTypeName(strategy);

// TODO: Embedded convention
// Enforce naming conventions on authorization strategies
if (!strategyTypeName.EndsWith(AuthorizationStrategyNameSuffix))
{
throw new ArgumentException(
$"The authorization strategy '{strategyTypeName}' does not follow proper naming conventions, ending with '{AuthorizationStrategyNameSuffix}'.");
}

string strategyName = strategyTypeName.TrimSuffix(AuthorizationStrategyNameSuffix);
strategyByName.Add(strategyName, strategy);
}

return strategyByName;
}

string GetStrategyTypeName(IAuthorizationStrategy strategy)
{
string rawTypeName = strategy.GetType().Name;

int genericMarkerPos = rawTypeName.IndexOf('`');

string strategyTypeName = genericMarkerPos < 0
? rawTypeName
: rawTypeName.Substring(0, genericMarkerPos);

return strategyTypeName;
}
}

/// <inheritdoc cref="IAuthorizationBasisMetadataSelector.SelectAuthorizationBasisMetadata" />
Expand Down Expand Up @@ -135,7 +95,8 @@ public AuthorizationBasisMetadata SelectAuthorizationBasisMetadata(
// No authorization strategies were defined for this request
if (authorizationStrategyNames == null || !authorizationStrategyNames.Any())
{
throw new Exception(
throw new SecurityConfigurationException(
SecurityConfigurationException.DefaultDetail,
string.Format(
"No authorization strategies were defined for the requested action '{0}' against resource URIs ['{1}'] matched by the caller's claim '{2}'.",
claimCheckResponse.RequestedAction,
Expand Down Expand Up @@ -171,13 +132,19 @@ IReadOnlyList<IAuthorizationStrategy> GetAuthorizationStrategies(IReadOnlyList<s
return strategyNames.Select(
strategyName =>
{
if (!_authorizationStrategyByName.ContainsKey(strategyName))
foreach (var authorizationStrategyProvider in _authorizationStrategyProviders)
{
throw new Exception(
$"Could not find authorization implementation for strategy '{strategyName}' based on naming convention of '{{strategyName}}{AuthorizationStrategyNameSuffix}'.");
var authorizationStrategy = authorizationStrategyProvider.GetByName(strategyName);

if (authorizationStrategy != null)
{
return authorizationStrategy;
}
}

return _authorizationStrategyByName[strategyName];
throw new SecurityConfigurationException(
SecurityConfigurationException.DefaultDetail,
$"Could not find an authorization strategy implementation for strategy name '{strategyName}'.");
})
.ToArray();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Threading.Tasks;
using EdFi.Common;
using EdFi.Ods.Common.Infrastructure.Pipelines;
using EdFi.Ods.Common.Security.Authorization;
using EdFi.Ods.Common.Security.Claims;
using EdFi.Security.DataAccess.Repositories;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ public ICriteria GetCriteriaQuery(TEntity specification, IQueryParameters queryP
// Create the "OR" junction
var mainDisjunction = new Disjunction();

// If there are multiple authorization strategies with views, we must use left outer joins and null/not null checks
var joinType = DetermineJoinType();
// If there are multiple relationship-based authorization strategies with views (that are combined with OR), we must use left outer joins and null/not null checks
var relationshipBasedAuthViewJoinType = DetermineRelationshipBasedAuthViewJoinType();

bool conjunctionFiltersWereApplied = ApplyAuthorizationStrategiesCombinedWithAndLogic();
bool disjunctionFiltersWereApplied = ApplyAuthorizationStrategiesCombinedWithOrLogic();
Expand All @@ -70,10 +70,11 @@ public ICriteria GetCriteriaQuery(TEntity specification, IQueryParameters queryP

return criteria;

JoinType DetermineJoinType()
JoinType DetermineRelationshipBasedAuthViewJoinType()
{
var countOfAuthorizationFiltersWithViewBasedFilters = authorizationFiltering.Count(
af => af.Filters.Select(afd =>
// Relationship-based authorization filters are combined using OR, Custom auth-view filters are combined using AND
var countOfRelationshipBasedAuthorizationFilters = authorizationFiltering.Count(
af => af.Operator == FilterOperator.Or && af.Filters.Select(afd =>
{
if (_authorizationFilterDefinitionProvider.TryGetAuthorizationFilterDefinition(afd.FilterName, out var filterDetails))
{
Expand All @@ -88,7 +89,7 @@ JoinType DetermineJoinType()
.OfType<ViewBasedAuthorizationFilterDefinition>()
.Any());

return countOfAuthorizationFiltersWithViewBasedFilters > 1
return countOfRelationshipBasedAuthorizationFilters > 1
? JoinType.LeftOuterJoin
: JoinType.InnerJoin;
}
Expand All @@ -102,7 +103,7 @@ bool ApplyAuthorizationStrategiesCombinedWithAndLogic()

foreach (var andStrategy in andStrategies)
{
if (!TryApplyFilters(mainConjunction, andStrategy.Filters))
if (!TryApplyFilters(mainConjunction, andStrategy.Filters, andStrategy.AuthorizationStrategy, JoinType.InnerJoin))
{
// All filters for AND strategies must be applied, and if not, this is an error condition
throw new Exception($"The following authorization filters are not recognized: {string.Join(" ", unsupportedAuthorizationFilters)}");
Expand All @@ -125,7 +126,7 @@ bool ApplyAuthorizationStrategiesCombinedWithOrLogic()
{
var filtersConjunction = new Conjunction(); // Combine filters with 'AND'

if (TryApplyFilters(filtersConjunction, orStrategy.Filters))
if (TryApplyFilters(filtersConjunction, orStrategy.Filters, orStrategy.AuthorizationStrategy, relationshipBasedAuthViewJoinType))
{
mainDisjunction.Add(filtersConjunction);

Expand All @@ -142,7 +143,11 @@ bool ApplyAuthorizationStrategiesCombinedWithOrLogic()
return disjunctionFiltersApplied;
}

bool TryApplyFilters(Conjunction conjunction, IReadOnlyList<AuthorizationFilterContext> filters)
bool TryApplyFilters(
Conjunction conjunction,
IReadOnlyList<AuthorizationFilterContext> filters,
IAuthorizationStrategy authorizationStrategy,
JoinType joinType)
{
bool allFiltersCanBeApplied = true;

Expand All @@ -169,17 +174,17 @@ bool TryApplyFilters(Conjunction conjunction, IReadOnlyList<AuthorizationFilterC
{
_authorizationFilterDefinitionProvider.TryGetAuthorizationFilterDefinition(
filterContext.FilterName,
out var filterApplicationDetails);
out var filterDefinition);

var applicator = filterApplicationDetails.CriteriaApplicator;

var parameterValues = new Dictionary<string, object>
{
{ filterContext.ClaimParameterName, filterContext.ClaimParameterValues }
};
var parameterValues = filterContext.ClaimParameterName == null
? new Dictionary<string, object>()
: new Dictionary<string, object>
{
{ filterContext.ClaimParameterName, filterContext.ClaimParameterValues }
};

// Apply the authorization strategy filter
applicator(criteria, conjunction, filterContext.SubjectEndpointName, parameterValues, joinType);
filterDefinition.CriteriaApplicator(criteria, conjunction, filterContext.SubjectEndpointNames, parameterValues, joinType, authorizationStrategy);

filtersApplied = true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ AuthorizationStrategyFilterResults[] PerformInstanceBasedAuthorization(
.Select(
f => new
{
FilterDefinition = _authorizationFilterDefinitionProvider.GetFilterDefinition(f.FilterName),
FilterDefinition = _authorizationFilterDefinitionProvider.GetAuthorizationFilterDefinition(f.FilterName),
FilterContext = f
})
.Select(
Expand Down
Loading

0 comments on commit 73a9791

Please sign in to comment.