Skip to content

Commit

Permalink
[ODS-6362] ODS/API Feature: Permissions API (#1139)
Browse files Browse the repository at this point in the history
  • Loading branch information
axelmarquezh authored Sep 13, 2024
1 parent 3b1708c commit dac30bf
Show file tree
Hide file tree
Showing 15 changed files with 333 additions and 55 deletions.
1 change: 1 addition & 0 deletions Application/EdFi.Ods.Api/Controllers/VersionController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ Dictionary<string, string> GetUrlsByName()
}

urlsByName["oauth"] = $"{rootUrl}/oauth/token";
urlsByName["oauthTokenIntrospection"] = $"{rootUrl}/oauth/token_info";

urlsByName["dataManagementApi"] = $"{rootUrl}/data/v{ApiVersionConstants.Ods}/";

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
Expand Up @@ -27,7 +27,7 @@
<Otherwise>
<ItemGroup>
<PackageReference Include="EdFi.Suite3.Admin.DataAccess" Version="7.3.168" />
<PackageReference Include="EdFi.Suite3.Security.DataAccess" Version="7.3.193" />
<PackageReference Include="EdFi.Suite3.Security.DataAccess" Version="7.3.201" />
<PackageReference Include="EdFi.Suite3.Common" Version="7.3.162" />
</ItemGroup>
</Otherwise>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,9 @@ IReadOnlyList<IAuthorizationStrategy> GetAuthorizationStrategies(IReadOnlyList<s
.ToArray();
}
}

private ClaimCheckResponse PerformClaimCheck(string claimSetName, IList<string> resourceClaimUris, string requestAction)

/// <inheritdoc cref="IAuthorizationBasisMetadataSelector.PerformClaimCheck" />
public ClaimCheckResponse PerformClaimCheck(string claimSetName, IList<string> resourceClaimUris, string requestAction)
{
var claimSetClaims = _claimSetClaimsProvider.GetClaims(claimSetName);

Expand Down Expand Up @@ -274,23 +275,4 @@ int GetBitValuesByAction(string action)
throw new NotSupportedException("The requested action is not a supported action. Authorization cannot be performed.");
}
}

private class ClaimCheckResponse
{
public bool Success { get; set; }

public IList<EdFiResourceClaim> RelevantClaims { get; set; }

public string RequestedAction { get; set; }

public IList<string> RequestedResourceUris { get; set; }

public IList<ResourceClaimAuthorizationMetadata> AuthorizationMetadata { get; set; }

public string SecurityExceptionDetail { get; set; }

public string SecurityExceptionMessage { get; set; }

public IEnumerable<string> SecurityExceptionInstanceTypeParts { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ AuthorizationBasisMetadata SelectAuthorizationBasisMetadata(
string claimSetName,
IList<string> requestResourceClaimUris,
string requestAction);

/// <summary>
/// Returns whether the given claim set name is authorized to the resource and action combination.
/// </summary>
ClaimCheckResponse PerformClaimCheck(
string claimSetName,
IList<string> resourceClaimUris,
string requestAction);
}

public class AuthorizationBasisMetadata
Expand All @@ -44,3 +52,22 @@ public AuthorizationBasisMetadata(

public string ValidationRuleSetName { get; }
}

public class ClaimCheckResponse
{
public bool Success { get; set; }

public IList<EdFiResourceClaim> RelevantClaims { get; set; }

public string RequestedAction { get; set; }

public IList<string> RequestedResourceUris { get; set; }

public IList<ResourceClaimAuthorizationMetadata> AuthorizationMetadata { get; set; }

public string SecurityExceptionDetail { get; set; }

public string SecurityExceptionMessage { get; set; }

public IEnumerable<string> SecurityExceptionInstanceTypeParts { get; set; }
}
16 changes: 14 additions & 2 deletions Application/EdFi.Ods.Features/TokenInfo/TokenInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,17 @@ public class TokenInfo
[JsonProperty("assigned_profiles")]
public IReadOnlyList<string> AssignedProfiles { get; private set; }

[JsonProperty("claim_set")]
public TokenInfoClaimSet TokenInfoClaimSet { get; private set; }

public IReadOnlyList<TokenInfoResource> Resources { get; private set; }

public IReadOnlyList<TokenInfoService> Services { get; private set; }

public static TokenInfo Create(ApiClientContext apiClientContext,
IList<TokenInfoEducationOrganizationData> tokenInfoData)
IList<TokenInfoEducationOrganizationData> tokenInfoData,
IReadOnlyList<TokenInfoResource> resources,
IReadOnlyList<TokenInfoService> services)
{
var dataGroupedByEdOrgId = tokenInfoData
.GroupBy(
Expand Down Expand Up @@ -74,7 +83,10 @@ public static TokenInfo Create(ApiClientContext apiClientContext,
NamespacePrefixes = apiClientContext.NamespacePrefixes.ToArray(),
AssignedProfiles = apiClientContext.Profiles.ToArray(),
StudentIdentificationSystem = apiClientContext.StudentIdentificationSystemDescriptor,
EducationOrganizations = tokenInfoEducationOrganizations.ToArray()
EducationOrganizations = tokenInfoEducationOrganizations.ToArray(),
TokenInfoClaimSet = new TokenInfoClaimSet { Name = apiClientContext.ClaimSetName },
Resources = resources,
Services = services
};
}
}
Expand Down
11 changes: 11 additions & 0 deletions Application/EdFi.Ods.Features/TokenInfo/TokenInfoClaimSet.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// 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.

