Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 93 additions & 35 deletions src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,27 @@ public class ResourceAccessEntry
public ResourceAccessType Access { get; set; }
}

/// <summary>
/// Represents a resource for which permissions can be granted.
/// </summary>
public class AuthorizationResource
{
/// <summary>
/// Gets or sets the type of the resource.
/// </summary>
public string Type { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the name of the resource.
/// </summary>
public string Name { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the supported access types.
/// </summary>
public IEnumerable<ResourceAccessType> AccessTypes { get; set; } = [];
}

#endregion

#region [ Methods ]
Expand Down Expand Up @@ -169,6 +190,53 @@ bool isSupported(string claimType) => claimsProvider
/// <returns>A list of resources within the application.</returns>
[HttpGet, Route("resources")]
public virtual async Task<IActionResult> GetResources(IAuthorizationPolicyProvider policyProvider, EndpointDataSource endpointDataSource)
{
Dictionary<string, HashSet<ResourceAccessType>> resourceAccessLookup = await ResourceAccessLookup<ControllerAccessRequirement>(policyProvider, endpointDataSource);

IEnumerable<AuthorizationResource> resources = resourceAccessLookup
.OrderBy(kvp => kvp.Key)
.Select(kvp => new AuthorizationResource
{
Type = "Controller",
Name = kvp.Key,
AccessTypes = kvp.Value.OrderBy(type => type)
});

return Ok(resources);

}

/// <summary>
/// Gets a list of API resources available for which permissions can be granted within the application.
/// </summary>
/// <param name="policyProvider">Provides authorization policies defined within the application</param>
/// <param name="endpointDataSource">Source for endpoint data used to look up controller and action metadata</param>
/// <returns>A list of resources within the application.</returns>
[HttpGet, Route("APIresources")]
public virtual async Task<IActionResult> GetAPIResources(IAuthorizationPolicyProvider policyProvider, EndpointDataSource endpointDataSource)
{
Dictionary<string, HashSet<ResourceAccessType>> resourceAccessLookup = await ResourceAccessLookup<APIAccessRequirement>(policyProvider, endpointDataSource);

IEnumerable<AuthorizationResource> resources = resourceAccessLookup
.OrderBy(kvp => kvp.Key)
.Select(kvp => new AuthorizationResource
{
Type = "API",
Name = kvp.Key,
AccessTypes = kvp.Value.OrderBy(type => type)
});

return Ok(resources);

}

/// <summary>
/// Gets a list of resources available with the provided <see cref="IAuthorizationRequirement"/>.
/// </summary>
/// <param name="policyProvider">Provides authorization policies defined within the application</param>
/// <param name="endpointDataSource">Source for endpoint data used to look up controller and action metadata</param>
/// <returns>A list of resources within the application.</returns>
private async Task<Dictionary<string, HashSet<ResourceAccessType>>> ResourceAccessLookup<T>(IAuthorizationPolicyProvider policyProvider, EndpointDataSource endpointDataSource) where T : IAuthorizationRequirement
{
Dictionary<string, HashSet<ResourceAccessType>> resourceAccessLookup = [];

Expand All @@ -185,12 +253,12 @@ public virtual async Task<IActionResult> GetResources(IAuthorizationPolicyProvid
IReadOnlyList<IAuthorizationRequirementData> requirementData = endpoint.Metadata.GetOrderedMetadata<IAuthorizationRequirementData>() ?? [];
AuthorizationPolicy? policy = await AuthorizationPolicy.CombineAsync(policyProvider, authorizeData, policies);

bool hasControllerAccessRequirement = requirementData
bool hasAccessRequirement = requirementData
.SelectMany(datum => datum.GetRequirements())
.Concat(policy?.Requirements ?? [])
.Any(requirement => requirement is ControllerAccessRequirement);
.Any(requirement => requirement is T);

if (!hasControllerAccessRequirement)
if (!hasAccessRequirement)
continue;

IReadOnlyList<ResourceAccessAttribute> accessAttributes = endpoint.Metadata
Expand All @@ -201,38 +269,7 @@ public virtual async Task<IActionResult> GetResources(IAuthorizationPolicyProvid
HashSet<ResourceAccessType> access = resourceAccessLookup.GetOrAdd(resourceName, _ => []);
access.UnionWith(accessTypes);
}

var resources = resourceAccessLookup
.OrderBy(kvp => kvp.Key)
.Select(kvp => new
{
Type = "Controller",
Name = kvp.Key,
AccessTypes = kvp.Value.OrderBy(type => type)
});

return Ok(resources);

static IEnumerable<ResourceAccessType> ToAccessTypes(Endpoint endpoint, IEnumerable<ResourceAccessAttribute> accessAttributes)
{
ResourceAccessType accessType = accessAttributes.GetAccessType();

if (accessType == ResourceAccessType.None)
return [];

if (accessType != ResourceAccessType.Default)
return [accessType];

HttpMethodMetadata? httpMethodMetadata = endpoint.Metadata
.GetMetadata<HttpMethodMetadata>();

IReadOnlyList<string> httpMethods = httpMethodMetadata?.HttpMethods
?? [];

return httpMethods
.Select(accessAttributes.GetAccessType)
.Where(type => type != ResourceAccessType.None);
}
return resourceAccessLookup;
}

/// <summary>
Expand All @@ -252,6 +289,27 @@ public virtual IEnumerable<bool> CheckAccess([FromBody] ResourceAccessEntry[] ac

// Static Methods

private static IEnumerable<ResourceAccessType> ToAccessTypes(Endpoint endpoint, IEnumerable<ResourceAccessAttribute> accessAttributes)
{
ResourceAccessType accessType = accessAttributes.GetAccessType();

if (accessType == ResourceAccessType.None)
return [];

if (accessType != ResourceAccessType.Default)
return [accessType];

HttpMethodMetadata? httpMethodMetadata = endpoint.Metadata
.GetMetadata<HttpMethodMetadata>();

IReadOnlyList<string> httpMethods = httpMethodMetadata?.HttpMethods
?? [];

return httpMethods
.Select(accessAttributes.GetAccessType)
.Where(type => type != ResourceAccessType.None);
}

private static Regex? ToSearchPattern(string? searchText)
{
if (searchText is null)
Expand Down
12 changes: 6 additions & 6 deletions src/Gemstone.Web/APIController/ReadOnlyModelController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -337,12 +337,12 @@ public virtual async Task<IActionResult> Search([FromBody] SearchPost<T> postDat

if (ParentKey != string.Empty && parentID is not null)
{
filters.Append(new RecordFilter<T>()
filters = filters.Append(new RecordFilter<T>()
{
FieldName = ParentKey,
Operator = "=",
SearchParameter = parentID
});
}).ToArray();
}

IAsyncEnumerable<T> result = tableOperations.QueryRecordsAsync(HttpContext.User, postData.OrderBy, postData.Ascending, page, PageSize, cancellationToken, filters);
Expand All @@ -367,12 +367,12 @@ public virtual async Task<IActionResult> GetPageInfo([FromBody] SearchPost<T> po

if (ParentKey != string.Empty && parentID is not null)
{
filters.Append(new RecordFilter<T>()
filters = filters.Append(new RecordFilter<T>()
{
FieldName = ParentKey,
Operator = "=",
SearchParameter = parentID
});
}).ToArray();
}

int recordCount = await tableOperations.QueryRecordCountAsync(HttpContext.User, cancellationToken, filters).ConfigureAwait(false);
Expand Down Expand Up @@ -401,12 +401,12 @@ public virtual async Task<IActionResult> GetPageInfo(string? parentID, Cancellat

if (ParentKey != string.Empty && parentID is not null)
{
filters.Append(new RecordFilter<T>()
filters = filters.Append(new RecordFilter<T>()
{
FieldName = ParentKey,
Operator = "=",
SearchParameter = parentID
});
}).ToArray();
}

int recordCount = await tableOperations.QueryRecordCountAsync(HttpContext.User, cancellationToken, filters).ConfigureAwait(false);
Expand Down
60 changes: 60 additions & 0 deletions src/Gemstone.Web/Security/APIAccessHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//******************************************************************************************************
// APIAccessHandler.cs - Gbtc
//
// Copyright © 2026, Grid Protection Alliance. All Rights Reserved.
//
// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
// the NOTICE file distributed with this work for additional information regarding copyright ownership.
// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this
// file except in compliance with the License. You may obtain a copy of the License at:
//
// http://opensource.org/licenses/MIT
//
// Unless agreed to in writing, the subject software distributed under the License is distributed on an
// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
// License for the specific language governing permissions and limitations.
//
// Code Modification History:
// ----------------------------------------------------------------------------------------------------
// 07/09/2026 - C. Lackner
// Generated original version of source code.
//
//******************************************************************************************************

using Microsoft.AspNetCore.Authorization;

namespace Gemstone.Web.Security;

/// <summary>
/// Authorization handler for access to rest api actions.
/// </summary>
public class APIAccessHandler : GemstoneAccessHandler<APIAccessRequirement>
{
/// <inheritdoc/>
protected override string ResourceType => "API";
}

/// <summary>
/// Requirement to be handled by the <see cref="APIAccessHandler"/>.
/// </summary>
public class APIAccessRequirement : IAuthorizationRequirement
{
}

/// <summary>
/// Defines extension methods for the <see cref="APIAccessHandler"/>.
/// </summary>
public static class APIAccessHandlerExtensions
{
private static APIAccessRequirement Requirement { get; } = new();

/// <summary>
/// Adds the <see cref="APIAccessRequirement"/> to the policy.
/// </summary>
/// <param name="builder">The policy builder</param>
/// <returns>The policy builder.</returns>
public static AuthorizationPolicyBuilder RequireAPIAccess(this AuthorizationPolicyBuilder builder)
{
return builder.AddRequirements(Requirement);
}
}
Loading
Loading