Skip to content

Commit

Permalink
[ODS-5665] Add ability to query organizations by an identification co…
Browse files Browse the repository at this point in the history
…de (#1166)
  • Loading branch information
mjaksn authored Oct 25, 2024
1 parent 6ffba4e commit 0938d93
Show file tree
Hide file tree
Showing 23 changed files with 1,665 additions and 111 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
using EdFi.Ods.Common.Models.Resource;
using EdFi.Ods.Common.ProblemDetails;
using EdFi.Ods.Common.Providers;
using EdFi.Ods.Common.Providers.Queries;
using EdFi.Ods.Common.Security;
using EdFi.Ods.Common.Specifications;
using EdFi.Ods.Common.Validation;
Expand Down Expand Up @@ -348,6 +349,10 @@ protected override void Load(ContainerBuilder builder)
builder.RegisterType<PersonTypesProvider>()
.As<IPersonTypesProvider>()
.SingleInstance();

builder.RegisterType<ResourceIdentificationCodePropertiesProvider>()
.As<IResourceIdentificationCodePropertiesProvider>()
.SingleInstance();

builder.RegisterType<CachingInterceptor>()
.Named<IInterceptor>(InterceptorCacheKeys.OdsInstances)
Expand Down
1 change: 0 additions & 1 deletion Application/EdFi.Ods.Api/EdFi.Ods.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
<ItemGroup>
<PackageReference Include="EdFi.Suite3.Admin.DataAccess" Version="7.3.168" />
<PackageReference Include="EdFi.Suite3.Security.DataAccess" Version="7.3.201" />
<PackageReference Include="EdFi.Suite3.Common" Version="7.3.162" />
</ItemGroup>
</Otherwise>
</Choose>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ bool TryApplyFilters(
filterDefinition.CriteriaApplicator(
queryBuilder,
conjunctionQueryBuilder,
authorizationPlan.RequestContext.Resource,
filterContext.SubjectEndpointNames,
parameterValues,
joinType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ bool TryApplyFilters(
filterDefinition.CriteriaApplicator(
queryBuilder,
conjunctionQueryBuilder,
authorizationPlan.RequestContext.Resource,
filterContext.SubjectEndpointNames,
parameterValues,
joinType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using EdFi.Ods.Common.Infrastructure.Filtering;
using EdFi.Ods.Common.Models;
using EdFi.Ods.Common.Models.Resource;
using EdFi.Ods.Common.Providers.Queries;
using EdFi.Ods.Common.Security.Authorization;
using EdFi.Ods.Common.Security.Claims;

Expand Down Expand Up @@ -65,6 +66,7 @@ public IReadOnlyList<AuthorizationFilterDefinition> CreatePredefinedAuthorizatio
private static void ApplyAuthorizationCriteria(
QueryBuilder queryBuilder,
QueryBuilder whereBuilder,
Resource resource,
string[] subjectEndpointNames,
IDictionary<string, object> parameters,
JoinType joinType,
Expand All @@ -87,14 +89,16 @@ private static void ApplyAuthorizationCriteria(
// Ensure the Namespace parameter is represented as an object array
var namespacePrefixes = parameterValue as object[] ?? [parameterValue];

var alias = resource.Entity?.RootTableAlias() ?? "r";

// Add the final namespaces criteria to the supplied WHERE clause (junction)
whereBuilder.WhereNotNull(subjectEndpointName)
whereBuilder.WhereNotNull($"{alias}.{subjectEndpointName}")
.Where(
qb =>
{
foreach (var namespacePrefix in namespacePrefixes)
{
qb.OrWhereLike(subjectEndpointName, namespacePrefix, MatchMode.Start);
qb.OrWhereLike($"{alias}.{subjectEndpointName}", namespacePrefix, MatchMode.Start);
}

return qb;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public IReadOnlyList<AuthorizationFilterDefinition> CreatePredefinedAuthorizatio
private static void ApplyAuthorizationCriteria(
QueryBuilder queryBuilder,
QueryBuilder whereQueryBuilder,
Resource _,
string[] subjectEndpointNames,
IDictionary<string, object> parameters,
JoinType joinType,
Expand All @@ -67,7 +68,7 @@ private static void ApplyAuthorizationCriteria(
private void ApplyTrackedChangesAuthorizationCriteria(
AuthorizationFilterDefinition filterDefinition,
AuthorizationFilterContext filterContext,
Resource resource,
Resource _,
int filterIndex,
QueryBuilder queryBuilder,
bool useOuterJoins)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public ViewBasedAuthorizationFilterDefinition(
SELECT {{newAlias1}}.{viewTargetEndpointName}
FROM {GetFullNameForView($"auth_{viewName}")} {{newAlias1}}
WHERE {{newAlias1}}.{viewSourceEndpointName} IN (:{RelationshipAuthorizationConventions.ClaimsParameterName}))",
(queryBuilder, @where, subjectEndpointNames, parameters, joinType, authorizationStrategy) =>
(queryBuilder, @where, resource, subjectEndpointNames, parameters, joinType, authorizationStrategy) =>
{
if (subjectEndpointNames?.Length != 1)
{
Expand Down Expand Up @@ -89,7 +89,7 @@ public ViewBasedAuthorizationFilterDefinition(
: base(
filterName,
null, // HQL condition (not supported by view-based authorization)
(criteria, @where, subjectEndpointNames, parameters, joinType, authorizationStrategy)
(criteria, @where, resource, subjectEndpointNames, parameters, joinType, authorizationStrategy)
=> criteria.ApplyCustomViewJoinFilter(
viewName,
subjectEndpointNames,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public class AuthorizationFilterDefinition
public AuthorizationFilterDefinition(
string filterName,
string friendlyHqlConditionFormat,
Action<QueryBuilder, QueryBuilder, string[], IDictionary<string, object>, JoinType, IAuthorizationStrategy> criteriaApplicator,
Action<QueryBuilder, QueryBuilder, Resource, string[], IDictionary<string, object>, JoinType, IAuthorizationStrategy> criteriaApplicator,
Action<AuthorizationFilterDefinition, AuthorizationFilterContext, Resource, int, QueryBuilder, bool> trackedChangesCriteriaApplicator,
Func<DataManagementRequestContext, AuthorizationFilterContext, string, InstanceAuthorizationResult> authorizeInstance)
{
Expand All @@ -58,7 +58,7 @@ public AuthorizationFilterDefinition(
/// <summary>
/// Gets the function for applying the filter using NHibernate's <see cref="NHibernate.ICriteria"/> API.
/// </summary>
public Action<QueryBuilder, QueryBuilder, string[], IDictionary<string, object>, JoinType, IAuthorizationStrategy> CriteriaApplicator { get; protected set; }
public Action<QueryBuilder, QueryBuilder, Resource, string[], IDictionary<string, object>, JoinType, IAuthorizationStrategy> CriteriaApplicator { get; protected set; }

/// <summary>
/// Gets the function for applying the filter to the <see cref="QueryBuilder" /> for tracked changes queries.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
// 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.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using EdFi.Ods.Common.Models.Domain;
using EdFi.Ods.Common.Specifications;

namespace EdFi.Ods.Common.Providers.Criteria;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// 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.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using EdFi.Common.Configuration;
using EdFi.Common.Extensions;
using EdFi.Ods.Common.Database.Querying;
using EdFi.Ods.Common.Descriptors;
using EdFi.Ods.Common.Models;
using EdFi.Ods.Common.Models.Domain;
using EdFi.Ods.Common.Models.Resource;
using EdFi.Ods.Common.Providers.Criteria;

namespace EdFi.Ods.Common.Providers.Queries.Criteria;

/// <summary>
/// Applies criteria to a query based on identification codes property values
/// </summary>
public class IdentificationCodeAggregateRootQueryCriteriaApplicator : IAggregateRootQueryCriteriaApplicator
{
private readonly IDescriptorResolver _descriptorResolver;
private readonly IResourceModelProvider _resourceModelProvider;
private readonly IResourceIdentificationCodePropertiesProvider _resourceIdentificationCodePropertiesProvider;
private readonly DatabaseEngine _databaseEngine;

private const string IdentificationCodeTableAlias = "idct";
private readonly ConcurrentDictionary<FullName, Join> _identificationCodeTableJoinByRootEntityName = new();

public IdentificationCodeAggregateRootQueryCriteriaApplicator(
IDescriptorResolver descriptorResolver,
IResourceModelProvider resourceModelProvider,
IResourceIdentificationCodePropertiesProvider resourceIdentificationCodePropertiesProvider,
DatabaseEngine databaseEngine)
{
_descriptorResolver = descriptorResolver;
_resourceIdentificationCodePropertiesProvider = resourceIdentificationCodePropertiesProvider;
_resourceModelProvider = resourceModelProvider;
_databaseEngine = databaseEngine;
}

public void ApplyAdditionalParameters(QueryBuilder queryBuilder, Entity entity, AggregateRootWithCompositeKey specification,
IDictionary<string, string> additionalParameters)
{
if (additionalParameters == null
|| !additionalParameters.Any()
|| additionalParameters.All(
ap => AggregateRootCriteriaProviderHelpers.PropertiesToIgnore.Contains(ap.Key, StringComparer.OrdinalIgnoreCase)))
{
return;
}

var resource = _resourceModelProvider.GetResourceModel().GetResourceByFullName(entity.FullName);

// If the entity does not have an identificationCodes collection with queryable properties, return
if (!_resourceIdentificationCodePropertiesProvider.TryGetIdentificationCodeProperties(
resource, out List<ResourceProperty> identificationCodeProperties))
{
return;
}

// Find any supplied additionalParameters with a non-default value and name matching that of a queryable identificationCode property, if none then return
var applicableAdditionalParameters = additionalParameters
.Where(
x => !x.Value.IsDefaultValue()
&& identificationCodeProperties.Any(y => y.PropertyName.Equals(x.Key, StringComparison.OrdinalIgnoreCase)))
.ToArray();

if (applicableAdditionalParameters.Length == 0)
{
return;
}

var identificationCodeTableJoin =
GetIdentificationCodeEntityTableJoin(entity, identificationCodeProperties.First().EntityProperty.Entity);

queryBuilder.Join(identificationCodeTableJoin.TableName, _ => identificationCodeTableJoin);

ApplyParameterValuesToQueryBuilder(
queryBuilder, applicableAdditionalParameters, identificationCodeProperties);
}

private void ApplyParameterValuesToQueryBuilder(QueryBuilder queryBuilder,
IEnumerable<KeyValuePair<string, string>> parameterValuesByName,
List<ResourceProperty> identificationCodeProperties)
{
foreach (KeyValuePair<string, string> parameterKeyValuePair in parameterValuesByName)
{
var identificationCodeProperty =
identificationCodeProperties.FirstOrDefault(
p => p.PropertyName.Equals(parameterKeyValuePair.Key, StringComparison.OrdinalIgnoreCase));

if (identificationCodeProperty == null)
{
continue;
}

var queryParameterValue = GetQueryParameterValueForProperty(identificationCodeProperty, parameterKeyValuePair.Value);

if (identificationCodeProperty.IsDescriptorUsage && queryParameterValue == null)
{
// Descriptor did not match any value -- criteria should exclude all entries
// Since no additional criteria need to be applied, exit the loop
queryBuilder.WhereRaw("1 = 0");
break;
}

queryBuilder.Where(
$"{IdentificationCodeTableAlias}.{identificationCodeProperty.EntityProperty}", queryParameterValue);
}

return;

object GetQueryParameterValueForProperty(ResourceProperty property, string parameterValue)
{
if (!property.IsDescriptorUsage)
{
return parameterValue;
}

var lookupId = _descriptorResolver.GetDescriptorId(property.DescriptorName, parameterValue);

return lookupId == 0
? null
: lookupId;
}
}

private Join GetIdentificationCodeEntityTableJoin(Entity rootEntity, Entity identificationCodeEntity)
{
return _identificationCodeTableJoinByRootEntityName.GetOrAdd(
rootEntity.FullName, _ =>
{
string alias = rootEntity.RootTableAlias();

var join = new Join(
$"{identificationCodeEntity.Schema}.{identificationCodeEntity.TableName(_databaseEngine)}"
.Alias(IdentificationCodeTableAlias));

foreach (var entityIdentificationCodePropertyColumnName in GetIdentificationCodeEntityTableJoinColumnNames(
identificationCodeEntity))
{
join.On(
$"{alias}.{entityIdentificationCodePropertyColumnName}",
$"{IdentificationCodeTableAlias}.{entityIdentificationCodePropertyColumnName}");
}

return join;
});

IEnumerable<string> GetIdentificationCodeEntityTableJoinColumnNames(Entity identificationCodeEntity)
{
return identificationCodeEntity.Identifier.Properties
.Where(p => p.IsFromParent && p.IsIdentifying)
.Select(p => p.ColumnName(_databaseEngine, p.PropertyName)).ToArray();
}
}

public static string IdentificationCodeEntityTableAlias() => IdentificationCodeTableAlias;
}
26 changes: 26 additions & 0 deletions Application/EdFi.Ods.Common/Providers/Queries/EntityExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// 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 EdFi.Ods.Common.Models.Domain;

namespace EdFi.Ods.Common.Providers.Queries;

public static class EntityExtensions
{
private const string BaseAlias = "b";
private const string StandardAlias = "r";

/// <summary>
/// Determines the appropriate table alias for the given aggregate root entity.
/// </summary>
/// <param name="aggregateRootEntity">The aggregate root entity to determine the alias for.</param>
/// <returns>
/// Returns "b" if the entity is derived, otherwise returns "r".
/// </returns>
public static string RootTableAlias(this Entity aggregateRootEntity)
{
return aggregateRootEntity.IsDerived ? BaseAlias : StandardAlias;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// 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.Collections.Generic;
using EdFi.Ods.Common.Models.Resource;

namespace EdFi.Ods.Common.Providers.Queries;

/// <summary>
/// Provides an interface for retrieving the queryable properties of the identificationCode collection of a resource
/// </summary>
public interface IResourceIdentificationCodePropertiesProvider
{
/// <summary>
/// Attempts to get the queryable properties of a resource's identificationCode collection.
/// </summary>
/// <param name="resource">The entity to try and get the identificationCode properties for.</param>
/// <param name="queryableIdentificationCodeProperties">The queryable properties of the identificationCode of the <paramref name="resource"/>.</param>
/// <returns><c>true</c> if the resource has an identificationCode collection with any queryable properties.</returns>
public bool TryGetIdentificationCodeProperties(Resource resource,
out List<ResourceProperty> queryableIdentificationCodeProperties);
}
Loading

0 comments on commit 0938d93

Please sign in to comment.