namespace EdFi.Ods.Features.TokenInfo;

public class TokenInfoClaimSet
{
public string Name { get; set; }
}
127 changes: 97 additions & 30 deletions Application/EdFi.Ods.Features/TokenInfo/TokenInfoProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,51 +3,118 @@
// 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.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using EdFi.Common.Extensions;
using EdFi.Ods.Api.Security.Authorization;
using EdFi.Ods.Api.Security.Claims;
using EdFi.Ods.Common.Extensions;
using EdFi.Ods.Common.Models;
using EdFi.Ods.Common.Security;
using EdFi.Ods.Common.Security.Authorization;
using EdFi.Ods.Common.Security.Claims;
using EdFi.Security.DataAccess.Repositories;
using NHibernate;
using NHibernate.Transform;

namespace EdFi.Ods.Features.TokenInfo
{
public class TokenInfoProvider : ITokenInfoProvider
public class TokenInfoProvider(
ISessionFactory sessionFactory,
IResourceModelProvider resourceModelProvider,
IClaimSetClaimsProvider claimSetClaimsProvider,
IResourceClaimUriProvider resourceClaimUriProvider,
ISecurityRepository securityRepository,
IAuthorizationBasisMetadataSelector authorizationBasisMetadataSelector)
: ITokenInfoProvider
{
private const string EdOrgIdentifiersSql = @"
SELECT accessibleEdOrg.EducationOrganizationId,
accessibleEdOrg.NameOfInstitution,
accessibleEdOrg.Discriminator,
ancestorTuples.SourceEducationOrganizationId AS AncestorEducationOrganizationId,
ancestorEdOrg.Discriminator AS AncestorDiscriminator
FROM auth.EducationOrganizationIdToEducationOrganizationId accessibleTuples
INNER JOIN edfi.EducationOrganization accessibleEdOrg
ON accessibleTuples.TargetEducationOrganizationId = accessibleEdOrg.EducationOrganizationId
INNER JOIN auth.EducationOrganizationIdToEducationOrganizationId ancestorTuples
ON accessibleTuples.TargetEducationOrganizationId = ancestorTuples.TargetEducationOrganizationId
INNER JOIN edfi.EducationOrganization ancestorEdOrg
ON ancestorTuples.SourceEducationOrganizationId = ancestorEdOrg.EducationOrganizationId
WHERE accessibleTuples.SourceEducationOrganizationId IN ({0});";

private readonly ISessionFactory _sessionFactory;

public TokenInfoProvider(ISessionFactory sessionFactory)
public async Task<TokenInfo> GetTokenInfoAsync(ApiClientContext apiContext)
{
_sessionFactory = sessionFactory;
return TokenInfo.Create(
apiContext,
await GetAuthorizedEducationOrganizations(apiContext),
GetAuthorizedResources(apiContext),
GetAuthorizedServices(apiContext));
}

public async Task<TokenInfo> GetTokenInfoAsync(ApiClientContext apiContext)
private async Task<IList<TokenInfoEducationOrganizationData>> GetAuthorizedEducationOrganizations(ApiClientContext apiContext)
{
using (var session = _sessionFactory.OpenStatelessSession())
{
string edOrgIds = string.Join(",", apiContext.EducationOrganizationIds);
const string EdOrgIdentifiersSql = """
SELECT accessibleEdOrg.EducationOrganizationId,
accessibleEdOrg.NameOfInstitution,
accessibleEdOrg.Discriminator,
ancestorTuples.SourceEducationOrganizationId AS AncestorEducationOrganizationId,
ancestorEdOrg.Discriminator AS AncestorDiscriminator
FROM auth.EducationOrganizationIdToEducationOrganizationId accessibleTuples
INNER JOIN edfi.EducationOrganization accessibleEdOrg
ON accessibleTuples.TargetEducationOrganizationId = accessibleEdOrg.EducationOrganizationId
INNER JOIN auth.EducationOrganizationIdToEducationOrganizationId ancestorTuples
ON accessibleTuples.TargetEducationOrganizationId = ancestorTuples.TargetEducationOrganizationId
INNER JOIN edfi.EducationOrganization ancestorEdOrg
ON ancestorTuples.SourceEducationOrganizationId = ancestorEdOrg.EducationOrganizationId
WHERE accessibleTuples.SourceEducationOrganizationId IN ({0});
""";

string edOrgIds = string.Join(",", apiContext.EducationOrganizationIds);

var tokenInfoEducationOrganizationData =
await session.CreateSQLQuery(string.Format(EdOrgIdentifiersSql, edOrgIds))
.SetResultTransformer(Transformers.AliasToBean<TokenInfoEducationOrganizationData>())
.ListAsync<TokenInfoEducationOrganizationData>(CancellationToken.None);
using var session = sessionFactory.OpenStatelessSession();

return TokenInfo.Create(apiContext, tokenInfoEducationOrganizationData);
}
return await session.CreateSQLQuery(string.Format(EdOrgIdentifiersSql, edOrgIds))
.SetResultTransformer(Transformers.AliasToBean<TokenInfoEducationOrganizationData>())
.ListAsync<TokenInfoEducationOrganizationData>(CancellationToken.None);
}

private IReadOnlyList<TokenInfoResource> GetAuthorizedResources(ApiClientContext apiContext)
{
return resourceModelProvider.GetResourceModel()
.GetAllResources()
.Where(r => !r.IsAbstract())
.Select(r =>
{
var claimUris = resourceClaimUriProvider.GetResourceClaimUris(r);

var authorizedActions = securityRepository.GetActions()
.Where(action =>
{
try
{
return authorizationBasisMetadataSelector.PerformClaimCheck(
apiContext.ClaimSetName, claimUris, action.ActionUri).Success;
}
catch (AuthorizationContextException)
{
return false;
}
})
.Select(action => action.ActionName)
.ToList();

return new TokenInfoResource
{
Resource = $"/{r.SchemaUriSegment()}/{r.PluralName.ToCamelCase()}",
Operations = authorizedActions
};
})
.Where(tir => tir.Operations.Any())
.ToList();
}

private IReadOnlyList<TokenInfoService> GetAuthorizedServices(ApiClientContext apiContext)
{
return claimSetClaimsProvider.GetClaims(apiContext.ClaimSetName)
.Where(c => c.ClaimName.StartsWith(EdFiOdsApiClaimTypes.ServicesPrefix, StringComparison.OrdinalIgnoreCase))
.Select(c => new TokenInfoService
{
Service = c.ClaimName[EdFiOdsApiClaimTypes.ServicesPrefix.Length..],
Operations = c.ClaimValue.Actions
.Select(a => securityRepository.GetActionByUri(a.Name).ActionName)
.ToList()
})
.Where(tis => tis.Operations.Any())
.ToList();
}
}
}
14 changes: 14 additions & 0 deletions Application/EdFi.Ods.Features/TokenInfo/TokenInfoResource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// 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;

