diff --git a/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs b/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs
index ae464eb4..f4fc1892 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 ]
@@ -169,6 +190,53 @@ bool isSupported(string claimType) => claimsProvider
/// A list of resources within the application.
[HttpGet, Route("resources")]
public virtual async Task GetResources(IAuthorizationPolicyProvider policyProvider, EndpointDataSource endpointDataSource)
+ {
+ Dictionary> resourceAccessLookup = await ResourceAccessLookup(policyProvider, endpointDataSource);
+
+ IEnumerable resources = resourceAccessLookup
+ .OrderBy(kvp => kvp.Key)
+ .Select(kvp => new AuthorizationResource
+ {
+ Type = "Controller",
+ Name = kvp.Key,
+ AccessTypes = kvp.Value.OrderBy(type => type)
+ });
+
+ return Ok(resources);
+
+ }
+
+ ///
+ /// 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 = 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 = [];
@@ -185,12 +253,12 @@ public virtual async Task GetResources(IAuthorizationPolicyProvid
IReadOnlyList requirementData = endpoint.Metadata.GetOrderedMetadata() ?? [];
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 accessAttributes = endpoint.Metadata
@@ -201,38 +269,7 @@ public virtual async Task GetResources(IAuthorizationPolicyProvid
HashSet 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 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);
- }
+ return resourceAccessLookup;
}
///
@@ -252,6 +289,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)
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);
diff --git a/src/Gemstone.Web/Security/APIAccessHandler.cs b/src/Gemstone.Web/Security/APIAccessHandler.cs
new file mode 100644
index 00000000..eebbe22f
--- /dev/null
+++ b/src/Gemstone.Web/Security/APIAccessHandler.cs
@@ -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;
+
+///
+/// Authorization handler for access to rest api actions.
+///
+public class APIAccessHandler : GemstoneAccessHandler
+{
+ ///
+ protected override string ResourceType => "API";
+}
+
+///
+/// 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..a4df0f93 100644
--- a/src/Gemstone.Web/Security/ControllerAccessHandler.cs
+++ b/src/Gemstone.Web/Security/ControllerAccessHandler.cs
@@ -21,150 +21,17 @@
//
//******************************************************************************************************
-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;
///
/// 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();
- }
-
- #endregion
+ protected override string ResourceType => "Controller";
}
///
diff --git a/src/Gemstone.Web/Security/GemstoneAccessHandler.cs b/src/Gemstone.Web/Security/GemstoneAccessHandler.cs
new file mode 100644
index 00000000..ac3f8337
--- /dev/null
+++ b/src/Gemstone.Web/Security/GemstoneAccessHandler.cs
@@ -0,0 +1,172 @@
+//******************************************************************************************************
+// 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 abstract class GemstoneAccessHandler : AuthorizationHandler where TRequirement : IAuthorizationRequirement
+{
+ #region [ Members ]
+
+ ///
+ /// Gets the type of resource handled by the authorization handler.
+ ///
+ protected abstract string ResourceType { get; }
+
+ // Nested Types
+ private enum Permission
+ {
+ Allow,
+ Deny,
+ Neither
+ }
+
+ private class ContextWrapper(AuthorizationHandlerContext context, TRequirement requirement, HttpContext httpContext, Endpoint endpoint, ControllerActionDescriptor descriptor)
+ {
+ private AuthorizationHandlerContext Context { get; } = context;
+ private TRequirement 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, TRequirement 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
+}