From d27f83b407c2339797664fb08810e44e2e8707d4 Mon Sep 17 00:00:00 2001 From: StephenCWills Date: Fri, 25 Jul 2025 10:32:11 -0400 Subject: [PATCH 01/30] Add middleware for Windows authentication provider --- .../APIController/ClaimsControllerBase.cs | 12 ++-- src/Gemstone.Web/Gemstone.Web.csproj | 1 + .../AuthenticationRuntimeMiddleware.cs | 57 +++++++++++++++++++ ...WindowsAuthenticationProviderMiddleware.cs | 55 ++++++++++++++++++ 4 files changed, 119 insertions(+), 6 deletions(-) create mode 100644 src/Gemstone.Web/Hosting/AuthenticationRuntimeMiddleware.cs create mode 100644 src/Gemstone.Web/Hosting/WindowsAuthenticationProviderMiddleware.cs diff --git a/src/Gemstone.Web/APIController/ClaimsControllerBase.cs b/src/Gemstone.Web/APIController/ClaimsControllerBase.cs index d7aac93..e9c3c1b 100644 --- a/src/Gemstone.Web/APIController/ClaimsControllerBase.cs +++ b/src/Gemstone.Web/APIController/ClaimsControllerBase.cs @@ -70,8 +70,8 @@ public IEnumerable GetClaims(string claimType) [HttpGet, Route("provider/{providerIdentity}/claimTypes")] public IActionResult GetClaimTypes(IServiceProvider serviceProvider, string providerIdentity) { - IAuthenticationClaimsProvider? claimsProvider = serviceProvider - .GetKeyedService(providerIdentity); + IAuthenticationProvider? claimsProvider = serviceProvider + .GetKeyedService(providerIdentity); return claimsProvider is not null ? Ok(claimsProvider.GetClaimTypes()) @@ -88,8 +88,8 @@ public IActionResult GetClaimTypes(IServiceProvider serviceProvider, string prov [HttpGet, Route("provider/{providerIdentity}/users")] public IActionResult FindUsers(IServiceProvider serviceProvider, string providerIdentity, string? searchText) { - IAuthenticationClaimsProvider? claimsProvider = serviceProvider - .GetKeyedService(providerIdentity); + IAuthenticationProvider? claimsProvider = serviceProvider + .GetKeyedService(providerIdentity); return claimsProvider is not null ? Ok(claimsProvider.FindUsers(searchText ?? "*")) @@ -107,8 +107,8 @@ public IActionResult FindUsers(IServiceProvider serviceProvider, string provider [HttpGet, Route("provider/{providerIdentity}/claims/{**claimType}")] public IActionResult FindClaims(IServiceProvider serviceProvider, string providerIdentity, string claimType, string? searchText) { - IAuthenticationClaimsProvider? claimsProvider = serviceProvider - .GetKeyedService(providerIdentity); + IAuthenticationProvider? claimsProvider = serviceProvider + .GetKeyedService(providerIdentity); return claimsProvider is not null ? Ok(claimsProvider.FindClaims(claimType, searchText ?? "*")) diff --git a/src/Gemstone.Web/Gemstone.Web.csproj b/src/Gemstone.Web/Gemstone.Web.csproj index a8d1e9b..dd9f46d 100644 --- a/src/Gemstone.Web/Gemstone.Web.csproj +++ b/src/Gemstone.Web/Gemstone.Web.csproj @@ -63,6 +63,7 @@ + diff --git a/src/Gemstone.Web/Hosting/AuthenticationRuntimeMiddleware.cs b/src/Gemstone.Web/Hosting/AuthenticationRuntimeMiddleware.cs new file mode 100644 index 0000000..d036787 --- /dev/null +++ b/src/Gemstone.Web/Hosting/AuthenticationRuntimeMiddleware.cs @@ -0,0 +1,57 @@ +//****************************************************************************************************** +// AuthenticationRuntimeMiddleware.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/23/2025 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Gemstone.Security.AuthenticationProviders; +using Microsoft.AspNetCore.Http; + +namespace Gemstone.Web.Hosting; + +/// +/// Represents a middleware used to assign claims to authenticated users. +/// +/// Delegate used to invoke the next middleware in the pipeline +/// The authentication runtime used to query assigned claims +/// The identity of the provider that authenticated the user +public class AuthenticationRuntimeMiddleware(RequestDelegate next, IAuthenticationRuntime runtime, string providerIdentity) +{ + private RequestDelegate Next { get; } = next; + private IAuthenticationRuntime Runtime { get; } = runtime; + private string ProviderIdentity { get; } = providerIdentity; + + /// + /// Assigns claims from the authentication runtime to the authenticated user. + /// + /// The context of the HTTP request + public async Task Invoke(HttpContext httpContext) + { + ClaimsPrincipal user = httpContext.User; + ClaimsIdentity newIdentity = new(user.Identity, user.Claims); + IEnumerable assignedClaims = Runtime.GetAssignedClaims(ProviderIdentity, user); + newIdentity.AddClaims(assignedClaims); + httpContext.User = new(newIdentity); + await Next(httpContext); + } +} diff --git a/src/Gemstone.Web/Hosting/WindowsAuthenticationProviderMiddleware.cs b/src/Gemstone.Web/Hosting/WindowsAuthenticationProviderMiddleware.cs new file mode 100644 index 0000000..3986140 --- /dev/null +++ b/src/Gemstone.Web/Hosting/WindowsAuthenticationProviderMiddleware.cs @@ -0,0 +1,55 @@ +//****************************************************************************************************** +// WindowsAuthenticationProviderMiddleware.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/16/2025 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Negotiate; +using Microsoft.AspNetCore.Http; + +namespace Gemstone.Web.Hosting; + +/// +/// Requires Windows authentication in a web application. +/// +/// The next middleware in the pipeline +public class WindowsAuthenticationProviderMiddleware(RequestDelegate next) +{ + private RequestDelegate Next { get; } = next; + + /// + /// Authenticates the user using Windows authentication. + /// + /// The context of the HTTP request + public async Task Invoke(HttpContext httpContext) + { + AuthenticateResult result = await httpContext.AuthenticateAsync(NegotiateDefaults.AuthenticationScheme); + + if (!result.Succeeded) + { + await httpContext.ChallengeAsync(NegotiateDefaults.AuthenticationScheme); + return; + } + + await Next(httpContext); + } +} From 1cb4122ec525d2066d3860f0c4fb30055cf644cb Mon Sep 17 00:00:00 2001 From: StephenCWills Date: Fri, 25 Jul 2025 14:38:37 -0400 Subject: [PATCH 02/30] Adds a builder to set up the authentication in the request pipeline --- .../Hosting/IAuthenticationWebBuilder.cs | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 src/Gemstone.Web/Hosting/IAuthenticationWebBuilder.cs diff --git a/src/Gemstone.Web/Hosting/IAuthenticationWebBuilder.cs b/src/Gemstone.Web/Hosting/IAuthenticationWebBuilder.cs new file mode 100644 index 0000000..f3a2e72 --- /dev/null +++ b/src/Gemstone.Web/Hosting/IAuthenticationWebBuilder.cs @@ -0,0 +1,180 @@ +//****************************************************************************************************** +// IAuthenticationWebBuilder.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/25/2025 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.Collections.Generic; +using Gemstone.Security.AuthenticationProviders; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Gemstone.Web.Hosting; + +/// +/// Represents a builder for Gemstone authentication web components. +/// +public interface IAuthenticationWebBuilder +{ + /// + /// Adds the default middleware type for the given provider identity to the application's request pipeline. + /// + /// The identity of the authentication provider + /// The authentication web builder. + IAuthenticationWebBuilder UseProviderMiddleware(string providerIdentity); + + /// + /// Adds the default middleware type for the given provider identity to the application's request pipeline. + /// + /// The identity of the authentication provider + /// The path on which requests will require authentication using this provider + /// The authentication web builder. + IAuthenticationWebBuilder UseProviderMiddleware(string providerIdentity, string pathMatch); + + /// + /// Adds the given middleware type for the given provider identity to the application's request pipeline. + /// + /// The identity of the authentication provider + /// The type of middleware to be used for authenticating the request + /// The authentication web builder. + IAuthenticationWebBuilder UseProviderMiddleware(string providerIdentity, Type middlewareType); + + /// + /// Adds the given middleware type for the given provider identity to the application's request pipeline. + /// + /// The identity of the authentication provider + /// The path on which requests will require authentication using this provider + /// The type of middleware to be used for authenticating the request + /// The authentication web builder. + IAuthenticationWebBuilder UseProviderMiddleware(string providerIdentity, string pathMatch, Type middlewareType); + + /// + /// Adds the given middleware type for the given provider identity to the application's request pipeline. + /// + /// The type of middleware to be used for authenticating the request + /// The identity of the authentication provider + /// The authentication web builder. + IAuthenticationWebBuilder UseProviderMiddleware(string providerIdentity); + + /// + /// Adds the given middleware type for the given provider identity to the application's request pipeline. + /// + /// The type of middleware to be used for authenticating the request + /// The identity of the authentication provider + /// The path on which requests will require authentication using this provider + /// The authentication web builder. + IAuthenticationWebBuilder UseProviderMiddleware(string providerIdentity, string pathMatch); +} + +/// +/// Extensions for the authentication web builder. +/// +public static class AuthenticationWebBuilderExtensions +{ + private class AuthenticationWebBuilder(IApplicationBuilder app) : IAuthenticationWebBuilder + { + private static Dictionary MiddlewareRegistry { get; } = new Dictionary() { + { "windows", ("/asi/auth/windows", typeof(WindowsAuthenticationProviderMiddleware)) } + }; + + public IAuthenticationWebBuilder UseProviderMiddleware(string providerIdentity) + { + (string endpoint, Type middlewareType) = FindMiddlewareDescriptor(providerIdentity); + return UseProviderMiddleware(providerIdentity, endpoint, middlewareType); + } + + public IAuthenticationWebBuilder UseProviderMiddleware(string providerIdentity, string endpoint) + { + (_, Type middlewareType) = FindMiddlewareDescriptor(providerIdentity); + return UseProviderMiddleware(providerIdentity, endpoint, middlewareType); + } + + public IAuthenticationWebBuilder UseProviderMiddleware(string providerIdentity, Type middlewareType) + { + (string endpoint, _) = FindMiddlewareDescriptor(providerIdentity); + return UseProviderMiddleware(providerIdentity, endpoint, middlewareType); + } + + public IAuthenticationWebBuilder UseProviderMiddleware(string providerIdentity, string endpoint, Type middlewareType) + { + app.Map(endpoint, branch => branch + .UseMiddleware(middlewareType) + .UseMiddleware(providerIdentity)); + + return this; + } + + public IAuthenticationWebBuilder UseProviderMiddleware(string providerIdentity) + { + (string endpoint, _) = FindMiddlewareDescriptor(providerIdentity); + return UseProviderMiddleware(providerIdentity, endpoint); + } + + public IAuthenticationWebBuilder UseProviderMiddleware(string providerIdentity, string endpoint) + { + app.Map(endpoint, branch => branch + .UseMiddleware() + .UseMiddleware(providerIdentity)); + + return this; + } + + private static (string, Type) FindMiddlewareDescriptor(string providerIdentity) + { + if (!MiddlewareRegistry.TryGetValue(providerIdentity, out (string, Type) descriptor)) + throw new KeyNotFoundException($"Provider \"{providerIdentity}\" is not recognized"); + + return descriptor; + } + } + + /// + /// Automatically configures the request pipeline to support well-known authentication providers. + /// + /// The application builder + /// The application builder. + public static IApplicationBuilder UseGemstoneAuthentication(this IApplicationBuilder app) + { + return app.UseGemstoneAuthentication(configure); + + void configure(IAuthenticationWebBuilder builder) + { + IAuthenticationRuntime runtime = app.ApplicationServices.GetRequiredService(); + + foreach (string providerIdentity in runtime.GetProviderIdentities()) + builder.UseProviderMiddleware(providerIdentity); + } + } + + /// + /// Configures the request pipeline to support authentication providers using the given configuration method. + /// + /// The application builder + /// The method to configure authentication provider middleware + /// The application builder. + public static IApplicationBuilder UseGemstoneAuthentication(this IApplicationBuilder app, Action configure) + { + AuthenticationWebBuilder builder = new(app); + configure(builder); + app.UseMiddleware(); + return app; + } +} From f5a10df6b608e6ebddbafcb9fc634c9a853b9a6d Mon Sep 17 00:00:00 2001 From: StephenCWills Date: Fri, 25 Jul 2025 15:07:54 -0400 Subject: [PATCH 03/30] Move security-related classes from Hosting to Security --- .../{Hosting => Security}/AuthenticationRuntimeMiddleware.cs | 2 +- .../{Hosting => Security}/AuthenticationSessionMiddleware.cs | 2 +- .../{Hosting => Security}/IAuthenticationWebBuilder.cs | 2 +- .../WindowsAuthenticationProviderMiddleware.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename src/Gemstone.Web/{Hosting => Security}/AuthenticationRuntimeMiddleware.cs (98%) rename src/Gemstone.Web/{Hosting => Security}/AuthenticationSessionMiddleware.cs (99%) rename src/Gemstone.Web/{Hosting => Security}/IAuthenticationWebBuilder.cs (99%) rename src/Gemstone.Web/{Hosting => Security}/WindowsAuthenticationProviderMiddleware.cs (98%) diff --git a/src/Gemstone.Web/Hosting/AuthenticationRuntimeMiddleware.cs b/src/Gemstone.Web/Security/AuthenticationRuntimeMiddleware.cs similarity index 98% rename from src/Gemstone.Web/Hosting/AuthenticationRuntimeMiddleware.cs rename to src/Gemstone.Web/Security/AuthenticationRuntimeMiddleware.cs index d036787..e02b701 100644 --- a/src/Gemstone.Web/Hosting/AuthenticationRuntimeMiddleware.cs +++ b/src/Gemstone.Web/Security/AuthenticationRuntimeMiddleware.cs @@ -27,7 +27,7 @@ using Gemstone.Security.AuthenticationProviders; using Microsoft.AspNetCore.Http; -namespace Gemstone.Web.Hosting; +namespace Gemstone.Web.Security; /// /// Represents a middleware used to assign claims to authenticated users. diff --git a/src/Gemstone.Web/Hosting/AuthenticationSessionMiddleware.cs b/src/Gemstone.Web/Security/AuthenticationSessionMiddleware.cs similarity index 99% rename from src/Gemstone.Web/Hosting/AuthenticationSessionMiddleware.cs rename to src/Gemstone.Web/Security/AuthenticationSessionMiddleware.cs index ec712c8..8b7684e 100644 --- a/src/Gemstone.Web/Hosting/AuthenticationSessionMiddleware.cs +++ b/src/Gemstone.Web/Security/AuthenticationSessionMiddleware.cs @@ -31,7 +31,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -namespace Gemstone.Web.Hosting; +namespace Gemstone.Web.Security; /// /// Represents configuration options for the . diff --git a/src/Gemstone.Web/Hosting/IAuthenticationWebBuilder.cs b/src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs similarity index 99% rename from src/Gemstone.Web/Hosting/IAuthenticationWebBuilder.cs rename to src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs index f3a2e72..b9a296f 100644 --- a/src/Gemstone.Web/Hosting/IAuthenticationWebBuilder.cs +++ b/src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs @@ -27,7 +27,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; -namespace Gemstone.Web.Hosting; +namespace Gemstone.Web.Security; /// /// Represents a builder for Gemstone authentication web components. diff --git a/src/Gemstone.Web/Hosting/WindowsAuthenticationProviderMiddleware.cs b/src/Gemstone.Web/Security/WindowsAuthenticationProviderMiddleware.cs similarity index 98% rename from src/Gemstone.Web/Hosting/WindowsAuthenticationProviderMiddleware.cs rename to src/Gemstone.Web/Security/WindowsAuthenticationProviderMiddleware.cs index 3986140..73d0a49 100644 --- a/src/Gemstone.Web/Hosting/WindowsAuthenticationProviderMiddleware.cs +++ b/src/Gemstone.Web/Security/WindowsAuthenticationProviderMiddleware.cs @@ -26,7 +26,7 @@ using Microsoft.AspNetCore.Authentication.Negotiate; using Microsoft.AspNetCore.Http; -namespace Gemstone.Web.Hosting; +namespace Gemstone.Web.Security; /// /// Requires Windows authentication in a web application. From 3757a128b6d0de165c96be328dc5d870f1b41028 Mon Sep 17 00:00:00 2001 From: StephenCWills Date: Thu, 31 Jul 2025 13:24:38 -0400 Subject: [PATCH 04/30] Authorization handler for controller actions --- .../Security/ControllerAccessHandler.cs | 191 ++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 src/Gemstone.Web/Security/ControllerAccessHandler.cs diff --git a/src/Gemstone.Web/Security/ControllerAccessHandler.cs b/src/Gemstone.Web/Security/ControllerAccessHandler.cs new file mode 100644 index 0000000..99c77b1 --- /dev/null +++ b/src/Gemstone.Web/Security/ControllerAccessHandler.cs @@ -0,0 +1,191 @@ +//****************************************************************************************************** +// 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.Linq; +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 +{ + #region [ Members ] + + // Nested Types + private enum Permission + { + Allow, + Deny, + Neither + } + + private class ContextWrapper(AuthorizationHandlerContext context, ControllerAccessRequirement requirement, 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 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 + .OfType() + .FirstOrDefault(); + + if (descriptor is null) + return Task.CompletedTask; + + ContextWrapper wrapper = new(context, requirement, endpoint, descriptor); + + if (HandleResourceActionPermission(wrapper)) + return Task.CompletedTask; + + HandleResourceAccessPermission(wrapper); + return Task.CompletedTask; + } + + private bool HandleResourceActionPermission(ContextWrapper wrapper) + { + string? routeName = wrapper.Endpoint.Metadata + .OfType() + .Select(metadata => metadata.RouteName) + .FirstOrDefault(); + + 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 void HandleResourceAccessPermission(ContextWrapper wrapper) + { + ILookup accessClaims = wrapper.Endpoint.Metadata + .OfType() + .SelectMany(attribute => attribute.Access, (attribute, accessLevel) => $"Controller {attribute.Name} {accessLevel}") + .ToLookup(claimValue => GetResourceAccessPermission(wrapper.User, claimValue)); + + bool fail = accessClaims[Permission.Deny] + .Select(ToFailureReason) + .Select(wrapper.Fail) + .DefaultIfEmpty(false) + .All(b => b); + + if (fail) + return; + + if (accessClaims[Permission.Allow].Any()) + wrapper.Succeed(); + } + + 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) + { + return GetResourcePermission(user, "Gemstone.ResourceAction", claimValue); + } + + private static Permission GetResourceAccessPermission(ClaimsPrincipal user, string claimValue) + { + return GetResourcePermission(user, "Gemstone.ResourceAccess", claimValue); + } + + private static Permission GetResourcePermission(ClaimsPrincipal user, string claimTypePrefix, string claimValue) + { + string allowClaim = $"{claimTypePrefix}.Allow"; + string denyClaim = $"{claimTypePrefix}.Deny"; + + if (user.HasClaim(denyClaim, claimValue)) + return Permission.Allow; + + return user.HasClaim(allowClaim, claimValue) + ? Permission.Deny + : Permission.Neither; + } + + #endregion +} + +/// +/// Requirement to be handled by the . +/// +public class ControllerAccessRequirement : IAuthorizationRequirement +{ +} From cf8d5886b05baf60385ace6e8734e07d0c92b934 Mon Sep 17 00:00:00 2001 From: StephenCWills Date: Wed, 6 Aug 2025 15:57:16 -0400 Subject: [PATCH 05/30] Use cookie authentication instead of session middleware --- .../AuthenticationSessionMiddleware.cs | 280 ------------------ .../Security/DefaultTicketStore.cs | 94 ++++++ .../Security/IAuthenticationWebBuilder.cs | 168 ++++++----- 3 files changed, 188 insertions(+), 354 deletions(-) delete mode 100644 src/Gemstone.Web/Security/AuthenticationSessionMiddleware.cs create mode 100644 src/Gemstone.Web/Security/DefaultTicketStore.cs diff --git a/src/Gemstone.Web/Security/AuthenticationSessionMiddleware.cs b/src/Gemstone.Web/Security/AuthenticationSessionMiddleware.cs deleted file mode 100644 index 8b7684e..0000000 --- a/src/Gemstone.Web/Security/AuthenticationSessionMiddleware.cs +++ /dev/null @@ -1,280 +0,0 @@ -//****************************************************************************************************** -// AuthenticationSessionMiddleware.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: -// ---------------------------------------------------------------------------------------------------- -// 04/18/2025 - Stephen C. Wills -// Generated original version of source code. -// -//****************************************************************************************************** - -using System; -using System.Security.Claims; -using System.Security.Cryptography; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; - -namespace Gemstone.Web.Security; - -/// -/// Represents configuration options for the . -/// -public class AuthenticationSessionOptions -{ - /// - /// Gets or sets the base path of the application to which the cookie will be scoped. - /// - public string ApplicationBasePath { get; set; } = "/"; - - /// - /// Gets or sets the name of the cookie used for storing the authentication session token. - /// - public string AuthenticationSessionCookie { get; set; } = "x-gemstone-auth"; - - /// - /// Gets or sets the amount of time the server will continue to store - /// the authentication session since the last request made by the user. - /// - public double IdleAuthenticationTokenExpirationMinutes { get; set; } = 15.0D; - - /// - /// Gets or sets the amount of time after the authenticated - /// session is established before the session cookie expires. - /// - public double AuthenticationSessionExpirationHours { get; set; } = 24.0D; - - /// - /// Gets the amount of time the server will continue to store the - /// authentication session since the last request made by the user. - /// - public TimeSpan AuthenticationTokenExpiration => - TimeSpan.FromMinutes(IdleAuthenticationTokenExpirationMinutes); - - /// - /// Gets the amount of time after the authenticated session - /// is established before the session cookie expires. - /// - public DateTimeOffset AuthenticationCookieExpiration => - DateTime.UtcNow.AddHours(AuthenticationSessionExpirationHours); -} - -/// -/// Represents a middleware used to establish an authenticated session between the client and server. -/// -/// Delegate used to invoke the next middleware in the pipeline. -/// Cache used to store session data. -/// Options used to configure how the session is managed. -public class AuthenticationSessionMiddleware(RequestDelegate next, IMemoryCache memoryCache, IOptionsMonitor options) -{ - #region [ Properties ] - - private RequestDelegate Next { get; } = next; - private IMemoryCache MemoryCache { get; } = memoryCache; - private AuthenticationSessionOptions Options => options.CurrentValue; - - #endregion - - #region [ Methods ] - - /// - /// Handles an HTTP request. - /// - /// The object providing context about the request. - /// Task that indicates when the middleware is finished processing the request. - public async Task Invoke(HttpContext httpContext) - { - AuthenticationSessionOptions options = Options; - - if (httpContext.Request.Headers.ContainsKey("Authorization")) - await HandleRequestWithCredentialsAsync(httpContext, options); - else - await HandleRequestAsync(httpContext, options); - } - - // If a request with credentials is authenticated, create a new session and provide a session cookie. - // If a request with credentials is not authenticated, flush session data and expire the session cookie. - private async Task HandleRequestWithCredentialsAsync(HttpContext httpContext, AuthenticationSessionOptions options) - { - await Next(httpContext); - - string? oldAuthenticationToken = ReadAuthenticationCookie(httpContext, options); - - if (oldAuthenticationToken is not null) - FlushSessionPrincipal(oldAuthenticationToken); - - if (httpContext.User.Identity?.IsAuthenticated == true) - HandleAuthenticatedRequest(httpContext, options); - else - ExpireAuthenticationCookie(httpContext, options); - } - - // Request without credentials will attempt to use the session to authenticate the request. - private async Task HandleRequestAsync(HttpContext httpContext, AuthenticationSessionOptions options) - { - string? authenticationToken = ReadAuthenticationCookie(httpContext, options); - - if (authenticationToken is null) - { - await Next(httpContext); - return; - } - - ClaimsPrincipal? sessionPrincipal = FetchSessionPrincipal(authenticationToken); - - if (sessionPrincipal is null) - { - await Next(httpContext); - return; - } - - httpContext.User = sessionPrincipal; - await Next(httpContext); - UpdateSessionPrincipal(authenticationToken, sessionPrincipal, options); - } - - // Establishes a new session with the client. - private void HandleAuthenticatedRequest(HttpContext httpContext, AuthenticationSessionOptions options) - { - string newAuthenticationToken = GenerateNewAuthenticationToken(); - ClaimsPrincipal sessionPrincipal = BuildSessionPrincipal(httpContext.User); - UpdateSessionPrincipal(newAuthenticationToken, sessionPrincipal, options); - UpdateAuthenticationCookie(httpContext, newAuthenticationToken, options); - } - - // Retrieve session data from the server-side cache. - private ClaimsPrincipal? FetchSessionPrincipal(string authenticationToken) - { - return MemoryCache.TryGetValue(authenticationToken, out ClaimsPrincipal? principal) - ? principal : null; - } - - // Updates the server-side cache entry and resets the expiration window. - private void UpdateSessionPrincipal(string authenticationToken, ClaimsPrincipal sessionPrincipal, AuthenticationSessionOptions options) - { - MemoryCache - .CreateEntry(authenticationToken) - .SetValue(sessionPrincipal) - .SetSlidingExpiration(options.AuthenticationTokenExpiration) - .Dispose(); - } - - // Removes session data from the server-side cache. - private void FlushSessionPrincipal(string authenticationToken) - { - MemoryCache.Remove(authenticationToken); - } - - // Prepares the cookie with the session token in the response to send it to the client. - private void UpdateAuthenticationCookie(HttpContext httpContext, string authenticationToken, AuthenticationSessionOptions options) - { - CookieOptions cookieOptions = new() - { - Path = Options.ApplicationBasePath, - HttpOnly = true, - Secure = true, - IsEssential = true, - Expires = options.AuthenticationCookieExpiration - }; - - httpContext.Response.Cookies.Append(options.AuthenticationSessionCookie, authenticationToken, cookieOptions); - } - - #endregion - - #region [ Static ] - - // Static Methods - - // Reads the session token from the cookie provided by the client. - private static string? ReadAuthenticationCookie(HttpContext httpContext, AuthenticationSessionOptions options) - { - return httpContext.Request.Cookies.TryGetValue(options.AuthenticationSessionCookie, out string? token) - ? token : null; - } - - // Generates a token the server can use to look up the session principal. - private static string GenerateNewAuthenticationToken() - { - byte[] tokenData = new byte[128]; - RandomNumberGenerator.Fill(tokenData); - return Convert.ToBase64String(tokenData); - } - - // Copies the claims from the authenticated request's principal into a new - // principal that will persist across requests within the same session. - private static ClaimsPrincipal BuildSessionPrincipal(ClaimsPrincipal httpPrincipal) - { - ClaimsIdentity sessionIdentity = new(httpPrincipal.Identity, httpPrincipal.Claims); - ClaimsPrincipal sessionPrincipal = new(sessionIdentity); - sessionIdentity.AddClaim(new(ClaimTypes.Role, "Session")); - return sessionPrincipal; - } - - // If the client provided a cookie, indicate to the client that the - // cookie is no longer valid by sending an expired cookie to replace it. - private static void ExpireAuthenticationCookie(HttpContext httpContext, AuthenticationSessionOptions options) - { - string authenticationCookie = options.AuthenticationSessionCookie; - - if (httpContext.Request.Cookies.ContainsKey(authenticationCookie)) - httpContext.Response.Cookies.Delete(authenticationCookie); - } - - #endregion -} - -/// -/// Extension methods for setting up the . -/// -public static class AuthenticationSessionMiddlewareExtensions -{ - /// - /// Registers the services required by the . - /// - /// Collection of services used by the app. - /// Collection of services used by the app. - public static IServiceCollection AddAuthenticationCache(this IServiceCollection services) - { - return services.AddMemoryCache(); - } - - /// - /// Registers the services required by the . - /// - /// Collection of services used by the app. - /// The action used to configure the options. - /// Collection of services used by the app. - public static IServiceCollection AddAuthenticationCache(this IServiceCollection services, Action configureOptions) - { - return services - .Configure(configureOptions) - .AddAuthenticationCache(); - } - - /// - /// Registers the in the app pipeline. - /// - /// The app builder with which to register the middleware. - /// The app builder with middleware registered. - public static IApplicationBuilder UseAuthenticationCache(this IApplicationBuilder app) - { - return app.UseMiddleware(); - } -} diff --git a/src/Gemstone.Web/Security/DefaultTicketStore.cs b/src/Gemstone.Web/Security/DefaultTicketStore.cs new file mode 100644 index 0000000..4a3c74d --- /dev/null +++ b/src/Gemstone.Web/Security/DefaultTicketStore.cs @@ -0,0 +1,94 @@ +//****************************************************************************************************** +// DefaultTicketStore.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: +// ---------------------------------------------------------------------------------------------------- +// 08/05/2025 - Stephen C. Wills +// Generated original version of source code. +// +//****************************************************************************************************** + +using System; +using System.Security.Cryptography; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.Extensions.Caching.Memory; + +namespace Gemstone.Web.Security; + +/// +/// Represents an in-memory storage mechanism for authentication tickets. +/// +/// The cache in which tickets will be stored +public class DefaultTicketStore(IMemoryCache memoryCache) : ITicketStore +{ + private IMemoryCache MemoryCache { get; } = memoryCache; + + /// + public Task StoreAsync(AuthenticationTicket ticket) + { + string key = GenerateNewSessionToken(); + UpdateEntry(key, ticket); + return Task.FromResult(key); + } + + /// + public Task RenewAsync(string key, AuthenticationTicket ticket) + { + UpdateEntry(key, ticket); + return Task.CompletedTask; + } + + /// + public Task RetrieveAsync(string key) + { + if (!MemoryCache.TryGetValue(key, out AuthenticationTicket? ticket)) + ticket = null; + + return Task.FromResult(ticket); + } + + /// + public Task RemoveAsync(string key) + { + MemoryCache.Remove(key); + return Task.CompletedTask; + } + + private void UpdateEntry(string key, AuthenticationTicket ticket) + { + DateTimeOffset? expiration = ticket.Properties.ExpiresUtc; + MemoryCacheEntryOptions options = new(); + + if (expiration is not null) + options.AbsoluteExpiration = ticket.Properties.ExpiresUtc; + else + options.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(24); + + MemoryCache + .CreateEntry(key) + .SetOptions(options) + .Dispose(); + } + + private static string GenerateNewSessionToken() + { + byte[] tokenData = new byte[128]; + RandomNumberGenerator.Fill(tokenData); + return Convert.ToBase64String(tokenData); + } +} diff --git a/src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs b/src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs index b9a296f..f35f975 100644 --- a/src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs +++ b/src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs @@ -23,9 +23,17 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Gemstone.Security.AuthenticationProviders; +using MathNet.Numerics; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; namespace Gemstone.Web.Security; @@ -39,7 +47,7 @@ public interface IAuthenticationWebBuilder /// /// The identity of the authentication provider /// The authentication web builder. - IAuthenticationWebBuilder UseProviderMiddleware(string providerIdentity); + IAuthenticationWebBuilder UseProvider(string providerIdentity); /// /// Adds the default middleware type for the given provider identity to the application's request pipeline. @@ -47,41 +55,16 @@ public interface IAuthenticationWebBuilder /// The identity of the authentication provider /// The path on which requests will require authentication using this provider /// The authentication web builder. - IAuthenticationWebBuilder UseProviderMiddleware(string providerIdentity, string pathMatch); + IAuthenticationWebBuilder UseProvider(string providerIdentity, string pathMatch); /// - /// Adds the given middleware type for the given provider identity to the application's request pipeline. - /// - /// The identity of the authentication provider - /// The type of middleware to be used for authenticating the request - /// The authentication web builder. - IAuthenticationWebBuilder UseProviderMiddleware(string providerIdentity, Type middlewareType); - - /// - /// Adds the given middleware type for the given provider identity to the application's request pipeline. - /// - /// The identity of the authentication provider - /// The path on which requests will require authentication using this provider - /// The type of middleware to be used for authenticating the request - /// The authentication web builder. - IAuthenticationWebBuilder UseProviderMiddleware(string providerIdentity, string pathMatch, Type middlewareType); - - /// - /// Adds the given middleware type for the given provider identity to the application's request pipeline. - /// - /// The type of middleware to be used for authenticating the request - /// The identity of the authentication provider - /// The authentication web builder. - IAuthenticationWebBuilder UseProviderMiddleware(string providerIdentity); - - /// - /// Adds the given middleware type for the given provider identity to the application's request pipeline. + /// Adds the default middleware type for the given provider identity to the application's request pipeline. /// - /// The type of middleware to be used for authenticating the request /// The identity of the authentication provider /// The path on which requests will require authentication using this provider + /// The authentication scheme to require on the endpoint /// The authentication web builder. - IAuthenticationWebBuilder UseProviderMiddleware(string providerIdentity, string pathMatch); + IAuthenticationWebBuilder UseProvider(string providerIdentity, string pathMatch, string scheme); } /// @@ -91,59 +74,66 @@ public static class AuthenticationWebBuilderExtensions { private class AuthenticationWebBuilder(IApplicationBuilder app) : IAuthenticationWebBuilder { - private static Dictionary MiddlewareRegistry { get; } = new Dictionary() { - { "windows", ("/asi/auth/windows", typeof(WindowsAuthenticationProviderMiddleware)) } - }; - - public IAuthenticationWebBuilder UseProviderMiddleware(string providerIdentity) - { - (string endpoint, Type middlewareType) = FindMiddlewareDescriptor(providerIdentity); - return UseProviderMiddleware(providerIdentity, endpoint, middlewareType); - } - - public IAuthenticationWebBuilder UseProviderMiddleware(string providerIdentity, string endpoint) + public IAuthenticationWebBuilder UseProvider(string providerIdentity) { - (_, Type middlewareType) = FindMiddlewareDescriptor(providerIdentity); - return UseProviderMiddleware(providerIdentity, endpoint, middlewareType); + string endpoint = $"/asi/auth/{providerIdentity}"; + return UseProvider(providerIdentity, endpoint); } - public IAuthenticationWebBuilder UseProviderMiddleware(string providerIdentity, Type middlewareType) + public IAuthenticationWebBuilder UseProvider(string providerIdentity, string pathMatch) { - (string endpoint, _) = FindMiddlewareDescriptor(providerIdentity); - return UseProviderMiddleware(providerIdentity, endpoint, middlewareType); + return UseProvider(providerIdentity, pathMatch, providerIdentity); } - public IAuthenticationWebBuilder UseProviderMiddleware(string providerIdentity, string endpoint, Type middlewareType) + public IAuthenticationWebBuilder UseProvider(string providerIdentity, string pathMatch, string scheme) { - app.Map(endpoint, branch => branch - .UseMiddleware(middlewareType) - .UseMiddleware(providerIdentity)); - - return this; - } - - public IAuthenticationWebBuilder UseProviderMiddleware(string providerIdentity) - { - (string endpoint, _) = FindMiddlewareDescriptor(providerIdentity); - return UseProviderMiddleware(providerIdentity, endpoint); - } - - public IAuthenticationWebBuilder UseProviderMiddleware(string providerIdentity, string endpoint) - { - app.Map(endpoint, branch => branch - .UseMiddleware() - .UseMiddleware(providerIdentity)); + app.Map(pathMatch, branch => branch + .UseRouting() + .UseAuthorization() + .UseMiddleware(providerIdentity) + .UseEndpoints(endpoints => + { + IEndpointConventionBuilder conventions = endpoints.MapGet("/", async context => + { + await context.SignInAsync(context.User); + + string? returnURL = context.Request.Query["redir"]; + context.Response.Redirect(returnURL ?? "/"); + }); + + conventions.RequireAuthorization(policy => policy + .AddAuthenticationSchemes(scheme) + .RequireAuthenticatedUser()); + })); return this; } + } - private static (string, Type) FindMiddlewareDescriptor(string providerIdentity) - { - if (!MiddlewareRegistry.TryGetValue(providerIdentity, out (string, Type) descriptor)) - throw new KeyNotFoundException($"Provider \"{providerIdentity}\" is not recognized"); + /// + /// Sets up the default configuration for services to support Gemstone authentication. + /// + /// Provides setup information for the runtime. + /// The collection of services + /// Builder for adding additional authentication schemes. + public static AuthenticationBuilder ConfigureGemstoneWebAuthentication(this IServiceCollection services) where T : class, IAuthenticationSetup + { + return services + .AddGemstoneAuthentication() + .ConfigureGemstoneWebDefaults(); + } - return descriptor; - } + /// + /// Sets up the default configuration for services to support Gemstone authentication. + /// + /// The collection of services + /// Method to configure the runtime + /// Builder for adding additional authentication schemes. + public static AuthenticationBuilder ConfigureGemstoneWebAuthentication(this IServiceCollection services, Action configure) + { + return services + .AddGemstoneAuthentication(configure) + .ConfigureGemstoneWebDefaults(); } /// @@ -160,7 +150,7 @@ void configure(IAuthenticationWebBuilder builder) IAuthenticationRuntime runtime = app.ApplicationServices.GetRequiredService(); foreach (string providerIdentity in runtime.GetProviderIdentities()) - builder.UseProviderMiddleware(providerIdentity); + builder.UseProvider(providerIdentity); } } @@ -172,9 +162,39 @@ void configure(IAuthenticationWebBuilder builder) /// The application builder. public static IApplicationBuilder UseGemstoneAuthentication(this IApplicationBuilder app, Action configure) { - AuthenticationWebBuilder builder = new(app); + AuthenticationWebBuilder builder = new(app.UseAuthentication()); configure(builder); - app.UseMiddleware(); return app; } + + private static AuthenticationBuilder ConfigureGemstoneWebDefaults(this IServiceCollection services) + { + services + .AddMemoryCache() + .TryAddSingleton(); + + services + .AddOptions() + .Configure((options, sessionStore) => + { + options.SessionStore = sessionStore; + + options.ExpireTimeSpan = TimeSpan.FromMinutes(15); + options.SlidingExpiration = true; + options.LoginPath = "/Login"; + options.ReturnUrlParameter = "redir"; + + options.Cookie.Name = "x-gemstone-auth"; + options.Cookie.Path = "/"; + options.Cookie.HttpOnly = true; + options.Cookie.SecurePolicy = CookieSecurePolicy.Always; + options.Cookie.IsEssential = true; + }); + + return services + .AddWindowsAuthenticationProvider() + .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) + .AddNegotiate("windows", _ => { }) + .AddCookie(); + } } From 5fb1fbd637680ff5d38656249ac8edb47df2471b Mon Sep 17 00:00:00 2001 From: StephenCWills Date: Wed, 6 Aug 2025 17:58:35 -0400 Subject: [PATCH 06/30] Fix errors in cookie authentication --- src/Gemstone.Web/Security/DefaultTicketStore.cs | 1 + src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Gemstone.Web/Security/DefaultTicketStore.cs b/src/Gemstone.Web/Security/DefaultTicketStore.cs index 4a3c74d..4172f4e 100644 --- a/src/Gemstone.Web/Security/DefaultTicketStore.cs +++ b/src/Gemstone.Web/Security/DefaultTicketStore.cs @@ -82,6 +82,7 @@ private void UpdateEntry(string key, AuthenticationTicket ticket) MemoryCache .CreateEntry(key) .SetOptions(options) + .SetValue(ticket) .Dispose(); } diff --git a/src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs b/src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs index f35f975..5ae036d 100644 --- a/src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs +++ b/src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs @@ -174,7 +174,7 @@ private static AuthenticationBuilder ConfigureGemstoneWebDefaults(this IServiceC .TryAddSingleton(); services - .AddOptions() + .AddOptions(CookieAuthenticationDefaults.AuthenticationScheme) .Configure((options, sessionStore) => { options.SessionStore = sessionStore; From 2fb475b5ed5b0d3f7e11085bb086fd89d9fd242f Mon Sep 17 00:00:00 2001 From: StephenCWills Date: Wed, 6 Aug 2025 17:59:43 -0400 Subject: [PATCH 07/30] Define helper for requiring controller access in an authorization policy --- .../Security/ControllerAccessHandler.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Gemstone.Web/Security/ControllerAccessHandler.cs b/src/Gemstone.Web/Security/ControllerAccessHandler.cs index 99c77b1..6245834 100644 --- a/src/Gemstone.Web/Security/ControllerAccessHandler.cs +++ b/src/Gemstone.Web/Security/ControllerAccessHandler.cs @@ -189,3 +189,21 @@ private static Permission GetResourcePermission(ClaimsPrincipal user, string cla public class ControllerAccessRequirement : IAuthorizationRequirement { } + +/// +/// Defines extension methods for the controller access handler. +/// +public static class ControllerAccessHandlerExtensions +{ + private static ControllerAccessRequirement Requirement { get; } = new(); + + /// + /// Adds the to the policy. + /// + /// The policy builder + /// The policy builder. + public static AuthorizationPolicyBuilder RequireControllerAccess(this AuthorizationPolicyBuilder builder) + { + return builder.AddRequirements(Requirement); + } +} From b9900ab374bbab5f4e06c09e75ec04c4b1efb54d Mon Sep 17 00:00:00 2001 From: StephenCWills Date: Thu, 7 Aug 2025 13:45:23 -0400 Subject: [PATCH 08/30] Use GetOrderedMetadata() instead of OfType() where applicable Signed-off-by: StephenCWills --- src/Gemstone.Web/Security/ControllerAccessHandler.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Gemstone.Web/Security/ControllerAccessHandler.cs b/src/Gemstone.Web/Security/ControllerAccessHandler.cs index 6245834..58b5e31 100644 --- a/src/Gemstone.Web/Security/ControllerAccessHandler.cs +++ b/src/Gemstone.Web/Security/ControllerAccessHandler.cs @@ -86,8 +86,7 @@ protected override Task HandleRequirementAsync(AuthorizationHandlerContext conte return Task.CompletedTask; ControllerActionDescriptor? descriptor = endpoint.Metadata - .OfType() - .FirstOrDefault(); + .GetMetadata(); if (descriptor is null) return Task.CompletedTask; @@ -104,7 +103,7 @@ protected override Task HandleRequirementAsync(AuthorizationHandlerContext conte private bool HandleResourceActionPermission(ContextWrapper wrapper) { string? routeName = wrapper.Endpoint.Metadata - .OfType() + .GetOrderedMetadata() .Select(metadata => metadata.RouteName) .FirstOrDefault(); @@ -130,7 +129,7 @@ bool fail() private void HandleResourceAccessPermission(ContextWrapper wrapper) { ILookup accessClaims = wrapper.Endpoint.Metadata - .OfType() + .GetOrderedMetadata() .SelectMany(attribute => attribute.Access, (attribute, accessLevel) => $"Controller {attribute.Name} {accessLevel}") .ToLookup(claimValue => GetResourceAccessPermission(wrapper.User, claimValue)); From 3e011ca9f85401a2cb00a62e28ab5752374fcfdd Mon Sep 17 00:00:00 2001 From: StephenCWills Date: Thu, 7 Aug 2025 14:50:56 -0400 Subject: [PATCH 09/30] Add role claims for resource access levels --- .../Security/ControllerAccessHandler.cs | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/Gemstone.Web/Security/ControllerAccessHandler.cs b/src/Gemstone.Web/Security/ControllerAccessHandler.cs index 58b5e31..e83f443 100644 --- a/src/Gemstone.Web/Security/ControllerAccessHandler.cs +++ b/src/Gemstone.Web/Security/ControllerAccessHandler.cs @@ -21,6 +21,7 @@ // //****************************************************************************************************** +using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; @@ -128,21 +129,34 @@ bool fail() private void HandleResourceAccessPermission(ContextWrapper wrapper) { - ILookup accessClaims = wrapper.Endpoint.Metadata - .GetOrderedMetadata() + IReadOnlyList accessAttributes = wrapper.Endpoint.Metadata + .GetOrderedMetadata(); + + ILookup accessClaims = accessAttributes .SelectMany(attribute => attribute.Access, (attribute, accessLevel) => $"Controller {attribute.Name} {accessLevel}") .ToLookup(claimValue => GetResourceAccessPermission(wrapper.User, claimValue)); - bool fail = accessClaims[Permission.Deny] + bool isDenied = accessClaims[Permission.Deny] .Select(ToFailureReason) .Select(wrapper.Fail) .DefaultIfEmpty(false) .All(b => b); - if (fail) + if (isDenied) return; if (accessClaims[Permission.Allow].Any()) + { + wrapper.Succeed(); + return; + } + + bool isAllowedByRole = accessAttributes + .SelectMany(attribute => attribute.Access) + .Select(accessLevel => accessLevel.ToString()) + .Any(role => wrapper.User.HasClaim("Gemstone.Role", role)); + + if (isAllowedByRole) wrapper.Succeed(); } From 17658193a9452c6f4da906a11afb5e06de44ed29 Mon Sep 17 00:00:00 2001 From: StephenCWills Date: Thu, 7 Aug 2025 16:47:13 -0400 Subject: [PATCH 10/30] Implement logout mechanism --- .../Security/IAuthenticationWebBuilder.cs | 43 ++++++++++++++++--- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs b/src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs index 5ae036d..09f2426 100644 --- a/src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs +++ b/src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs @@ -22,18 +22,16 @@ //****************************************************************************************************** using System; -using System.Collections.Generic; using System.Threading.Tasks; using Gemstone.Security.AuthenticationProviders; -using MathNet.Numerics; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; namespace Gemstone.Web.Security; @@ -110,6 +108,40 @@ public IAuthenticationWebBuilder UseProvider(string providerIdentity, string pat } } + private class LogoutMiddleware(RequestDelegate next, IOptionsMonitor cookieOptions) + { + private CookieAuthenticationOptions Options => cookieOptions.CurrentValue; + + public async Task Invoke(HttpContext httpContext) + { + if (!IsLogoutRequest(httpContext.Request)) + { + await next(httpContext); + return; + } + + await httpContext.SignOutAsync(); + + // This handles the redirect to login page + if (!IsAjaxRequest(httpContext.Request)) + await httpContext.ChallengeAsync(); + } + + private bool IsLogoutRequest(HttpRequest request) + { + PathString logoutPath = Options.LogoutPath; + return logoutPath.HasValue && request.Path.StartsWithSegments(logoutPath); + } + + // Taken from Microsoft's reference source for the Cookie authentication implementation + // https://github.com/dotnet/aspnetcore/blob/v9.0.8/src/Security/Authentication/Cookies/src/CookieAuthenticationEvents.cs#L105 + private static bool IsAjaxRequest(HttpRequest request) + { + return string.Equals(request.Query[HeaderNames.XRequestedWith], "XMLHttpRequest", StringComparison.Ordinal) || + string.Equals(request.Headers.XRequestedWith, "XMLHttpRequest", StringComparison.Ordinal); + } + } + /// /// Sets up the default configuration for services to support Gemstone authentication. /// @@ -164,7 +196,7 @@ public static IApplicationBuilder UseGemstoneAuthentication(this IApplicationBui { AuthenticationWebBuilder builder = new(app.UseAuthentication()); configure(builder); - return app; + return app.UseMiddleware(); } private static AuthenticationBuilder ConfigureGemstoneWebDefaults(this IServiceCollection services) @@ -182,6 +214,7 @@ private static AuthenticationBuilder ConfigureGemstoneWebDefaults(this IServiceC options.ExpireTimeSpan = TimeSpan.FromMinutes(15); options.SlidingExpiration = true; options.LoginPath = "/Login"; + options.LogoutPath = "/asi/logout"; options.ReturnUrlParameter = "redir"; options.Cookie.Name = "x-gemstone-auth"; From b2721895e0402b10da9fde05ad5b94d066d1dde9 Mon Sep 17 00:00:00 2001 From: StephenCWills Date: Fri, 8 Aug 2025 14:18:41 -0400 Subject: [PATCH 11/30] Register provider endpoints based on authentication setup rather than runtime --- src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs b/src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs index 09f2426..127068e 100644 --- a/src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs +++ b/src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs @@ -179,9 +179,9 @@ public static IApplicationBuilder UseGemstoneAuthentication(this IApplicationBui void configure(IAuthenticationWebBuilder builder) { - IAuthenticationRuntime runtime = app.ApplicationServices.GetRequiredService(); + IAuthenticationSetup setup = app.ApplicationServices.GetRequiredService(); - foreach (string providerIdentity in runtime.GetProviderIdentities()) + foreach (string providerIdentity in setup.GetProviderIdentities()) builder.UseProvider(providerIdentity); } } From ee8a80e9ea2dd7268db79d27288267f27ce21774 Mon Sep 17 00:00:00 2001 From: StephenCWills Date: Fri, 8 Aug 2025 16:48:23 -0400 Subject: [PATCH 12/30] Fix resource access logic and introduce HttpMethod fallback --- .../Security/ControllerAccessHandler.cs | 47 ++++++++++++++----- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/src/Gemstone.Web/Security/ControllerAccessHandler.cs b/src/Gemstone.Web/Security/ControllerAccessHandler.cs index e83f443..796d16a 100644 --- a/src/Gemstone.Web/Security/ControllerAccessHandler.cs +++ b/src/Gemstone.Web/Security/ControllerAccessHandler.cs @@ -21,7 +21,6 @@ // //****************************************************************************************************** -using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; @@ -49,13 +48,15 @@ private enum Permission Neither } - private class ContextWrapper(AuthorizationHandlerContext context, ControllerAccessRequirement requirement, Endpoint endpoint, ControllerActionDescriptor descriptor) + 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() { @@ -92,7 +93,7 @@ protected override Task HandleRequirementAsync(AuthorizationHandlerContext conte if (descriptor is null) return Task.CompletedTask; - ContextWrapper wrapper = new(context, requirement, endpoint, descriptor); + ContextWrapper wrapper = new(context, requirement, httpContext, endpoint, descriptor); if (HandleResourceActionPermission(wrapper)) return Task.CompletedTask; @@ -103,10 +104,10 @@ protected override Task HandleRequirementAsync(AuthorizationHandlerContext conte private bool HandleResourceActionPermission(ContextWrapper wrapper) { - string? routeName = wrapper.Endpoint.Metadata - .GetOrderedMetadata() - .Select(metadata => metadata.RouteName) - .FirstOrDefault(); + IRouteNameMetadata? routeNameMetadata = wrapper.Endpoint.Metadata + .GetMetadata(); + + string? routeName = routeNameMetadata?.RouteName; string resource = wrapper.Descriptor.ControllerName; string action = routeName ?? wrapper.Descriptor.ActionName; @@ -129,11 +130,19 @@ bool fail() private void HandleResourceAccessPermission(ContextWrapper wrapper) { - IReadOnlyList accessAttributes = wrapper.Endpoint.Metadata - .GetOrderedMetadata(); + ResourceAccessAttribute? accessAttribute = wrapper.Endpoint.Metadata + .GetMetadata(); + + string resourceName = + accessAttribute?.Name ?? + wrapper.Descriptor.ControllerName; + + ResourceAccessLevel[] access = + accessAttribute?.Access ?? + [ToAccessLevel(wrapper.HttpMethod)]; - ILookup accessClaims = accessAttributes - .SelectMany(attribute => attribute.Access, (attribute, accessLevel) => $"Controller {attribute.Name} {accessLevel}") + ILookup accessClaims = access + .Select(accessLevel => $"Controller {resourceName} {accessLevel}") .ToLookup(claimValue => GetResourceAccessPermission(wrapper.User, claimValue)); bool isDenied = accessClaims[Permission.Deny] @@ -151,13 +160,25 @@ private void HandleResourceAccessPermission(ContextWrapper wrapper) return; } - bool isAllowedByRole = accessAttributes - .SelectMany(attribute => attribute.Access) + bool isAllowedByRole = access .Select(accessLevel => accessLevel.ToString()) .Any(role => wrapper.User.HasClaim("Gemstone.Role", role)); if (isAllowedByRole) wrapper.Succeed(); + + static ResourceAccessLevel ToAccessLevel(string httpMethod) + { + bool isReadOnly = + HttpMethods.IsGet(httpMethod) || + HttpMethods.IsHead(httpMethod) || + HttpMethods.IsOptions(httpMethod) || + HttpMethods.IsTrace(httpMethod); + + return isReadOnly + ? ResourceAccessLevel.View + : ResourceAccessLevel.Edit; + } } private AuthorizationFailureReason ToFailureReason(string claim) From 656f162cacb8e01bc672f6eb2aad6392e4628f3b Mon Sep 17 00:00:00 2001 From: StephenCWills Date: Fri, 8 Aug 2025 17:27:32 -0400 Subject: [PATCH 13/30] Fix logout middleware to retrieve named instance of cookie auth options --- src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs b/src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs index 127068e..719ec55 100644 --- a/src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs +++ b/src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs @@ -110,7 +110,8 @@ public IAuthenticationWebBuilder UseProvider(string providerIdentity, string pat private class LogoutMiddleware(RequestDelegate next, IOptionsMonitor cookieOptions) { - private CookieAuthenticationOptions Options => cookieOptions.CurrentValue; + private CookieAuthenticationOptions Options => cookieOptions + .Get(CookieAuthenticationDefaults.AuthenticationScheme); public async Task Invoke(HttpContext httpContext) { From 28a86668086e1f4ce245fd10f5dfcedfea9048d7 Mon Sep 17 00:00:00 2001 From: StephenCWills Date: Fri, 8 Aug 2025 18:25:51 -0400 Subject: [PATCH 14/30] Don't assume view/edit implies admin --- src/Gemstone.Web/Security/ControllerAccessHandler.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Gemstone.Web/Security/ControllerAccessHandler.cs b/src/Gemstone.Web/Security/ControllerAccessHandler.cs index 796d16a..4e7dbad 100644 --- a/src/Gemstone.Web/Security/ControllerAccessHandler.cs +++ b/src/Gemstone.Web/Security/ControllerAccessHandler.cs @@ -139,7 +139,7 @@ private void HandleResourceAccessPermission(ContextWrapper wrapper) ResourceAccessLevel[] access = accessAttribute?.Access ?? - [ToAccessLevel(wrapper.HttpMethod)]; + ToAccessLevels(wrapper.HttpMethod); ILookup accessClaims = access .Select(accessLevel => $"Controller {resourceName} {accessLevel}") @@ -167,7 +167,7 @@ private void HandleResourceAccessPermission(ContextWrapper wrapper) if (isAllowedByRole) wrapper.Succeed(); - static ResourceAccessLevel ToAccessLevel(string httpMethod) + static ResourceAccessLevel[] ToAccessLevels(string httpMethod) { bool isReadOnly = HttpMethods.IsGet(httpMethod) || @@ -176,8 +176,8 @@ static ResourceAccessLevel ToAccessLevel(string httpMethod) HttpMethods.IsTrace(httpMethod); return isReadOnly - ? ResourceAccessLevel.View - : ResourceAccessLevel.Edit; + ? [ResourceAccessLevel.Admin, ResourceAccessLevel.Edit, ResourceAccessLevel.View] + : [ResourceAccessLevel.Admin, ResourceAccessLevel.Edit]; } } From 86d042d76716091dbab326984a2da826d84eff1d Mon Sep 17 00:00:00 2001 From: StephenCWills Date: Mon, 11 Aug 2025 14:12:46 -0400 Subject: [PATCH 15/30] Remove unused authentication provider middleware --- ...WindowsAuthenticationProviderMiddleware.cs | 55 ------------------- 1 file changed, 55 deletions(-) delete mode 100644 src/Gemstone.Web/Security/WindowsAuthenticationProviderMiddleware.cs diff --git a/src/Gemstone.Web/Security/WindowsAuthenticationProviderMiddleware.cs b/src/Gemstone.Web/Security/WindowsAuthenticationProviderMiddleware.cs deleted file mode 100644 index 73d0a49..0000000 --- a/src/Gemstone.Web/Security/WindowsAuthenticationProviderMiddleware.cs +++ /dev/null @@ -1,55 +0,0 @@ -//****************************************************************************************************** -// WindowsAuthenticationProviderMiddleware.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/16/2025 - Stephen C. Wills -// Generated original version of source code. -// -//****************************************************************************************************** - -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.Negotiate; -using Microsoft.AspNetCore.Http; - -namespace Gemstone.Web.Security; - -/// -/// Requires Windows authentication in a web application. -/// -/// The next middleware in the pipeline -public class WindowsAuthenticationProviderMiddleware(RequestDelegate next) -{ - private RequestDelegate Next { get; } = next; - - /// - /// Authenticates the user using Windows authentication. - /// - /// The context of the HTTP request - public async Task Invoke(HttpContext httpContext) - { - AuthenticateResult result = await httpContext.AuthenticateAsync(NegotiateDefaults.AuthenticationScheme); - - if (!result.Succeeded) - { - await httpContext.ChallengeAsync(NegotiateDefaults.AuthenticationScheme); - return; - } - - await Next(httpContext); - } -} From 2638ce4d5a3ae5b6cfaccb19310866b0c6d284b1 Mon Sep 17 00:00:00 2001 From: StephenCWills Date: Wed, 13 Aug 2025 13:52:59 -0400 Subject: [PATCH 16/30] Add resource info to the authorization info controller --- ....cs => AuthorizationInfoControllerBase.cs} | 86 ++++++++++++++++++- .../Security/ControllerAccessHandler.cs | 22 +---- 2 files changed, 84 insertions(+), 24 deletions(-) rename src/Gemstone.Web/APIController/{ClaimsControllerBase.cs => AuthorizationInfoControllerBase.cs} (57%) diff --git a/src/Gemstone.Web/APIController/ClaimsControllerBase.cs b/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs similarity index 57% rename from src/Gemstone.Web/APIController/ClaimsControllerBase.cs rename to src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs index e9c3c1b..3baf977 100644 --- a/src/Gemstone.Web/APIController/ClaimsControllerBase.cs +++ b/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs @@ -1,5 +1,5 @@ //****************************************************************************************************** -// ClaimsControllerBase.cs - Gbtc +// AuthorizationInfoControllerBase.cs - Gbtc // // Copyright © 2025, Grid Protection Alliance. All Rights Reserved. // @@ -24,8 +24,16 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; +using Gemstone.Collections.CollectionExtensions; +using Gemstone.Security.AccessControl; using Gemstone.Security.AuthenticationProviders; +using Gemstone.Web.Security; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; namespace Gemstone.Web.APIController; @@ -33,13 +41,13 @@ namespace Gemstone.Web.APIController; /// /// Base class for a controller that provides information about claims to client applications. /// -public abstract class ClaimsControllerBase : ControllerBase +public abstract class AuthorizationInfoControllerBase : ControllerBase { /// /// Gets all the claims associated with the authenticated user. /// /// All of the authenticated user's claims. - [HttpGet, Route("claims")] + [HttpGet, Route("user/claims")] public IActionResult GetAllClaims() { var claims = HttpContext.User.Claims @@ -53,7 +61,7 @@ public IActionResult GetAllClaims() /// /// The type of the claims to be returned /// The authenticated user's claims of the given type. - [HttpGet, Route("claims/{**claimType}")] + [HttpGet, Route("user/claims/{**claimType}")] public IEnumerable GetClaims(string claimType) { return HttpContext.User @@ -114,4 +122,74 @@ public IActionResult FindClaims(IServiceProvider serviceProvider, string provide ? Ok(claimsProvider.FindClaims(claimType, searchText ?? "*")) : NotFound(); } + + /// + /// Gets a list of 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("resources")] + public 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; + + ResourceAccessAttribute? accessAttribute = endpoint.Metadata + .GetMetadata(); + + string resourceName = accessAttribute.GetResourceName(descriptor); + IEnumerable accessLevels = ToAccessLevels(endpoint, accessAttribute); + HashSet access = resourceAccessLookup.GetOrAdd(resourceName, _ => []); + access.UnionWith(accessLevels); + } + + var resources = resourceAccessLookup + .OrderBy(kvp => kvp.Key) + .Select(kvp => new + { + Type = "Controller", + Name = kvp.Key, + AccessLevels = kvp.Value + .OrderBy(level => level) + .Select(level => $"{level}") + }); + + return Ok(resources); + + static IEnumerable ToAccessLevels(Endpoint endpoint, ResourceAccessAttribute? accessAttribute) + { + if (accessAttribute is not null) + return accessAttribute.Access; + + HttpMethodMetadata? httpMethodMetadata = endpoint.Metadata + .GetMetadata(); + + IReadOnlyList httpMethods = httpMethodMetadata?.HttpMethods + ?? [HttpMethods.Get, HttpMethods.Post]; + + return httpMethods + .SelectMany(accessAttribute.GetAccessLevels); + } + } } diff --git a/src/Gemstone.Web/Security/ControllerAccessHandler.cs b/src/Gemstone.Web/Security/ControllerAccessHandler.cs index 4e7dbad..040cfdc 100644 --- a/src/Gemstone.Web/Security/ControllerAccessHandler.cs +++ b/src/Gemstone.Web/Security/ControllerAccessHandler.cs @@ -133,13 +133,8 @@ private void HandleResourceAccessPermission(ContextWrapper wrapper) ResourceAccessAttribute? accessAttribute = wrapper.Endpoint.Metadata .GetMetadata(); - string resourceName = - accessAttribute?.Name ?? - wrapper.Descriptor.ControllerName; - - ResourceAccessLevel[] access = - accessAttribute?.Access ?? - ToAccessLevels(wrapper.HttpMethod); + string resourceName = accessAttribute.GetResourceName(wrapper.Descriptor); + ResourceAccessLevel[] access = accessAttribute.GetAccessLevels(wrapper.HttpMethod); ILookup accessClaims = access .Select(accessLevel => $"Controller {resourceName} {accessLevel}") @@ -166,19 +161,6 @@ private void HandleResourceAccessPermission(ContextWrapper wrapper) if (isAllowedByRole) wrapper.Succeed(); - - static ResourceAccessLevel[] ToAccessLevels(string httpMethod) - { - bool isReadOnly = - HttpMethods.IsGet(httpMethod) || - HttpMethods.IsHead(httpMethod) || - HttpMethods.IsOptions(httpMethod) || - HttpMethods.IsTrace(httpMethod); - - return isReadOnly - ? [ResourceAccessLevel.Admin, ResourceAccessLevel.Edit, ResourceAccessLevel.View] - : [ResourceAccessLevel.Admin, ResourceAccessLevel.Edit]; - } } private AuthorizationFailureReason ToFailureReason(string claim) From a79da09a58fa7bdf8671fd046e34db26253683aa Mon Sep 17 00:00:00 2001 From: StephenCWills Date: Wed, 13 Aug 2025 15:34:06 -0400 Subject: [PATCH 17/30] Add a more human-readable alias to claim types returned by AuthorizationInfoControllerBase --- .../AuthorizationInfoControllerBase.cs | 76 ++++++++++++++++++- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs b/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs index 3baf977..4761077 100644 --- a/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs +++ b/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs @@ -24,6 +24,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Security.Claims; using System.Threading.Tasks; using Gemstone.Collections.CollectionExtensions; using Gemstone.Security.AccessControl; @@ -81,9 +82,14 @@ public IActionResult GetClaimTypes(IServiceProvider serviceProvider, string prov IAuthenticationProvider? claimsProvider = serviceProvider .GetKeyedService(providerIdentity); - return claimsProvider is not null - ? Ok(claimsProvider.GetClaimTypes()) - : NotFound(); + if (claimsProvider is null) + return NotFound(); + + var claimTypes = claimsProvider + .GetClaimTypes() + .Select(type => new { Type = type, Alias = GetClaimTypeAlias(type) }); + + return Ok(claimTypes); } /// @@ -192,4 +198,68 @@ static IEnumerable ToAccessLevels(Endpoint endpoint, Resour .SelectMany(accessAttribute.GetAccessLevels); } } + + private static string GetClaimTypeAlias(string claimType) + { + return ClaimTypeAliases.TryGetValue(claimType, out string? alias) + ? alias : claimType; + } + + private static Dictionary ClaimTypeAliases { get; } = new() + { + { ClaimTypes.Actor, nameof(ClaimTypes.Actor) }, + { ClaimTypes.Anonymous, nameof(ClaimTypes.Anonymous) }, + { ClaimTypes.Authentication, nameof(ClaimTypes.Authentication) }, + { ClaimTypes.AuthenticationInstant, nameof(ClaimTypes.AuthenticationInstant) }, + { ClaimTypes.AuthenticationMethod, nameof(ClaimTypes.AuthenticationMethod) }, + { ClaimTypes.AuthorizationDecision, nameof(ClaimTypes.AuthorizationDecision) }, + { ClaimTypes.CookiePath, nameof(ClaimTypes.CookiePath) }, + { ClaimTypes.Country, nameof(ClaimTypes.Country) }, + { ClaimTypes.DateOfBirth, nameof(ClaimTypes.DateOfBirth) }, + { ClaimTypes.DenyOnlyPrimaryGroupSid, nameof(ClaimTypes.DenyOnlyPrimaryGroupSid) }, + { ClaimTypes.DenyOnlyPrimarySid, nameof(ClaimTypes.DenyOnlyPrimarySid) }, + { ClaimTypes.DenyOnlySid, nameof(ClaimTypes.DenyOnlySid) }, + { ClaimTypes.DenyOnlyWindowsDeviceGroup, nameof(ClaimTypes.DenyOnlyWindowsDeviceGroup) }, + { ClaimTypes.Dns, nameof(ClaimTypes.Dns) }, + { ClaimTypes.Dsa, nameof(ClaimTypes.Dsa) }, + { ClaimTypes.Email, nameof(ClaimTypes.Email) }, + { ClaimTypes.Expiration, nameof(ClaimTypes.Expiration) }, + { ClaimTypes.Expired, nameof(ClaimTypes.Expired) }, + { ClaimTypes.Gender, nameof(ClaimTypes.Gender) }, + { ClaimTypes.GivenName, nameof(ClaimTypes.GivenName) }, + { ClaimTypes.GroupSid, nameof(ClaimTypes.GroupSid) }, + { ClaimTypes.Hash, nameof(ClaimTypes.Hash) }, + { ClaimTypes.HomePhone, nameof(ClaimTypes.HomePhone) }, + { ClaimTypes.IsPersistent, nameof(ClaimTypes.IsPersistent) }, + { ClaimTypes.Locality, nameof(ClaimTypes.Locality) }, + { ClaimTypes.MobilePhone, nameof(ClaimTypes.MobilePhone) }, + { ClaimTypes.Name, nameof(ClaimTypes.Name) }, + { ClaimTypes.NameIdentifier, nameof(ClaimTypes.NameIdentifier) }, + { ClaimTypes.OtherPhone, nameof(ClaimTypes.OtherPhone) }, + { ClaimTypes.PostalCode, nameof(ClaimTypes.PostalCode) }, + { ClaimTypes.PrimaryGroupSid, nameof(ClaimTypes.PrimaryGroupSid) }, + { ClaimTypes.PrimarySid, nameof(ClaimTypes.PrimarySid) }, + { ClaimTypes.Role, nameof(ClaimTypes.Role) }, + { ClaimTypes.Rsa, nameof(ClaimTypes.Rsa) }, + { ClaimTypes.SerialNumber, nameof(ClaimTypes.SerialNumber) }, + { ClaimTypes.Sid, nameof(ClaimTypes.Sid) }, + { ClaimTypes.Spn, nameof(ClaimTypes.Spn) }, + { ClaimTypes.StateOrProvince, nameof(ClaimTypes.StateOrProvince) }, + { ClaimTypes.StreetAddress, nameof(ClaimTypes.StreetAddress) }, + { ClaimTypes.Surname, nameof(ClaimTypes.Surname) }, + { ClaimTypes.System, nameof(ClaimTypes.System) }, + { ClaimTypes.Thumbprint, nameof(ClaimTypes.Thumbprint) }, + { ClaimTypes.Upn, nameof(ClaimTypes.Upn) }, + { ClaimTypes.Uri, nameof(ClaimTypes.Uri) }, + { ClaimTypes.UserData, nameof(ClaimTypes.UserData) }, + { ClaimTypes.Version, nameof(ClaimTypes.Version) }, + { ClaimTypes.Webpage, nameof(ClaimTypes.Webpage) }, + { ClaimTypes.WindowsAccountName, nameof(ClaimTypes.WindowsAccountName) }, + { ClaimTypes.WindowsDeviceClaim, nameof(ClaimTypes.WindowsDeviceClaim) }, + { ClaimTypes.WindowsDeviceGroup, nameof(ClaimTypes.WindowsDeviceGroup) }, + { ClaimTypes.WindowsFqbnVersion, nameof(ClaimTypes.WindowsFqbnVersion) }, + { ClaimTypes.WindowsSubAuthority, nameof(ClaimTypes.WindowsSubAuthority) }, + { ClaimTypes.WindowsUserClaim, nameof(ClaimTypes.WindowsUserClaim) }, + { ClaimTypes.X500DistinguishedName, nameof(ClaimTypes.X500DistinguishedName) } + }; } From 4ce644248bf2f82568292110af7de6b432d30f30 Mon Sep 17 00:00:00 2001 From: StephenCWills Date: Thu, 14 Aug 2025 10:29:07 -0400 Subject: [PATCH 18/30] Add sliding expiration to authentication session cache entries --- .../Security/DefaultTicketStore.cs | 20 +++++++++++++++---- .../Security/IAuthenticationWebBuilder.cs | 4 ++-- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/Gemstone.Web/Security/DefaultTicketStore.cs b/src/Gemstone.Web/Security/DefaultTicketStore.cs index 4172f4e..b31acdb 100644 --- a/src/Gemstone.Web/Security/DefaultTicketStore.cs +++ b/src/Gemstone.Web/Security/DefaultTicketStore.cs @@ -27,16 +27,30 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; namespace Gemstone.Web.Security; +/// +/// Options to configure the memory cache used by the authentication ticket store. +/// +public class SessionCacheOptions +{ + /// + /// Gets or sets the amount of time a ticket should + /// remain valid since the last time it was accessed. + /// + public TimeSpan SlidingExpiration { get; set; } = TimeSpan.FromMinutes(15); +} + /// /// Represents an in-memory storage mechanism for authentication tickets. /// /// The cache in which tickets will be stored -public class DefaultTicketStore(IMemoryCache memoryCache) : ITicketStore +public class DefaultTicketStore(IMemoryCache memoryCache, IOptionsMonitor optionsMonitor) : ITicketStore { private IMemoryCache MemoryCache { get; } = memoryCache; + private SessionCacheOptions Options { get; } = optionsMonitor.CurrentValue; /// public Task StoreAsync(AuthenticationTicket ticket) @@ -72,12 +86,10 @@ public Task RemoveAsync(string key) private void UpdateEntry(string key, AuthenticationTicket ticket) { DateTimeOffset? expiration = ticket.Properties.ExpiresUtc; - MemoryCacheEntryOptions options = new(); + MemoryCacheEntryOptions options = new() { SlidingExpiration = Options.SlidingExpiration }; if (expiration is not null) options.AbsoluteExpiration = ticket.Properties.ExpiresUtc; - else - options.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(24); MemoryCache .CreateEntry(key) diff --git a/src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs b/src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs index 719ec55..fcfbb47 100644 --- a/src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs +++ b/src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs @@ -212,8 +212,8 @@ private static AuthenticationBuilder ConfigureGemstoneWebDefaults(this IServiceC { options.SessionStore = sessionStore; - options.ExpireTimeSpan = TimeSpan.FromMinutes(15); - options.SlidingExpiration = true; + options.ExpireTimeSpan = TimeSpan.FromHours(24); + options.SlidingExpiration = false; options.LoginPath = "/Login"; options.LogoutPath = "/asi/logout"; options.ReturnUrlParameter = "redir"; From 597f2c1f3729b5750e2dad9b60f3a0c002e1608b Mon Sep 17 00:00:00 2001 From: StephenCWills Date: Mon, 18 Aug 2025 13:17:21 -0400 Subject: [PATCH 19/30] Exclude authentication provider claims in authentication runtime middleware --- src/Gemstone.Web/Security/AuthenticationRuntimeMiddleware.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gemstone.Web/Security/AuthenticationRuntimeMiddleware.cs b/src/Gemstone.Web/Security/AuthenticationRuntimeMiddleware.cs index e02b701..cb561d5 100644 --- a/src/Gemstone.Web/Security/AuthenticationRuntimeMiddleware.cs +++ b/src/Gemstone.Web/Security/AuthenticationRuntimeMiddleware.cs @@ -48,7 +48,7 @@ public class AuthenticationRuntimeMiddleware(RequestDelegate next, IAuthenticati public async Task Invoke(HttpContext httpContext) { ClaimsPrincipal user = httpContext.User; - ClaimsIdentity newIdentity = new(user.Identity, user.Claims); + ClaimsIdentity newIdentity = new(user.Identity); IEnumerable assignedClaims = Runtime.GetAssignedClaims(ProviderIdentity, user); newIdentity.AddClaims(assignedClaims); httpContext.User = new(newIdentity); From fe5cee943f1a77d6825926d9335da5c16be92a67 Mon Sep 17 00:00:00 2001 From: StephenCWills Date: Tue, 19 Aug 2025 14:16:07 -0400 Subject: [PATCH 20/30] Simplify controller access logic --- .../Security/ControllerAccessHandler.cs | 65 +++++-------------- 1 file changed, 16 insertions(+), 49 deletions(-) diff --git a/src/Gemstone.Web/Security/ControllerAccessHandler.cs b/src/Gemstone.Web/Security/ControllerAccessHandler.cs index 040cfdc..70aa7d5 100644 --- a/src/Gemstone.Web/Security/ControllerAccessHandler.cs +++ b/src/Gemstone.Web/Security/ControllerAccessHandler.cs @@ -21,7 +21,6 @@ // //****************************************************************************************************** -using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using Gemstone.Security.AccessControl; @@ -128,41 +127,6 @@ bool fail() } } - private void HandleResourceAccessPermission(ContextWrapper wrapper) - { - ResourceAccessAttribute? accessAttribute = wrapper.Endpoint.Metadata - .GetMetadata(); - - string resourceName = accessAttribute.GetResourceName(wrapper.Descriptor); - ResourceAccessLevel[] access = accessAttribute.GetAccessLevels(wrapper.HttpMethod); - - ILookup accessClaims = access - .Select(accessLevel => $"Controller {resourceName} {accessLevel}") - .ToLookup(claimValue => GetResourceAccessPermission(wrapper.User, claimValue)); - - bool isDenied = accessClaims[Permission.Deny] - .Select(ToFailureReason) - .Select(wrapper.Fail) - .DefaultIfEmpty(false) - .All(b => b); - - if (isDenied) - return; - - if (accessClaims[Permission.Allow].Any()) - { - wrapper.Succeed(); - return; - } - - bool isAllowedByRole = access - .Select(accessLevel => accessLevel.ToString()) - .Any(role => wrapper.User.HasClaim("Gemstone.Role", role)); - - if (isAllowedByRole) - wrapper.Succeed(); - } - private AuthorizationFailureReason ToFailureReason(string claim) { return new AuthorizationFailureReason(this, $"{claim} permission denied"); @@ -173,27 +137,30 @@ private AuthorizationFailureReason ToFailureReason(string claim) #region [ Static ] // Static Methods + private static Permission GetResourceActionPermission(ClaimsPrincipal user, string claimValue) { - return GetResourcePermission(user, "Gemstone.ResourceAction", claimValue); - } + string allowClaim = $"Gemstone.ResourceAction.Allow"; + string denyClaim = $"Gemstone.ResourceAction.Deny"; - private static Permission GetResourceAccessPermission(ClaimsPrincipal user, string claimValue) - { - return GetResourcePermission(user, "Gemstone.ResourceAccess", claimValue); + if (user.HasClaim(denyClaim, claimValue)) + return Permission.Deny; + + return user.HasClaim(allowClaim, claimValue) + ? Permission.Allow + : Permission.Neither; } - private static Permission GetResourcePermission(ClaimsPrincipal user, string claimTypePrefix, string claimValue) + private static void HandleResourceAccessPermission(ContextWrapper wrapper) { - string allowClaim = $"{claimTypePrefix}.Allow"; - string denyClaim = $"{claimTypePrefix}.Deny"; + ResourceAccessAttribute? accessAttribute = wrapper.Endpoint.Metadata + .GetMetadata(); - if (user.HasClaim(denyClaim, claimValue)) - return Permission.Allow; + string resourceName = accessAttribute.GetResourceName(wrapper.Descriptor); + ResourceAccessLevel[] access = accessAttribute.GetAccessLevels(wrapper.HttpMethod); - return user.HasClaim(allowClaim, claimValue) - ? Permission.Deny - : Permission.Neither; + if (wrapper.User.HasAccessTo("Controller", resourceName, access)) + wrapper.Succeed(); } #endregion From c788059aa6431e3c379c9ae7329c72547ac76b57 Mon Sep 17 00:00:00 2001 From: StephenCWills Date: Tue, 19 Aug 2025 16:36:35 -0400 Subject: [PATCH 21/30] Add endpoint to check the user's level of access to resources --- .../AuthorizationInfoControllerBase.cs | 44 ++++++++++++++++--- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs b/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs index 4761077..8dbc4a5 100644 --- a/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs +++ b/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs @@ -49,7 +49,7 @@ public abstract class AuthorizationInfoControllerBase : ControllerBase /// /// All of the authenticated user's claims. [HttpGet, Route("user/claims")] - public IActionResult GetAllClaims() + public virtual IActionResult GetAllClaims() { var claims = HttpContext.User.Claims .Select(claim => new { claim.Type, claim.Value }); @@ -63,7 +63,7 @@ public IActionResult GetAllClaims() /// The type of the claims to be returned /// The authenticated user's claims of the given type. [HttpGet, Route("user/claims/{**claimType}")] - public IEnumerable GetClaims(string claimType) + public virtual IEnumerable GetClaims(string claimType) { return HttpContext.User .FindAll(claim => claim.Type == claimType) @@ -77,7 +77,7 @@ public IEnumerable GetClaims(string claimType) /// Identity of the authentication provider /// The list of claim types used by the authentication provider. [HttpGet, Route("provider/{providerIdentity}/claimTypes")] - public IActionResult GetClaimTypes(IServiceProvider serviceProvider, string providerIdentity) + public virtual IActionResult GetClaimTypes(IServiceProvider serviceProvider, string providerIdentity) { IAuthenticationProvider? claimsProvider = serviceProvider .GetKeyedService(providerIdentity); @@ -100,7 +100,7 @@ public IActionResult GetClaimTypes(IServiceProvider serviceProvider, string prov /// Text used to narrow the list of results /// A list of users from the authentication provider. [HttpGet, Route("provider/{providerIdentity}/users")] - public IActionResult FindUsers(IServiceProvider serviceProvider, string providerIdentity, string? searchText) + public virtual IActionResult FindUsers(IServiceProvider serviceProvider, string providerIdentity, string? searchText) { IAuthenticationProvider? claimsProvider = serviceProvider .GetKeyedService(providerIdentity); @@ -119,7 +119,7 @@ public IActionResult FindUsers(IServiceProvider serviceProvider, string provider /// Text used to narrow the list of results /// A list of claims from the authentication provider. [HttpGet, Route("provider/{providerIdentity}/claims/{**claimType}")] - public IActionResult FindClaims(IServiceProvider serviceProvider, string providerIdentity, string claimType, string? searchText) + public virtual IActionResult FindClaims(IServiceProvider serviceProvider, string providerIdentity, string claimType, string? searchText) { IAuthenticationProvider? claimsProvider = serviceProvider .GetKeyedService(providerIdentity); @@ -136,7 +136,7 @@ public IActionResult FindClaims(IServiceProvider serviceProvider, string provide /// Source for endpoint data used to look up controller and action metadata /// A list of resources within the application. [HttpGet, Route("resources")] - public async Task GetResources(IAuthorizationPolicyProvider policyProvider, EndpointDataSource endpointDataSource) + public virtual async Task GetResources(IAuthorizationPolicyProvider policyProvider, EndpointDataSource endpointDataSource) { Dictionary> resourceAccessLookup = []; @@ -199,6 +199,38 @@ static IEnumerable ToAccessLevels(Endpoint endpoint, Resour } } + /// + /// Checks whether the user has permission to access each of the resources listed in the access list. + /// + /// List of resources the user is attempting to access + /// List of boolean values indicating whether the user has access to each requested resource. + [HttpPost, Route("access")] + public virtual IEnumerable CheckAccess([FromBody] ResourceAccessEntry[] accessList) + { + return accessList.Select(entry => User.HasAccessTo(entry.ResourceType, entry.ResourceName, entry.Access)); + } + + /// + /// Represents an entry in a resource access list. + /// + public class ResourceAccessEntry + { + /// + /// Gets or sets the type of the resource. + /// + public string ResourceType { get; set; } = string.Empty; + + /// + /// Gets or sets the name of the resource. + /// + public string ResourceName { get; set; } = string.Empty; + + /// + /// Gets or sets the level of access needed. + /// + public ResourceAccessLevel[] Access { get; set; } = []; + } + private static string GetClaimTypeAlias(string claimType) { return ClaimTypeAliases.TryGetValue(claimType, out string? alias) From 2f6b8ff27e5ac5761f46c49bf7e64cd1bf6de717 Mon Sep 17 00:00:00 2001 From: StephenCWills Date: Wed, 20 Aug 2025 10:39:20 -0400 Subject: [PATCH 22/30] Update claim type endpoint in authorization controller --- .../AuthorizationInfoControllerBase.cs | 76 +------------------ 1 file changed, 3 insertions(+), 73 deletions(-) diff --git a/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs b/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs index 8dbc4a5..9c12db8 100644 --- a/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs +++ b/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs @@ -24,7 +24,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Security.Claims; using System.Threading.Tasks; using Gemstone.Collections.CollectionExtensions; using Gemstone.Security.AccessControl; @@ -82,14 +81,9 @@ public virtual IActionResult GetClaimTypes(IServiceProvider serviceProvider, str IAuthenticationProvider? claimsProvider = serviceProvider .GetKeyedService(providerIdentity); - if (claimsProvider is null) - return NotFound(); - - var claimTypes = claimsProvider - .GetClaimTypes() - .Select(type => new { Type = type, Alias = GetClaimTypeAlias(type) }); - - return Ok(claimTypes); + return claimsProvider is not null + ? Ok(claimsProvider.GetClaimTypes()) + : NotFound(); } /// @@ -230,68 +224,4 @@ public class ResourceAccessEntry /// public ResourceAccessLevel[] Access { get; set; } = []; } - - private static string GetClaimTypeAlias(string claimType) - { - return ClaimTypeAliases.TryGetValue(claimType, out string? alias) - ? alias : claimType; - } - - private static Dictionary ClaimTypeAliases { get; } = new() - { - { ClaimTypes.Actor, nameof(ClaimTypes.Actor) }, - { ClaimTypes.Anonymous, nameof(ClaimTypes.Anonymous) }, - { ClaimTypes.Authentication, nameof(ClaimTypes.Authentication) }, - { ClaimTypes.AuthenticationInstant, nameof(ClaimTypes.AuthenticationInstant) }, - { ClaimTypes.AuthenticationMethod, nameof(ClaimTypes.AuthenticationMethod) }, - { ClaimTypes.AuthorizationDecision, nameof(ClaimTypes.AuthorizationDecision) }, - { ClaimTypes.CookiePath, nameof(ClaimTypes.CookiePath) }, - { ClaimTypes.Country, nameof(ClaimTypes.Country) }, - { ClaimTypes.DateOfBirth, nameof(ClaimTypes.DateOfBirth) }, - { ClaimTypes.DenyOnlyPrimaryGroupSid, nameof(ClaimTypes.DenyOnlyPrimaryGroupSid) }, - { ClaimTypes.DenyOnlyPrimarySid, nameof(ClaimTypes.DenyOnlyPrimarySid) }, - { ClaimTypes.DenyOnlySid, nameof(ClaimTypes.DenyOnlySid) }, - { ClaimTypes.DenyOnlyWindowsDeviceGroup, nameof(ClaimTypes.DenyOnlyWindowsDeviceGroup) }, - { ClaimTypes.Dns, nameof(ClaimTypes.Dns) }, - { ClaimTypes.Dsa, nameof(ClaimTypes.Dsa) }, - { ClaimTypes.Email, nameof(ClaimTypes.Email) }, - { ClaimTypes.Expiration, nameof(ClaimTypes.Expiration) }, - { ClaimTypes.Expired, nameof(ClaimTypes.Expired) }, - { ClaimTypes.Gender, nameof(ClaimTypes.Gender) }, - { ClaimTypes.GivenName, nameof(ClaimTypes.GivenName) }, - { ClaimTypes.GroupSid, nameof(ClaimTypes.GroupSid) }, - { ClaimTypes.Hash, nameof(ClaimTypes.Hash) }, - { ClaimTypes.HomePhone, nameof(ClaimTypes.HomePhone) }, - { ClaimTypes.IsPersistent, nameof(ClaimTypes.IsPersistent) }, - { ClaimTypes.Locality, nameof(ClaimTypes.Locality) }, - { ClaimTypes.MobilePhone, nameof(ClaimTypes.MobilePhone) }, - { ClaimTypes.Name, nameof(ClaimTypes.Name) }, - { ClaimTypes.NameIdentifier, nameof(ClaimTypes.NameIdentifier) }, - { ClaimTypes.OtherPhone, nameof(ClaimTypes.OtherPhone) }, - { ClaimTypes.PostalCode, nameof(ClaimTypes.PostalCode) }, - { ClaimTypes.PrimaryGroupSid, nameof(ClaimTypes.PrimaryGroupSid) }, - { ClaimTypes.PrimarySid, nameof(ClaimTypes.PrimarySid) }, - { ClaimTypes.Role, nameof(ClaimTypes.Role) }, - { ClaimTypes.Rsa, nameof(ClaimTypes.Rsa) }, - { ClaimTypes.SerialNumber, nameof(ClaimTypes.SerialNumber) }, - { ClaimTypes.Sid, nameof(ClaimTypes.Sid) }, - { ClaimTypes.Spn, nameof(ClaimTypes.Spn) }, - { ClaimTypes.StateOrProvince, nameof(ClaimTypes.StateOrProvince) }, - { ClaimTypes.StreetAddress, nameof(ClaimTypes.StreetAddress) }, - { ClaimTypes.Surname, nameof(ClaimTypes.Surname) }, - { ClaimTypes.System, nameof(ClaimTypes.System) }, - { ClaimTypes.Thumbprint, nameof(ClaimTypes.Thumbprint) }, - { ClaimTypes.Upn, nameof(ClaimTypes.Upn) }, - { ClaimTypes.Uri, nameof(ClaimTypes.Uri) }, - { ClaimTypes.UserData, nameof(ClaimTypes.UserData) }, - { ClaimTypes.Version, nameof(ClaimTypes.Version) }, - { ClaimTypes.Webpage, nameof(ClaimTypes.Webpage) }, - { ClaimTypes.WindowsAccountName, nameof(ClaimTypes.WindowsAccountName) }, - { ClaimTypes.WindowsDeviceClaim, nameof(ClaimTypes.WindowsDeviceClaim) }, - { ClaimTypes.WindowsDeviceGroup, nameof(ClaimTypes.WindowsDeviceGroup) }, - { ClaimTypes.WindowsFqbnVersion, nameof(ClaimTypes.WindowsFqbnVersion) }, - { ClaimTypes.WindowsSubAuthority, nameof(ClaimTypes.WindowsSubAuthority) }, - { ClaimTypes.WindowsUserClaim, nameof(ClaimTypes.WindowsUserClaim) }, - { ClaimTypes.X500DistinguishedName, nameof(ClaimTypes.X500DistinguishedName) } - }; } From 435fc1164cfa8580c87c7a193ba646cd3066c49f Mon Sep 17 00:00:00 2001 From: Christoph Lackner Date: Thu, 21 Aug 2025 12:42:27 -0400 Subject: [PATCH 23/30] Removed AuthChecks from ModelController --- .../APIController/ModelController.cs | 70 +------------------ 1 file changed, 3 insertions(+), 67 deletions(-) diff --git a/src/Gemstone.Web/APIController/ModelController.cs b/src/Gemstone.Web/APIController/ModelController.cs index 542c280..b2e096a 100644 --- a/src/Gemstone.Web/APIController/ModelController.cs +++ b/src/Gemstone.Web/APIController/ModelController.cs @@ -44,33 +44,10 @@ namespace Gemstone.Web.APIController /// /// Creates a new /// - public ModelController() - { - DeleteRoles = typeof(T).GetCustomAttribute()?.Roles ?? ""; - PostRoles = typeof(T).GetCustomAttribute()?.Roles ?? ""; - PatchRoles = typeof(T).GetCustomAttribute()?.Roles ?? ""; - } - + public ModelController() { } + #endregion - #region [ Properties ] - - /// - /// Gets the roles required for PATCH requests. - /// - protected string PatchRoles { get; } - - /// - /// Gets the roles required for POST requests. - /// - protected string PostRoles { get; } - - /// - /// Gets the roles required for DELETE requests. - /// - protected string DeleteRoles { get; } - - #endregion /// /// Updates a record from associated table. @@ -81,9 +58,6 @@ public ModelController() [HttpPatch, Route("")] public virtual async Task Patch([FromBody] T record, CancellationToken cancellationToken) { - if (!PatchAuthCheck()) - return Unauthorized(); - await using AdoDataConnection connection = CreateConnection(); TableOperations tableOperations = new(connection); await tableOperations.UpdateRecordAsync(record, cancellationToken); @@ -100,9 +74,6 @@ public virtual async Task Patch([FromBody] T record, Cancellation [HttpPost, Route("")] public virtual async Task Post([FromBody]T record, CancellationToken cancellationToken) { - if (!PostAuthCheck()) - return Unauthorized(); - await using AdoDataConnection connection = CreateConnection(); TableOperations tableOperations = new(connection); await tableOperations.AddNewRecordAsync(record, cancellationToken); @@ -120,9 +91,6 @@ public virtual async Task Post([FromBody]T record, CancellationTo [HttpDelete, Route("")] public virtual async Task Delete([FromBody] T record, CancellationToken cancellationToken) { - if (!DeleteAuthCheck()) - return Unauthorized(); - await using AdoDataConnection connection = CreateConnection(); TableOperations tableOperations = new(connection); await tableOperations.DeleteRecordAsync(record, cancellationToken); @@ -139,9 +107,6 @@ public virtual async Task Delete([FromBody] T record, Cancellatio [HttpDelete, Route("{id}")] public virtual async Task Delete(string id, CancellationToken cancellationToken) { - if(!DeleteAuthCheck()) - return Unauthorized(); - await using AdoDataConnection connection = CreateConnection(); TableOperations tableOperations = new(connection); await tableOperations.DeleteRecordWhereAsync($"{PrimaryKeyField} = {{0}}", cancellationToken, id); @@ -149,36 +114,7 @@ public virtual async Task Delete(string id, CancellationToken can return Ok(1); } - #region [ Methods ] - - /// - /// Check if current user is authorized for POST Requests. - /// - /// true if User is authorized for POST requests; otherwise, false. - protected virtual bool PostAuthCheck() - { - return PostRoles == string.Empty || User.IsInRole(PostRoles); - } - - /// - /// Check if current user is authorized for DELETE Requests. - /// - /// true if User is authorized for DELETE requests; otherwise, false. - protected virtual bool DeleteAuthCheck() - { - return DeleteRoles == string.Empty || User.IsInRole(DeleteRoles); - } - - /// - /// Check if current user is authorized for PATCH Requests. - /// - /// true if User is authorized for PATCH requests; otherwise, false. - protected virtual bool PatchAuthCheck() - { - return PatchRoles == string.Empty || User.IsInRole(PatchRoles); - } - - #endregion + #region [ Static ] From bcd255e5f30c68f18e7ba5195b6a7375ac8a2b4b Mon Sep 17 00:00:00 2001 From: prestoncraw Date: Thu, 21 Aug 2025 15:47:25 -0400 Subject: [PATCH 24/30] update returned options to have Label, Value properties with a LongLabel --- .../AuthorizationInfoControllerBase.cs | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs b/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs index 9c12db8..f307f49 100644 --- a/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs +++ b/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs @@ -24,6 +24,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Threading.Tasks; using Gemstone.Collections.CollectionExtensions; using Gemstone.Security.AccessControl; @@ -81,9 +82,14 @@ public virtual IActionResult GetClaimTypes(IServiceProvider serviceProvider, str IAuthenticationProvider? claimsProvider = serviceProvider .GetKeyedService(providerIdentity); - return claimsProvider is not null - ? Ok(claimsProvider.GetClaimTypes()) - : NotFound(); + if (claimsProvider is null) + return NotFound(); + + var claimTypes = claimsProvider + .GetClaimTypes() + .Select(type => new { Value = type.Type, Label = type.Alias, LongLabel = type.Description }); + + return Ok(claimTypes); } /// @@ -118,9 +124,14 @@ public virtual IActionResult FindClaims(IServiceProvider serviceProvider, string IAuthenticationProvider? claimsProvider = serviceProvider .GetKeyedService(providerIdentity); - return claimsProvider is not null - ? Ok(claimsProvider.FindClaims(claimType, searchText ?? "*")) - : NotFound(); + if (claimsProvider is null) + return NotFound(); + + var claims = claimsProvider + .FindClaims(claimType, searchText ?? "*") + .Select(claim => new { Label = claim?.Description, Value = claim?.Value, LongLabel = claim?.LongDescription }); + + return Ok(claims); } /// From 4e7ac30da73a6960bfb44a34969f091b65843a47 Mon Sep 17 00:00:00 2001 From: StephenCWills Date: Thu, 21 Aug 2025 16:58:54 -0400 Subject: [PATCH 25/30] Change access levels to access types --- .../AuthorizationInfoControllerBase.cs | 24 +++++++++---------- .../Security/ControllerAccessHandler.cs | 11 +++++---- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs b/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs index f307f49..7444038 100644 --- a/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs +++ b/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs @@ -143,7 +143,7 @@ public virtual IActionResult FindClaims(IServiceProvider serviceProvider, string [HttpGet, Route("resources")] public virtual async Task GetResources(IAuthorizationPolicyProvider policyProvider, EndpointDataSource endpointDataSource) { - Dictionary> resourceAccessLookup = []; + Dictionary> resourceAccessLookup = []; foreach (Endpoint endpoint in endpointDataSource.Endpoints) { @@ -170,9 +170,9 @@ public virtual async Task GetResources(IAuthorizationPolicyProvid .GetMetadata(); string resourceName = accessAttribute.GetResourceName(descriptor); - IEnumerable accessLevels = ToAccessLevels(endpoint, accessAttribute); - HashSet access = resourceAccessLookup.GetOrAdd(resourceName, _ => []); - access.UnionWith(accessLevels); + IEnumerable accessTypes = ToAccessTypes(endpoint, accessAttribute); + HashSet access = resourceAccessLookup.GetOrAdd(resourceName, _ => []); + access.UnionWith(accessTypes); } var resources = resourceAccessLookup @@ -181,26 +181,26 @@ public virtual async Task GetResources(IAuthorizationPolicyProvid { Type = "Controller", Name = kvp.Key, - AccessLevels = kvp.Value - .OrderBy(level => level) - .Select(level => $"{level}") + AccessTypes = kvp.Value.OrderBy(type => type) }); return Ok(resources); - static IEnumerable ToAccessLevels(Endpoint endpoint, ResourceAccessAttribute? accessAttribute) + static IEnumerable ToAccessTypes(Endpoint endpoint, ResourceAccessAttribute? accessAttribute) { if (accessAttribute is not null) - return accessAttribute.Access; + return [accessAttribute.Access]; HttpMethodMetadata? httpMethodMetadata = endpoint.Metadata .GetMetadata(); IReadOnlyList httpMethods = httpMethodMetadata?.HttpMethods - ?? [HttpMethods.Get, HttpMethods.Post]; + ?? []; return httpMethods - .SelectMany(accessAttribute.GetAccessLevels); + .Select(accessAttribute.GetAccessType) + .Where(type => type is not null) + .Select(type => type.GetValueOrDefault()); } } @@ -233,6 +233,6 @@ public class ResourceAccessEntry /// /// Gets or sets the level of access needed. /// - public ResourceAccessLevel[] Access { get; set; } = []; + public ResourceAccessType Access { get; set; } } } diff --git a/src/Gemstone.Web/Security/ControllerAccessHandler.cs b/src/Gemstone.Web/Security/ControllerAccessHandler.cs index 70aa7d5..1006838 100644 --- a/src/Gemstone.Web/Security/ControllerAccessHandler.cs +++ b/src/Gemstone.Web/Security/ControllerAccessHandler.cs @@ -153,13 +153,16 @@ private static Permission GetResourceActionPermission(ClaimsPrincipal user, stri private static void HandleResourceAccessPermission(ContextWrapper wrapper) { - ResourceAccessAttribute? accessAttribute = wrapper.Endpoint.Metadata - .GetMetadata(); + IResourceAccessAttribute? accessAttribute = wrapper.Endpoint.Metadata + .GetMetadata(); + + if (accessAttribute is NoResourceAccessAttribute) + return; string resourceName = accessAttribute.GetResourceName(wrapper.Descriptor); - ResourceAccessLevel[] access = accessAttribute.GetAccessLevels(wrapper.HttpMethod); + ResourceAccessType? access = accessAttribute.GetAccessType(wrapper.HttpMethod); - if (wrapper.User.HasAccessTo("Controller", resourceName, access)) + if (access.HasValue && wrapper.User.HasAccessTo("Controller", resourceName, access.GetValueOrDefault())) wrapper.Succeed(); } From 196655951e684556cac7a7692cf0ad286317f00d Mon Sep 17 00:00:00 2001 From: StephenCWills Date: Fri, 22 Aug 2025 11:39:19 -0400 Subject: [PATCH 26/30] Add support for search text to the claimTypes endpoint --- .../AuthorizationInfoControllerBase.cs | 79 ++++++++++++++----- 1 file changed, 60 insertions(+), 19 deletions(-) diff --git a/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs b/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs index 7444038..e6606ee 100644 --- a/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs +++ b/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs @@ -24,7 +24,7 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Net; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Gemstone.Collections.CollectionExtensions; using Gemstone.Security.AccessControl; @@ -42,8 +42,37 @@ namespace Gemstone.Web.APIController; /// /// Base class for a controller that provides information about claims to client applications. /// -public abstract class AuthorizationInfoControllerBase : ControllerBase +public abstract partial class AuthorizationInfoControllerBase : ControllerBase { + #region [ Members ] + + // Nested Types + + /// + /// Represents an entry in a resource access list. + /// + public class ResourceAccessEntry + { + /// + /// Gets or sets the type of the resource. + /// + public string ResourceType { get; set; } = string.Empty; + + /// + /// Gets or sets the name of the resource. + /// + public string ResourceName { get; set; } = string.Empty; + + /// + /// Gets or sets the level of access needed. + /// + public ResourceAccessType Access { get; set; } + } + + #endregion + + #region [ Methods ] + /// /// Gets all the claims associated with the authenticated user. /// @@ -75,9 +104,10 @@ public virtual IEnumerable GetClaims(string claimType) /// /// Provides injected dependencies /// Identity of the authentication provider + /// Text used to narrow the list of results /// The list of claim types used by the authentication provider. [HttpGet, Route("provider/{providerIdentity}/claimTypes")] - public virtual IActionResult GetClaimTypes(IServiceProvider serviceProvider, string providerIdentity) + public virtual IActionResult FindClaimTypes(IServiceProvider serviceProvider, string providerIdentity, string? searchText) { IAuthenticationProvider? claimsProvider = serviceProvider .GetKeyedService(providerIdentity); @@ -85,8 +115,11 @@ public virtual IActionResult GetClaimTypes(IServiceProvider serviceProvider, str if (claimsProvider is null) return NotFound(); + Regex? searchPattern = ToSearchPattern(searchText); + var claimTypes = claimsProvider .GetClaimTypes() + .Where(type => searchPattern is null || searchPattern.IsMatch(type.Type)) .Select(type => new { Value = type.Type, Label = type.Alias, LongLabel = type.Description }); return Ok(claimTypes); @@ -215,24 +248,32 @@ public virtual IEnumerable CheckAccess([FromBody] ResourceAccessEntry[] ac return accessList.Select(entry => User.HasAccessTo(entry.ResourceType, entry.ResourceName, entry.Access)); } - /// - /// Represents an entry in a resource access list. - /// - public class ResourceAccessEntry + #endregion + + #region [ Static ] + + // Static Methods + + private static Regex? ToSearchPattern(string? searchText) { - /// - /// Gets or sets the type of the resource. - /// - public string ResourceType { get; set; } = string.Empty; + if (searchText is null) + return null; - /// - /// Gets or sets the name of the resource. - /// - public string ResourceName { get; set; } = string.Empty; + Regex conversionPattern = SearchTextConversionPattern(); - /// - /// Gets or sets the level of access needed. - /// - public ResourceAccessType Access { get; set; } + string searchPattern = conversionPattern.Replace(searchText, match => match.Value switch + { + "*" => ".*", + @"\\" => @"\\", + string v when v.StartsWith('\\') => Regex.Escape(v[1..]), + string v => Regex.Escape(v) + }); + + return new(searchPattern); } + + [GeneratedRegex(@"\\.|\*|[^\\*]+")] + private static partial Regex SearchTextConversionPattern(); + + #endregion } From 53d45e112538fd032226418f25ac7e0aaee2977c Mon Sep 17 00:00:00 2001 From: StephenCWills Date: Fri, 22 Aug 2025 14:12:52 -0400 Subject: [PATCH 27/30] Remove user search and fix up claim search --- .../AuthorizationInfoControllerBase.cs | 27 ++++++------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs b/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs index e6606ee..dd4d8c9 100644 --- a/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs +++ b/src/Gemstone.Web/APIController/AuthorizationInfoControllerBase.cs @@ -125,24 +125,6 @@ public virtual IActionResult FindClaimTypes(IServiceProvider serviceProvider, st return Ok(claimTypes); } - /// - /// Gets a list of users that can be authorized via the authentication provider. - /// - /// Provides injected dependencies - /// Identity of the authentication provider - /// Text used to narrow the list of results - /// A list of users from the authentication provider. - [HttpGet, Route("provider/{providerIdentity}/users")] - public virtual IActionResult FindUsers(IServiceProvider serviceProvider, string providerIdentity, string? searchText) - { - IAuthenticationProvider? claimsProvider = serviceProvider - .GetKeyedService(providerIdentity); - - return claimsProvider is not null - ? Ok(claimsProvider.FindUsers(searchText ?? "*")) - : NotFound(); - } - /// /// Gets a list of claims that can be assigned to users who authenticate with the provider. /// @@ -160,11 +142,18 @@ public virtual IActionResult FindClaims(IServiceProvider serviceProvider, string if (claimsProvider is null) return NotFound(); + if (!isSupported(claimType)) + return BadRequest($"Claim type not supported"); + var claims = claimsProvider .FindClaims(claimType, searchText ?? "*") - .Select(claim => new { Label = claim?.Description, Value = claim?.Value, LongLabel = claim?.LongDescription }); + .Select(claim => new { Label = claim.Description, claim.Value, LongLabel = claim.LongDescription }); return Ok(claims); + + bool isSupported(string claimType) => claimsProvider + .GetClaimTypes() + .Any(type => type.Type == claimType); } /// From 1c413370b142dc234d21ca4e04e1a12aef17ec50 Mon Sep 17 00:00:00 2001 From: StephenCWills Date: Tue, 26 Aug 2025 12:52:04 -0400 Subject: [PATCH 28/30] Use CookieSecurePolicy.SameAsRequest to support authentication for http --- src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs b/src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs index fcfbb47..3898fa9 100644 --- a/src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs +++ b/src/Gemstone.Web/Security/IAuthenticationWebBuilder.cs @@ -221,7 +221,6 @@ private static AuthenticationBuilder ConfigureGemstoneWebDefaults(this IServiceC options.Cookie.Name = "x-gemstone-auth"; options.Cookie.Path = "/"; options.Cookie.HttpOnly = true; - options.Cookie.SecurePolicy = CookieSecurePolicy.Always; options.Cookie.IsEssential = true; }); From 3e4340298b25f68a45e4383c82e575002d653223 Mon Sep 17 00:00:00 2001 From: StephenCWills Date: Tue, 26 Aug 2025 14:32:21 -0400 Subject: [PATCH 29/30] Carry name claims over from the authentication provider --- src/Gemstone.Web/Security/AuthenticationRuntimeMiddleware.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gemstone.Web/Security/AuthenticationRuntimeMiddleware.cs b/src/Gemstone.Web/Security/AuthenticationRuntimeMiddleware.cs index cb561d5..d099685 100644 --- a/src/Gemstone.Web/Security/AuthenticationRuntimeMiddleware.cs +++ b/src/Gemstone.Web/Security/AuthenticationRuntimeMiddleware.cs @@ -48,7 +48,7 @@ public class AuthenticationRuntimeMiddleware(RequestDelegate next, IAuthenticati public async Task Invoke(HttpContext httpContext) { ClaimsPrincipal user = httpContext.User; - ClaimsIdentity newIdentity = new(user.Identity); + ClaimsIdentity newIdentity = new(user.Identity, user.FindAll(ClaimTypes.Name)); IEnumerable assignedClaims = Runtime.GetAssignedClaims(ProviderIdentity, user); newIdentity.AddClaims(assignedClaims); httpContext.User = new(newIdentity); From b5bd4d6c5261c9c6fa1de4f9cfffc4ea37550ac1 Mon Sep 17 00:00:00 2001 From: Christoph Lackner Date: Tue, 26 Aug 2025 16:16:37 -0400 Subject: [PATCH 30/30] Removed GetAuth Check --- .../APIController/ReadOnlyModelController.cs | 48 ++----------------- 1 file changed, 3 insertions(+), 45 deletions(-) diff --git a/src/Gemstone.Web/APIController/ReadOnlyModelController.cs b/src/Gemstone.Web/APIController/ReadOnlyModelController.cs index b7fcb70..ba5c6fb 100644 --- a/src/Gemstone.Web/APIController/ReadOnlyModelController.cs +++ b/src/Gemstone.Web/APIController/ReadOnlyModelController.cs @@ -33,6 +33,7 @@ using Gemstone.Configuration; using Gemstone.Data; using Gemstone.Data.Model; +using Gemstone.Security.AccessControl; using Microsoft.AspNetCore.Mvc; namespace Gemstone.Web.APIController @@ -174,9 +175,6 @@ public ReadOnlyModelController() [HttpGet, Route("Open/{filterExpression}/{parameters}/{expiration:double?}")] public Task Open(string? filterExpression, object?[] parameters, double? expiration, CancellationToken cancellationToken) { - if (!GetAuthCheck()) - return Task.FromResult(Unauthorized()); - ConnectionCache cache = ConnectionCache.Create(expiration ?? 1.0D); cache.Records = cache.Table.QueryRecordsWhereAsync(filterExpression, cancellationToken, parameters).GetAsyncEnumerator(cancellationToken); @@ -199,9 +197,6 @@ public Task Open(string? filterExpression, object?[] parameters, [HttpGet, Route("Next/{token}/{count:int?}")] public async Task Next(string token, int? count, CancellationToken cancellationToken) { - if (!GetAuthCheck()) - return Unauthorized(); - if (!ConnectionCache.TryGet(token, out ConnectionCache? cache) || cache is null) return NotFound(); @@ -228,9 +223,6 @@ public async Task Next(string token, int? count, CancellationToke [HttpGet, Route("Close/{token}")] public IActionResult Close(string token) { - if (!GetAuthCheck()) - return Unauthorized(); - return ConnectionCache.Close(token) ? Ok() : NotFound(); } @@ -244,9 +236,6 @@ public IActionResult Close(string token) [HttpGet, Route("{page:min(0)}/{parentID?}")] public virtual async Task Get(string? parentID, int page, CancellationToken cancellationToken) { - if (!GetAuthCheck()) - return Unauthorized(); - await using AdoDataConnection connection = CreateConnection(); TableOperations tableOperations = new(connection); RecordFilter? filter = null; @@ -277,9 +266,6 @@ public virtual async Task Get(string? parentID, int page, Cancell [HttpGet, Route("{page:min(0)}/{sort}/{ascending:bool}")] public virtual async Task Get(string sort, bool ascending, int page, CancellationToken cancellationToken) { - if (!GetAuthCheck()) - return Unauthorized(); - await using AdoDataConnection connection = CreateConnection(); TableOperations tableOperations = new(connection); RecordFilter? filter = null; @@ -301,9 +287,6 @@ public virtual async Task Get(string sort, bool ascending, int pa [HttpGet, Route("{page:min(0)}/{parentID}/{sort}/{ascending:bool}")] public virtual async Task Get(string parentID, string sort, bool ascending, int page, CancellationToken cancellationToken) { - if (!GetAuthCheck()) - return Unauthorized(); - await using AdoDataConnection connection = CreateConnection(); TableOperations tableOperations = new(connection); RecordFilter filter = new() @@ -327,9 +310,6 @@ public virtual async Task Get(string parentID, string sort, bool [HttpGet, Route("One/{id}")] public virtual async Task GetOne(string id, CancellationToken cancellationToken) { - if (!GetAuthCheck()) - return Unauthorized(); - await using AdoDataConnection connection = CreateConnection(); TableOperations tableOperations = new(connection); T? result = await tableOperations.QueryRecordAsync(new RecordRestriction($"{PrimaryKeyField} = {{0}}", id), cancellationToken).ConfigureAwait(false); @@ -348,11 +328,9 @@ public virtual async Task GetOne(string id, CancellationToken can /// Propagates notification that operations should be canceled. /// An containing or . [HttpPost, Route("Search/{page:min(0)}/{parentID?}")] + [ResourceAccess(ResourceAccessType.Read)] public virtual async Task Search([FromBody] SearchPost postData, int page, string? parentID, CancellationToken cancellationToken) { - if (!GetAuthCheck()) - return Unauthorized(); - await using AdoDataConnection connection = CreateConnection(); TableOperations tableOperations = new(connection); RecordFilter[] filters = postData.Searches.ToArray(); @@ -380,11 +358,9 @@ public virtual async Task Search([FromBody] SearchPost postDat /// Propagates notification that operations should be canceled. /// A object containing the pagination information or . [HttpPost, Route("PageInfo/{parentID?}")] + [ResourceAccess(ResourceAccessType.Read)] public virtual async Task GetPageInfo(SearchPost postData, string? parentID, CancellationToken cancellationToken) { - if (!GetAuthCheck()) - return Unauthorized(); - await using AdoDataConnection connection = CreateConnection(); TableOperations tableOperations = new(connection); RecordFilter[] filters = postData.Searches.ToArray(); @@ -419,9 +395,6 @@ public virtual async Task GetPageInfo(SearchPost postData, str [HttpGet, Route("PageInfo/{parentID?}")] public virtual async Task GetPageInfo(string? parentID, CancellationToken cancellationToken) { - if (!GetAuthCheck()) - return Unauthorized(); - await using AdoDataConnection connection = CreateConnection(); TableOperations tableOperations = new(connection); RecordFilter[] filters = []; @@ -454,9 +427,6 @@ public virtual async Task GetPageInfo(string? parentID, Cancellat [HttpGet, Route("New")] public virtual async Task New(CancellationToken cancellationToken) { - if (!GetAuthCheck()) - return Unauthorized(); - await using AdoDataConnection connection = CreateConnection(); TableOperations tableOperations = new(connection); @@ -473,9 +443,6 @@ public virtual async Task New(CancellationToken cancellationToken [HttpGet, Route("Max/{fieldName}")] public virtual async Task GetMaxValue(string fieldName, CancellationToken cancellationToken) { - if (!GetAuthCheck()) - return Unauthorized(); - // Validate that the field exists on the model T using reflection PropertyInfo? property = typeof(T).GetProperty(fieldName); if (property is null) @@ -492,15 +459,6 @@ public virtual async Task GetMaxValue(string fieldName, Cancellat return Ok(maxValue ?? 0); } - /// - /// Check if current User is authorized for GET Requests. - /// - /// true if User is authorized for GET requests; otherwise, false. - protected virtual bool GetAuthCheck() - { - return GetRoles == string.Empty || User.IsInRole(GetRoles); - } - /// /// Create the for the controller. ///