namespace EdFi.Ods.Features.TokenInfo;

public class TokenInfoResource
{
public string Resource { get; set; }
public IReadOnlyList<string> Operations { get; set; }
}
14 changes: 14 additions & 0 deletions Application/EdFi.Ods.Features/TokenInfo/TokenInfoService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// 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;

namespace EdFi.Ods.Features.TokenInfo;

public class TokenInfoService
{
public string Service { get; set; }
public IReadOnlyList<string> Operations { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ public void Get_WithProvidedMultTenancyAndOdsRouteContextSettings_ShouldBuildRou
{ "dependencies", $"http://localhost/{expectedPathRootSegment}metadata/data/v3/dependencies" },
{ "openApiMetadata", $"http://localhost/{expectedPathRootSegment}metadata/" },
{ "oauth", $"http://localhost/{expectedPathRootSegment}oauth/token" },
{ "oauthTokenIntrospection", $"http://localhost/{expectedPathRootSegment}oauth/token_info" },
{ "dataManagementApi", $"http://localhost/{expectedPathRootSegment}data/v3/" },
{ "xsdMetadata", $"http://localhost/{expectedPathRootSegment}metadata/xsd" },
{ "changeQueries", $"http://localhost/{expectedPathRootSegment}changeQueries/v1/" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using EdFi.Ods.Api.Security.Authorization;
using EdFi.Ods.Api.Security.Claims;
using EdFi.Ods.Common.Models;
using EdFi.Ods.Common.Security;
using EdFi.Ods.Common.Security.Authorization;
using EdFi.Ods.Features.TokenInfo;
using EdFi.Security.DataAccess.Repositories;
using FakeItEasy;
using NHibernate;
using NHibernate.Transform;
Expand Down Expand Up @@ -112,7 +117,13 @@ public async Task Should_get_education_organization_identifiers_for_a_user_lea_c
.Returns(CreateEducationOrganizationIdentifiers());

// Act
var tokenInfoProvider = new TokenInfoProvider(sessionFactory);
var tokenInfoProvider = new TokenInfoProvider(
sessionFactory,
A.Fake<IResourceModelProvider>(),
A.Fake<IClaimSetClaimsProvider>(),
A.Fake<IResourceClaimUriProvider>(),
A.Fake<ISecurityRepository>(),
A.Fake<IAuthorizationBasisMetadataSelector>());

var results = await tokenInfoProvider.GetTokenInfoAsync(CreateApiContext());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@ namespace EdFi.Security.DataAccess.Repositories;
[Intercept("cache-security")]
public interface ISecurityRepository
{
IList<Action> GetActions();

Action GetActionByName(string actionName);

Action GetActionByUri(string uri);

AuthorizationStrategy GetAuthorizationStrategyByName(string authorizationStrategyName);

IList<ClaimSetResourceClaimAction> GetClaimsForClaimSet(string claimSetName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,21 @@ public SecurityRepository(ISecurityTableGateway securityTableGateway)
_securityTableGateway = securityTableGateway ?? throw new ArgumentNullException(nameof(securityTableGateway));
}

public virtual IList<Action> GetActions()
{
return _securityTableGateway.GetActions();
}

public virtual Action GetActionByName(string actionName)
{
return _securityTableGateway.GetActions().First(a => a.ActionName.Equals(actionName, StringComparison.OrdinalIgnoreCase));
}

public virtual Action GetActionByUri(string uri)
{
return _securityTableGateway.GetActions().First(a => a.ActionUri.Equals(uri, StringComparison.OrdinalIgnoreCase));
}

public virtual AuthorizationStrategy GetAuthorizationStrategyByName(string authorizationStrategyName)
{
return _securityTableGateway.GetAuthorizationStrategies().First(
Expand Down
Loading

0 comments on commit dac30bf

Please sign in to comment.