From 7370cd3b5f85df6855ca6e19d17823145b6762eb Mon Sep 17 00:00:00 2001 From: prestoncraw Date: Tue, 4 Aug 2026 10:47:14 -0400 Subject: [PATCH 1/6] Add DTO for AuthorizationResource --- .../AuthorizationInfoControllerBase.cs | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs b/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs index ae464eb4..49507353 100644 --- a/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs +++ b/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs @@ -69,6 +69,27 @@ public class ResourceAccessEntry public ResourceAccessType Access { get; set; } } + /// + /// Represents a resource for which permissions can be granted. + /// + public class AuthorizationResource + { + /// + /// Gets or sets the type of the resource. + /// + public string Type { get; set; } = string.Empty; + + /// + /// Gets or sets the name of the resource. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the supported access types. + /// + public IEnumerable AccessTypes { get; set; } = []; + } + #endregion #region [ Methods ] @@ -202,9 +223,9 @@ public virtual async Task GetResources(IAuthorizationPolicyProvid access.UnionWith(accessTypes); } - var resources = resourceAccessLookup + IEnumerable resources = resourceAccessLookup .OrderBy(kvp => kvp.Key) - .Select(kvp => new + .Select(kvp => new AuthorizationResource { Type = "Controller", Name = kvp.Key, From f1ec693c4a2c635616fcb0556d1cec7c3c72c455 Mon Sep 17 00:00:00 2001 From: Christoph Lackner Date: Fri, 10 Jul 2026 16:27:51 -0400 Subject: [PATCH 2/6] Added APIAccessHandler --- src/Gemstone.Web/Security/APIAccessHandler.cs | 74 ++++++++ .../Security/ControllerAccessHandler.cs | 129 +------------ .../Security/GemstoneAccessHandler.cs | 170 ++++++++++++++++++ 3 files changed, 247 insertions(+), 126 deletions(-) create mode 100644 src/Gemstone.Web/Security/APIAccessHandler.cs create mode 100644 src/Gemstone.Web/Security/GemstoneAccessHandler.cs diff --git a/src/Gemstone.Web/Security/APIAccessHandler.cs b/src/Gemstone.Web/Security/APIAccessHandler.cs new file mode 100644 index 00000000..a32de207 --- /dev/null +++ b/src/Gemstone.Web/Security/APIAccessHandler.cs @@ -0,0 +1,74 @@ +//****************************************************************************************************** +// ControllerAccessHandler.cs - Gbtc +// +// Copyright © 2025, 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/29/2025 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Security.Claims; +using System.Threading.Tasks; +using Gemstone.Reflection.MemberInfoExtensions; +using Gemstone.Security; +using Gemstone.Security.AccessControl; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Routing; + +namespace Gemstone.Web.Security; + +/// +/// Authorization handler for access to rest api actions. +/// +public class APIAccessHandler: GemstoneAccessHandler +{ + /// + protected override string ResourceType => "API"; + + /// #ToDo - Remove Support for Gemstone.ResourceAccess.Default +} + +/// +/// Requirement to be handled by the . +/// +public class APIAccessRequirement : IAuthorizationRequirement +{ +} + +/// +/// Defines extension methods for the . +/// +public static class APIAccessHandlerExtensions +{ + private static APIAccessRequirement Requirement { get; } = new(); + + /// + /// Adds the to the policy. + /// + /// The policy builder + /// The policy builder. + public static AuthorizationPolicyBuilder RequireAPIAccess(this AuthorizationPolicyBuilder builder) + { + return builder.AddRequirements(Requirement); + } +} diff --git a/src/Gemstone.Web/Security/ControllerAccessHandler.cs b/src/Gemstone.Web/Security/ControllerAccessHandler.cs index 3d642779..18d23de2 100644 --- a/src/Gemstone.Web/Security/ControllerAccessHandler.cs +++ b/src/Gemstone.Web/Security/ControllerAccessHandler.cs @@ -36,135 +36,12 @@ namespace Gemstone.Web.Security; /// /// Authorization handler for access to controller actions. /// -public class ControllerAccessHandler : AuthorizationHandler +public class ControllerAccessHandler : GemstoneAccessHandler { - #region [ Members ] - - // Nested Types - private enum Permission - { - Allow, - Deny, - Neither - } - - private class ContextWrapper(AuthorizationHandlerContext context, ControllerAccessRequirement requirement, HttpContext httpContext, Endpoint endpoint, ControllerActionDescriptor descriptor) - { - private AuthorizationHandlerContext Context { get; } = context; - private ControllerAccessRequirement Requirement { get; } = requirement; - - public ClaimsPrincipal User { get; } = context.User; - public Endpoint Endpoint { get; } = endpoint; - public ControllerActionDescriptor Descriptor { get; } = descriptor; - public string HttpMethod => httpContext.Request.Method; - - public bool Succeed() - { - Context.Succeed(Requirement); - return true; - } - - public bool Fail(AuthorizationFailureReason reason) - { - Context.Fail(reason); - return true; - } - } - - #endregion - - #region [ Methods ] - + /// - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ControllerAccessRequirement requirement) - { - if (context.Resource is not HttpContext httpContext) - return Task.CompletedTask; - - IEndpointFeature? endpointFeature = httpContext.Features.Get(); - Endpoint? endpoint = endpointFeature?.Endpoint; - - if (endpoint is null) - return Task.CompletedTask; - - ControllerActionDescriptor? descriptor = endpoint.Metadata - .GetMetadata(); - - if (descriptor is null) - return Task.CompletedTask; - - ContextWrapper wrapper = new(context, requirement, httpContext, endpoint, descriptor); - - if (HandleResourceActionPermission(wrapper)) - return Task.CompletedTask; - - HandleResourceAccessPermission(wrapper); - return Task.CompletedTask; - } - - private bool HandleResourceActionPermission(ContextWrapper wrapper) - { - IRouteNameMetadata? routeNameMetadata = wrapper.Endpoint.Metadata - .GetMetadata(); - - string? routeName = routeNameMetadata?.RouteName; - - string resource = wrapper.Descriptor.ControllerName; - string action = routeName ?? wrapper.Descriptor.ActionName; - string claimValue = $"Controller {resource} {action}"; - Permission permission = GetResourceActionPermission(wrapper.User, claimValue); - - return - (permission == Permission.Deny && fail()) || - (permission == Permission.Allow && succeed()); - - bool succeed() => - wrapper.Succeed(); - - bool fail() - { - AuthorizationFailureReason reason = ToFailureReason(claimValue); - return wrapper.Fail(reason); - } - } - - private AuthorizationFailureReason ToFailureReason(string claim) - { - return new AuthorizationFailureReason(this, $"{claim} permission denied"); - } - - #endregion - - #region [ Static ] - - // Static Methods - - private static Permission GetResourceActionPermission(ClaimsPrincipal user, string claimValue) - { - string allowClaim = $"Gemstone.ResourceAction.Allow"; - string denyClaim = $"Gemstone.ResourceAction.Deny"; - - if (user.HasClaim(denyClaim, claimValue)) - return Permission.Deny; - - return user.HasClaim(allowClaim, claimValue) - ? Permission.Allow - : Permission.Neither; - } - - private static void HandleResourceAccessPermission(ContextWrapper wrapper) - { - IReadOnlyList accessAttributes = wrapper.Endpoint.Metadata - .GetOrderedMetadata(); - - string resourceName = accessAttributes.GetResourceName(wrapper.Descriptor); - ResourceAccessType access = accessAttributes.GetAccessType(wrapper.HttpMethod); - - if (wrapper.User.HasAccessTo("Controller", resourceName, access)) - wrapper.Succeed(); - } + protected override string ResourceType => "Controller"; - #endregion } /// diff --git a/src/Gemstone.Web/Security/GemstoneAccessHandler.cs b/src/Gemstone.Web/Security/GemstoneAccessHandler.cs new file mode 100644 index 00000000..7881c39c --- /dev/null +++ b/src/Gemstone.Web/Security/GemstoneAccessHandler.cs @@ -0,0 +1,170 @@ +//****************************************************************************************************** +// GemstoneAccessHandler.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 System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Gemstone.Security; +using Gemstone.Security.AccessControl; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Routing; + +namespace Gemstone.Web.Security; + +/// +/// Authorization handler for access to generic Resources. +/// +public class GemstoneAccessHandler : AuthorizationHandler where T : IAuthorizationRequirement +{ + #region [ Members ] + + protected virtual string ResourceType { get; } + + // Nested Types + private enum Permission + { + Allow, + Deny, + Neither + } + + private class ContextWrapper(AuthorizationHandlerContext context, T requirement, HttpContext httpContext, Endpoint endpoint, ControllerActionDescriptor descriptor) where T : IAuthorizationRequirement + { + private AuthorizationHandlerContext Context { get; } = context; + private T Requirement { get; } = requirement; + + public ClaimsPrincipal User { get; } = context.User; + public Endpoint Endpoint { get; } = endpoint; + public ControllerActionDescriptor Descriptor { get; } = descriptor; + public string HttpMethod => httpContext.Request.Method; + + public bool Succeed() + { + Context.Succeed(Requirement); + return true; + } + + public bool Fail(AuthorizationFailureReason reason) + { + Context.Fail(reason); + return true; + } + } + + + #endregion + + #region [ Methods ] + + /// + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, T requirement) + { + if (context.Resource is not HttpContext httpContext) + return Task.CompletedTask; + + IEndpointFeature? endpointFeature = httpContext.Features.Get(); + Endpoint? endpoint = endpointFeature?.Endpoint; + + if (endpoint is null) + return Task.CompletedTask; + + ControllerActionDescriptor? descriptor = endpoint.Metadata + .GetMetadata(); + + if (descriptor is null) + return Task.CompletedTask; + + ContextWrapper wrapper = new(context, requirement, httpContext, endpoint, descriptor); + + if (HandleResourceActionPermission(wrapper)) + return Task.CompletedTask; + + HandleResourceAccessPermission(wrapper, ResourceType); + return Task.CompletedTask; + } + + private bool HandleResourceActionPermission(ContextWrapper wrapper) + { + IRouteNameMetadata? routeNameMetadata = wrapper.Endpoint.Metadata + .GetMetadata(); + + string? routeName = routeNameMetadata?.RouteName; + + string resource = wrapper.Descriptor.ControllerName; + string action = routeName ?? wrapper.Descriptor.ActionName; + string claimValue = $"{ResourceType} {resource} {action}"; + Permission permission = GetResourceActionPermission(wrapper.User, claimValue); + + return + (permission == Permission.Deny && fail()) || + (permission == Permission.Allow && succeed()); + + bool succeed() => + wrapper.Succeed(); + + bool fail() + { + AuthorizationFailureReason reason = ToFailureReason(claimValue); + return wrapper.Fail(reason); + } + } + + private AuthorizationFailureReason ToFailureReason(string claim) + { + return new AuthorizationFailureReason(this, $"{claim} permission denied"); + } + + #endregion + + #region [ Static ] + + // Static Methods + + private static Permission GetResourceActionPermission(ClaimsPrincipal user, string claimValue) + { + + if (user.HasClaim(GemstoneClaimTypes.DenyClaim, claimValue)) + return Permission.Deny; + + return user.HasClaim(GemstoneClaimTypes.AllowClaim, claimValue) + ? Permission.Allow + : Permission.Neither; + } + + private static void HandleResourceAccessPermission(ContextWrapper wrapper, string resourceType) + { + IReadOnlyList accessAttributes = wrapper.Endpoint.Metadata + .GetOrderedMetadata(); + + string resourceName = accessAttributes.GetResourceName(wrapper.Descriptor); + ResourceAccessType access = accessAttributes.GetAccessType(wrapper.HttpMethod); + + if (wrapper.User.HasAccessTo(resourceType, resourceName, access)) + wrapper.Succeed(); + } + + #endregion +} From da5b8b3813165009f339ee91a960c4e7a4f839c7 Mon Sep 17 00:00:00 2001 From: StephenCWills Date: Tue, 4 Aug 2026 15:32:52 -0400 Subject: [PATCH 3/6] Cleanup --- src/Gemstone.Web/Security/APIAccessHandler.cs | 22 ++++--------------- .../Security/ControllerAccessHandler.cs | 10 --------- .../Security/GemstoneAccessHandler.cs | 20 +++++++++-------- 3 files changed, 15 insertions(+), 37 deletions(-) diff --git a/src/Gemstone.Web/Security/APIAccessHandler.cs b/src/Gemstone.Web/Security/APIAccessHandler.cs index a32de207..eebbe22f 100644 --- a/src/Gemstone.Web/Security/APIAccessHandler.cs +++ b/src/Gemstone.Web/Security/APIAccessHandler.cs @@ -1,7 +1,7 @@ //****************************************************************************************************** -// ControllerAccessHandler.cs - Gbtc +// APIAccessHandler.cs - Gbtc // -// Copyright © 2025, Grid Protection Alliance. All Rights Reserved. +// 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. @@ -16,36 +16,22 @@ // // Code Modification History: // ---------------------------------------------------------------------------------------------------- -// 07/29/2025 - Stephen C. Wills +// 07/09/2026 - C. Lackner // Generated original version of source code. // //****************************************************************************************************** -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Security.Claims; -using System.Threading.Tasks; -using Gemstone.Reflection.MemberInfoExtensions; -using Gemstone.Security; -using Gemstone.Security.AccessControl; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Mvc.Controllers; -using Microsoft.AspNetCore.Routing; namespace Gemstone.Web.Security; /// /// Authorization handler for access to rest api actions. /// -public class APIAccessHandler: GemstoneAccessHandler +public class APIAccessHandler : GemstoneAccessHandler { /// protected override string ResourceType => "API"; - - /// #ToDo - Remove Support for Gemstone.ResourceAccess.Default } /// diff --git a/src/Gemstone.Web/Security/ControllerAccessHandler.cs b/src/Gemstone.Web/Security/ControllerAccessHandler.cs index 18d23de2..a4df0f93 100644 --- a/src/Gemstone.Web/Security/ControllerAccessHandler.cs +++ b/src/Gemstone.Web/Security/ControllerAccessHandler.cs @@ -21,15 +21,7 @@ // //****************************************************************************************************** -using System.Collections.Generic; -using System.Security.Claims; -using System.Threading.Tasks; -using Gemstone.Security.AccessControl; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Mvc.Controllers; -using Microsoft.AspNetCore.Routing; namespace Gemstone.Web.Security; @@ -38,10 +30,8 @@ namespace Gemstone.Web.Security; /// public class ControllerAccessHandler : GemstoneAccessHandler { - /// protected override string ResourceType => "Controller"; - } /// diff --git a/src/Gemstone.Web/Security/GemstoneAccessHandler.cs b/src/Gemstone.Web/Security/GemstoneAccessHandler.cs index 7881c39c..ac3f8337 100644 --- a/src/Gemstone.Web/Security/GemstoneAccessHandler.cs +++ b/src/Gemstone.Web/Security/GemstoneAccessHandler.cs @@ -37,11 +37,14 @@ namespace Gemstone.Web.Security; /// /// Authorization handler for access to generic Resources. /// -public class GemstoneAccessHandler : AuthorizationHandler where T : IAuthorizationRequirement +public abstract class GemstoneAccessHandler : AuthorizationHandler where TRequirement : IAuthorizationRequirement { #region [ Members ] - protected virtual string ResourceType { get; } + /// + /// Gets the type of resource handled by the authorization handler. + /// + protected abstract string ResourceType { get; } // Nested Types private enum Permission @@ -51,10 +54,10 @@ private enum Permission Neither } - private class ContextWrapper(AuthorizationHandlerContext context, T requirement, HttpContext httpContext, Endpoint endpoint, ControllerActionDescriptor descriptor) where T : IAuthorizationRequirement + private class ContextWrapper(AuthorizationHandlerContext context, TRequirement requirement, HttpContext httpContext, Endpoint endpoint, ControllerActionDescriptor descriptor) { private AuthorizationHandlerContext Context { get; } = context; - private T Requirement { get; } = requirement; + private TRequirement Requirement { get; } = requirement; public ClaimsPrincipal User { get; } = context.User; public Endpoint Endpoint { get; } = endpoint; @@ -80,7 +83,7 @@ public bool Fail(AuthorizationFailureReason reason) #region [ Methods ] /// - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, T requirement) + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement) { if (context.Resource is not HttpContext httpContext) return Task.CompletedTask; @@ -97,7 +100,7 @@ protected override Task HandleRequirementAsync(AuthorizationHandlerContext conte if (descriptor is null) return Task.CompletedTask; - ContextWrapper wrapper = new(context, requirement, httpContext, endpoint, descriptor); + ContextWrapper wrapper = new(context, requirement, httpContext, endpoint, descriptor); if (HandleResourceActionPermission(wrapper)) return Task.CompletedTask; @@ -106,7 +109,7 @@ protected override Task HandleRequirementAsync(AuthorizationHandlerContext conte return Task.CompletedTask; } - private bool HandleResourceActionPermission(ContextWrapper wrapper) + private bool HandleResourceActionPermission(ContextWrapper wrapper) { IRouteNameMetadata? routeNameMetadata = wrapper.Endpoint.Metadata .GetMetadata(); @@ -145,7 +148,6 @@ private AuthorizationFailureReason ToFailureReason(string claim) private static Permission GetResourceActionPermission(ClaimsPrincipal user, string claimValue) { - if (user.HasClaim(GemstoneClaimTypes.DenyClaim, claimValue)) return Permission.Deny; @@ -154,7 +156,7 @@ private static Permission GetResourceActionPermission(ClaimsPrincipal user, stri : Permission.Neither; } - private static void HandleResourceAccessPermission(ContextWrapper wrapper, string resourceType) + private static void HandleResourceAccessPermission(ContextWrapper wrapper, string resourceType) { IReadOnlyList accessAttributes = wrapper.Endpoint.Metadata .GetOrderedMetadata(); From 2c36fe6f5af2eed94326c3940b2dc385c44d6c6c Mon Sep 17 00:00:00 2001 From: prestoncraw Date: Thu, 6 Aug 2026 15:47:13 -0400 Subject: [PATCH 4/6] Fix parentID filters not being appended --- .../APIController/ReadOnlyModelController.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Gemstone.Web/APIController/ReadOnlyModelController.cs b/src/Gemstone.Web/APIController/ReadOnlyModelController.cs index f681826e..b2d9e113 100644 --- a/src/Gemstone.Web/APIController/ReadOnlyModelController.cs +++ b/src/Gemstone.Web/APIController/ReadOnlyModelController.cs @@ -337,12 +337,12 @@ public virtual async Task Search([FromBody] SearchPost postDat if (ParentKey != string.Empty && parentID is not null) { - filters.Append(new RecordFilter() + filters = filters.Append(new RecordFilter() { FieldName = ParentKey, Operator = "=", SearchParameter = parentID - }); + }).ToArray(); } IAsyncEnumerable result = tableOperations.QueryRecordsAsync(HttpContext.User, postData.OrderBy, postData.Ascending, page, PageSize, cancellationToken, filters); @@ -367,12 +367,12 @@ public virtual async Task GetPageInfo([FromBody] SearchPost po if (ParentKey != string.Empty && parentID is not null) { - filters.Append(new RecordFilter() + filters = filters.Append(new RecordFilter() { FieldName = ParentKey, Operator = "=", SearchParameter = parentID - }); + }).ToArray(); } int recordCount = await tableOperations.QueryRecordCountAsync(HttpContext.User, cancellationToken, filters).ConfigureAwait(false); @@ -401,12 +401,12 @@ public virtual async Task GetPageInfo(string? parentID, Cancellat if (ParentKey != string.Empty && parentID is not null) { - filters.Append(new RecordFilter() + filters = filters.Append(new RecordFilter() { FieldName = ParentKey, Operator = "=", SearchParameter = parentID - }); + }).ToArray(); } int recordCount = await tableOperations.QueryRecordCountAsync(HttpContext.User, cancellationToken, filters).ConfigureAwait(false); From 19d7c83c235f2581cff07dab7f9293f09dd14aa3 Mon Sep 17 00:00:00 2001 From: Christoph Lackner Date: Tue, 4 Aug 2026 16:28:36 -0400 Subject: [PATCH 5/6] Added Ednpoint for API Access Resources --- .../AuthorizationInfoControllerBase.cs | 81 ++++++++++++++++--- 1 file changed, 68 insertions(+), 13 deletions(-) diff --git a/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs b/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs index 49507353..b5748648 100644 --- a/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs +++ b/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs @@ -234,26 +234,60 @@ public virtual async Task GetResources(IAuthorizationPolicyProvid return Ok(resources); - static IEnumerable ToAccessTypes(Endpoint endpoint, IEnumerable accessAttributes) + } + + /// + /// Gets a list of API resources available for which permissions can be granted within the application. + /// + /// Provides authorization policies defined within the application + /// Source for endpoint data used to look up controller and action metadata + /// A list of resources within the application. + [HttpGet, Route("APIresources")] + public virtual async Task GetAPIResources(IAuthorizationPolicyProvider policyProvider, EndpointDataSource endpointDataSource) + { + Dictionary> resourceAccessLookup = []; + + foreach (Endpoint endpoint in endpointDataSource.Endpoints) { - ResourceAccessType accessType = accessAttributes.GetAccessType(); + ControllerActionDescriptor? descriptor = endpoint.Metadata + .GetMetadata(); + + if (descriptor is null) + continue; - if (accessType == ResourceAccessType.None) - return []; + IReadOnlyList authorizeData = endpoint.Metadata.GetOrderedMetadata() ?? []; + IReadOnlyList policies = endpoint.Metadata.GetOrderedMetadata() ?? []; + IReadOnlyList requirementData = endpoint.Metadata.GetOrderedMetadata() ?? []; + AuthorizationPolicy? policy = await AuthorizationPolicy.CombineAsync(policyProvider, authorizeData, policies); - if (accessType != ResourceAccessType.Default) - return [accessType]; + bool hasAPIAccessRequirement = requirementData + .SelectMany(datum => datum.GetRequirements()) + .Concat(policy?.Requirements ?? []) + .Any(requirement => requirement is APIAccessRequirement); - HttpMethodMetadata? httpMethodMetadata = endpoint.Metadata - .GetMetadata(); + if (!hasAPIAccessRequirement) + continue; - IReadOnlyList httpMethods = httpMethodMetadata?.HttpMethods - ?? []; + IReadOnlyList accessAttributes = endpoint.Metadata + .GetOrderedMetadata(); - return httpMethods - .Select(accessAttributes.GetAccessType) - .Where(type => type != ResourceAccessType.None); + string resourceName = accessAttributes.GetResourceName(descriptor); + IEnumerable accessTypes = ToAccessTypes(endpoint, accessAttributes); + HashSet access = resourceAccessLookup.GetOrAdd(resourceName, _ => []); + access.UnionWith(accessTypes); } + + IEnumerable resources = resourceAccessLookup + .OrderBy(kvp => kvp.Key) + .Select(kvp => new AuthorizationResource + { + Type = "API", + Name = kvp.Key, + AccessTypes = kvp.Value.OrderBy(type => type) + }); + + return Ok(resources); + } /// @@ -273,6 +307,27 @@ public virtual IEnumerable CheckAccess([FromBody] ResourceAccessEntry[] ac // Static Methods + private static IEnumerable ToAccessTypes(Endpoint endpoint, IEnumerable accessAttributes) + { + ResourceAccessType accessType = accessAttributes.GetAccessType(); + + if (accessType == ResourceAccessType.None) + return []; + + if (accessType != ResourceAccessType.Default) + return [accessType]; + + HttpMethodMetadata? httpMethodMetadata = endpoint.Metadata + .GetMetadata(); + + IReadOnlyList httpMethods = httpMethodMetadata?.HttpMethods + ?? []; + + return httpMethods + .Select(accessAttributes.GetAccessType) + .Where(type => type != ResourceAccessType.None); + } + private static Regex? ToSearchPattern(string? searchText) { if (searchText is null) From 08af15a353ed4a6b78295fe8c530809e8fd7f63c Mon Sep 17 00:00:00 2001 From: Christoph Lackner Date: Fri, 7 Aug 2026 08:58:05 -0400 Subject: [PATCH 6/6] Simplified Code --- .../AuthorizationInfoControllerBase.cs | 74 +++++++------------ 1 file changed, 28 insertions(+), 46 deletions(-) diff --git a/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs b/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs index b5748648..f4fc1892 100644 --- a/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs +++ b/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs @@ -191,37 +191,7 @@ bool isSupported(string claimType) => claimsProvider [HttpGet, Route("resources")] public virtual async Task GetResources(IAuthorizationPolicyProvider policyProvider, EndpointDataSource endpointDataSource) { - Dictionary> resourceAccessLookup = []; - - foreach (Endpoint endpoint in endpointDataSource.Endpoints) - { - ControllerActionDescriptor? descriptor = endpoint.Metadata - .GetMetadata(); - - if (descriptor is null) - continue; - - IReadOnlyList authorizeData = endpoint.Metadata.GetOrderedMetadata() ?? []; - IReadOnlyList policies = endpoint.Metadata.GetOrderedMetadata() ?? []; - IReadOnlyList requirementData = endpoint.Metadata.GetOrderedMetadata() ?? []; - AuthorizationPolicy? policy = await AuthorizationPolicy.CombineAsync(policyProvider, authorizeData, policies); - - bool hasControllerAccessRequirement = requirementData - .SelectMany(datum => datum.GetRequirements()) - .Concat(policy?.Requirements ?? []) - .Any(requirement => requirement is ControllerAccessRequirement); - - if (!hasControllerAccessRequirement) - continue; - - IReadOnlyList accessAttributes = endpoint.Metadata - .GetOrderedMetadata(); - - string resourceName = accessAttributes.GetResourceName(descriptor); - IEnumerable accessTypes = ToAccessTypes(endpoint, accessAttributes); - HashSet access = resourceAccessLookup.GetOrAdd(resourceName, _ => []); - access.UnionWith(accessTypes); - } + Dictionary> resourceAccessLookup = await ResourceAccessLookup(policyProvider, endpointDataSource); IEnumerable resources = resourceAccessLookup .OrderBy(kvp => kvp.Key) @@ -244,6 +214,29 @@ public virtual async Task GetResources(IAuthorizationPolicyProvid /// A list of resources within the application. [HttpGet, Route("APIresources")] public virtual async Task GetAPIResources(IAuthorizationPolicyProvider policyProvider, EndpointDataSource endpointDataSource) + { + Dictionary> resourceAccessLookup = await ResourceAccessLookup(policyProvider, endpointDataSource); + + IEnumerable resources = resourceAccessLookup + .OrderBy(kvp => kvp.Key) + .Select(kvp => new AuthorizationResource + { + Type = "API", + Name = kvp.Key, + AccessTypes = kvp.Value.OrderBy(type => type) + }); + + return Ok(resources); + + } + + /// + /// Gets a list of resources available with the provided . + /// + /// Provides authorization policies defined within the application + /// Source for endpoint data used to look up controller and action metadata + /// A list of resources within the application. + private async Task>> ResourceAccessLookup(IAuthorizationPolicyProvider policyProvider, EndpointDataSource endpointDataSource) where T : IAuthorizationRequirement { Dictionary> resourceAccessLookup = []; @@ -260,12 +253,12 @@ public virtual async Task GetAPIResources(IAuthorizationPolicyPro IReadOnlyList requirementData = endpoint.Metadata.GetOrderedMetadata() ?? []; AuthorizationPolicy? policy = await AuthorizationPolicy.CombineAsync(policyProvider, authorizeData, policies); - bool hasAPIAccessRequirement = requirementData + bool hasAccessRequirement = requirementData .SelectMany(datum => datum.GetRequirements()) .Concat(policy?.Requirements ?? []) - .Any(requirement => requirement is APIAccessRequirement); + .Any(requirement => requirement is T); - if (!hasAPIAccessRequirement) + if (!hasAccessRequirement) continue; IReadOnlyList accessAttributes = endpoint.Metadata @@ -276,18 +269,7 @@ public virtual async Task GetAPIResources(IAuthorizationPolicyPro HashSet access = resourceAccessLookup.GetOrAdd(resourceName, _ => []); access.UnionWith(accessTypes); } - - IEnumerable resources = resourceAccessLookup - .OrderBy(kvp => kvp.Key) - .Select(kvp => new AuthorizationResource - { - Type = "API", - Name = kvp.Key, - AccessTypes = kvp.Value.OrderBy(type => type) - }); - - return Ok(resources); - + return resourceAccessLookup; } ///