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