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 +}