From c99f5bc6bf98e42c75917b29ad431c0ccaca4432 Mon Sep 17 00:00:00 2001 From: chylex <contact@chylex.com> Date: Sun, 31 Mar 2024 12:07:33 +0200 Subject: [PATCH] Remove login session management logic from Web --- .../Users/AuthenticatedUserInfo.cs | 10 +++ .../Users/LogInSuccess.cs | 5 +- Common/Phantom.Common.Data/Optional.cs | 10 +++ .../ToController/GetAuthenticatedUser.cs | 13 ++++ .../WebMessageRegistries.cs | 32 +++++---- .../Rpc/WebMessageHandlerActor.cs | 8 ++- .../Users/UserLoginManager.cs | 69 +++++++++++++++---- .../PhantomComponent.cs | 10 +-- .../AuthenticationStateExtensions.cs | 27 ++++++++ .../CustomAuthenticationStateProvider.cs | 20 +++--- .../Authentication/CustomClaimsPrincipal.cs | 18 +++++ .../Authentication/UserInfo.cs | 23 ------- .../Authentication/UserLoginManager.cs | 14 ++-- .../Authentication/UserSessionManager.cs | 35 ---------- .../Authentication/UserSessions.cs | 54 --------------- .../PermissionBasedPolicyHandler.cs | 9 +-- .../Authorization/PermissionManager.cs | 22 ------ .../Authorization/PermissionView.razor | 5 +- .../PhantomWebServices.cs | 2 - Web/Phantom.Web/Layout/NavMenu.razor | 7 +- Web/Phantom.Web/Pages/Users.razor | 7 +- 21 files changed, 190 insertions(+), 210 deletions(-) create mode 100644 Common/Phantom.Common.Data.Web/Users/AuthenticatedUserInfo.cs create mode 100644 Common/Phantom.Common.Data/Optional.cs create mode 100644 Common/Phantom.Common.Messages.Web/ToController/GetAuthenticatedUser.cs create mode 100644 Web/Phantom.Web.Services/Authentication/AuthenticationStateExtensions.cs create mode 100644 Web/Phantom.Web.Services/Authentication/CustomClaimsPrincipal.cs delete mode 100644 Web/Phantom.Web.Services/Authentication/UserInfo.cs delete mode 100644 Web/Phantom.Web.Services/Authentication/UserSessionManager.cs delete mode 100644 Web/Phantom.Web.Services/Authentication/UserSessions.cs delete mode 100644 Web/Phantom.Web.Services/Authorization/PermissionManager.cs diff --git a/Common/Phantom.Common.Data.Web/Users/AuthenticatedUserInfo.cs b/Common/Phantom.Common.Data.Web/Users/AuthenticatedUserInfo.cs new file mode 100644 index 0000000..7db483b --- /dev/null +++ b/Common/Phantom.Common.Data.Web/Users/AuthenticatedUserInfo.cs @@ -0,0 +1,10 @@ +using MemoryPack; + +namespace Phantom.Common.Data.Web.Users; + +[MemoryPackable(GenerateType.VersionTolerant)] +public sealed partial record AuthenticatedUserInfo( + [property: MemoryPackOrder(0)] Guid Guid, + [property: MemoryPackOrder(1)] string Name, + [property: MemoryPackOrder(2)] PermissionSet Permissions +); diff --git a/Common/Phantom.Common.Data.Web/Users/LogInSuccess.cs b/Common/Phantom.Common.Data.Web/Users/LogInSuccess.cs index 172e9b5..28d12c0 100644 --- a/Common/Phantom.Common.Data.Web/Users/LogInSuccess.cs +++ b/Common/Phantom.Common.Data.Web/Users/LogInSuccess.cs @@ -5,7 +5,6 @@ namespace Phantom.Common.Data.Web.Users; [MemoryPackable(GenerateType.VersionTolerant)] public sealed partial record LogInSuccess( - [property: MemoryPackOrder(0)] Guid UserGuid, - [property: MemoryPackOrder(1)] PermissionSet Permissions, - [property: MemoryPackOrder(2)] ImmutableArray<byte> Token + [property: MemoryPackOrder(0)] AuthenticatedUserInfo UserInfo, + [property: MemoryPackOrder(1)] ImmutableArray<byte> Token ); diff --git a/Common/Phantom.Common.Data/Optional.cs b/Common/Phantom.Common.Data/Optional.cs new file mode 100644 index 0000000..bc93a5f --- /dev/null +++ b/Common/Phantom.Common.Data/Optional.cs @@ -0,0 +1,10 @@ +using MemoryPack; + +namespace Phantom.Common.Data; + +[MemoryPackable] +public readonly partial record struct Optional<T>(T? Value) { + public static implicit operator Optional<T>(T? value) { + return new Optional<T>(value); + } +} diff --git a/Common/Phantom.Common.Messages.Web/ToController/GetAuthenticatedUser.cs b/Common/Phantom.Common.Messages.Web/ToController/GetAuthenticatedUser.cs new file mode 100644 index 0000000..27ccec9 --- /dev/null +++ b/Common/Phantom.Common.Messages.Web/ToController/GetAuthenticatedUser.cs @@ -0,0 +1,13 @@ +using System.Collections.Immutable; +using MemoryPack; +using Phantom.Common.Data; +using Phantom.Common.Data.Web.Users; +using Phantom.Utils.Actor; + +namespace Phantom.Common.Messages.Web.ToController; + +[MemoryPackable(GenerateType.VersionTolerant)] +public sealed partial record GetAuthenticatedUser( + [property: MemoryPackOrder(0)] Guid UserGuid, + [property: MemoryPackOrder(1)] ImmutableArray<byte> SessionToken +) : IMessageToController, ICanReply<Optional<AuthenticatedUserInfo>>; diff --git a/Common/Phantom.Common.Messages.Web/WebMessageRegistries.cs b/Common/Phantom.Common.Messages.Web/WebMessageRegistries.cs index bf48c78..f1aa4b5 100644 --- a/Common/Phantom.Common.Messages.Web/WebMessageRegistries.cs +++ b/Common/Phantom.Common.Messages.Web/WebMessageRegistries.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using Phantom.Common.Data; using Phantom.Common.Data.Java; using Phantom.Common.Data.Minecraft; using Phantom.Common.Data.Replies; @@ -25,21 +26,22 @@ public static class WebMessageRegistries { ToController.Add<UnregisterWebMessage>(1); ToController.Add<LogInMessage, LogInSuccess?>(2); ToController.Add<LogOutMessage>(3); - ToController.Add<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(4); - ToController.Add<CreateUserMessage, CreateUserResult>(5); - ToController.Add<DeleteUserMessage, DeleteUserResult>(6); - ToController.Add<GetUsersMessage, ImmutableArray<UserInfo>>(7); - ToController.Add<GetRolesMessage, ImmutableArray<RoleInfo>>(8); - ToController.Add<GetUserRolesMessage, ImmutableDictionary<Guid, ImmutableArray<Guid>>>(9); - ToController.Add<ChangeUserRolesMessage, ChangeUserRolesResult>(10); - ToController.Add<CreateOrUpdateInstanceMessage, InstanceActionResult<CreateOrUpdateInstanceResult>>(11); - ToController.Add<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(12); - ToController.Add<StopInstanceMessage, InstanceActionResult<StopInstanceResult>>(13); - ToController.Add<SendCommandToInstanceMessage, InstanceActionResult<SendCommandToInstanceResult>>(14); - ToController.Add<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(15); - ToController.Add<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(16); - ToController.Add<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(17); - ToController.Add<GetEventLogMessage, ImmutableArray<EventLogItem>>(18); + ToController.Add<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(4); + ToController.Add<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(5); + ToController.Add<CreateUserMessage, CreateUserResult>(6); + ToController.Add<DeleteUserMessage, DeleteUserResult>(7); + ToController.Add<GetUsersMessage, ImmutableArray<UserInfo>>(8); + ToController.Add<GetRolesMessage, ImmutableArray<RoleInfo>>(9); + ToController.Add<GetUserRolesMessage, ImmutableDictionary<Guid, ImmutableArray<Guid>>>(10); + ToController.Add<ChangeUserRolesMessage, ChangeUserRolesResult>(11); + ToController.Add<CreateOrUpdateInstanceMessage, InstanceActionResult<CreateOrUpdateInstanceResult>>(12); + ToController.Add<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(13); + ToController.Add<StopInstanceMessage, InstanceActionResult<StopInstanceResult>>(14); + ToController.Add<SendCommandToInstanceMessage, InstanceActionResult<SendCommandToInstanceResult>>(15); + ToController.Add<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(16); + ToController.Add<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(17); + ToController.Add<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(18); + ToController.Add<GetEventLogMessage, ImmutableArray<EventLogItem>>(19); ToController.Add<ReplyMessage>(127); ToWeb.Add<RegisterWebResultMessage>(0); diff --git a/Controller/Phantom.Controller.Services/Rpc/WebMessageHandlerActor.cs b/Controller/Phantom.Controller.Services/Rpc/WebMessageHandlerActor.cs index d429b4d..7f64630 100644 --- a/Controller/Phantom.Controller.Services/Rpc/WebMessageHandlerActor.cs +++ b/Controller/Phantom.Controller.Services/Rpc/WebMessageHandlerActor.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using Phantom.Common.Data; using Phantom.Common.Data.Java; using Phantom.Common.Data.Minecraft; using Phantom.Common.Data.Replies; @@ -71,6 +72,7 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> { Receive<UnregisterWebMessage>(HandleUnregisterWeb); ReceiveAndReplyLater<LogInMessage, LogInSuccess?>(HandleLogIn); Receive<LogOutMessage>(HandleLogOut); + ReceiveAndReply<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(GetAuthenticatedUser); ReceiveAndReplyLater<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(HandleCreateOrUpdateAdministratorUser); ReceiveAndReplyLater<CreateUserMessage, CreateUserResult>(HandleCreateUser); ReceiveAndReplyLater<GetUsersMessage, ImmutableArray<UserInfo>>(HandleGetUsers); @@ -104,7 +106,11 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> { private void HandleLogOut(LogOutMessage message) { _ = userLoginManager.LogOut(message.UserGuid, message.SessionToken); } - + + private Optional<AuthenticatedUserInfo> GetAuthenticatedUser(GetAuthenticatedUser message) { + return userLoginManager.GetAuthenticatedUser(message.UserGuid, message.SessionToken); + } + private Task<CreateOrUpdateAdministratorUserResult> HandleCreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUserMessage message) { return userManager.CreateOrUpdateAdministrator(message.Username, message.Password); } diff --git a/Controller/Phantom.Controller.Services/Users/UserLoginManager.cs b/Controller/Phantom.Controller.Services/Users/UserLoginManager.cs index e9f27cc..6584908 100644 --- a/Controller/Phantom.Controller.Services/Users/UserLoginManager.cs +++ b/Controller/Phantom.Controller.Services/Users/UserLoginManager.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using System.Collections.Immutable; +using System.Runtime.CompilerServices; using System.Security.Cryptography; using Phantom.Common.Data.Web.Users; using Phantom.Controller.Database; @@ -9,7 +10,7 @@ namespace Phantom.Controller.Services.Users; sealed class UserLoginManager { private const int SessionIdBytes = 20; - private readonly ConcurrentDictionary<Guid, List<ImmutableArray<byte>>> sessionTokensByUserGuid = new (); + private readonly ConcurrentDictionary<Guid, UserSession> sessionsByUserGuid = new (); private readonly UserManager userManager; private readonly PermissionManager permissionManager; @@ -27,11 +28,11 @@ sealed class UserLoginManager { return null; } + var permissions = await permissionManager.FetchPermissionsForUserId(user.UserGuid); + var userInfo = new AuthenticatedUserInfo(user.UserGuid, user.Name, permissions); var token = ImmutableArray.Create(RandomNumberGenerator.GetBytes(SessionIdBytes)); - var sessionTokens = sessionTokensByUserGuid.GetOrAdd(user.UserGuid, static _ => new List<ImmutableArray<byte>>()); - lock (sessionTokens) { - sessionTokens.Add(token); - } + + sessionsByUserGuid.AddOrUpdate(user.UserGuid, UserSession.Create, UserSession.Add, new NewUserSession(userInfo, token)); await using (var db = dbProvider.Lazy()) { var auditLogWriter = new AuditLogRepository(db).Writer(user.UserGuid); @@ -40,18 +41,18 @@ sealed class UserLoginManager { await db.Ctx.SaveChangesAsync(); } - return new LogInSuccess(user.UserGuid, await permissionManager.FetchPermissionsForUserId(user.UserGuid), token); + return new LogInSuccess(userInfo, token); } - public async Task LogOut(Guid userGuid, ImmutableArray<byte> sessionToken) { - if (!sessionTokensByUserGuid.TryGetValue(userGuid, out var sessionTokens)) { - return; - } - - lock (sessionTokens) { - if (sessionTokens.RemoveAll(token => token.SequenceEqual(sessionToken)) == 0) { + public async Task LogOut(Guid userGuid, ImmutableArray<byte> token) { + while (true) { + if (!sessionsByUserGuid.TryGetValue(userGuid, out var oldSession)) { return; } + + if (sessionsByUserGuid.TryUpdate(userGuid, oldSession.RemoveToken(token), oldSession)) { + break; + } } await using var db = dbProvider.Lazy(); @@ -61,4 +62,46 @@ sealed class UserLoginManager { await db.Ctx.SaveChangesAsync(); } + + public AuthenticatedUserInfo? GetAuthenticatedUser(Guid userGuid, ImmutableArray<byte> token) { + return sessionsByUserGuid.TryGetValue(userGuid, out var session) && session.Tokens.Contains(token, TokenEqualityComparer.Instance) ? session.UserInfo : null; + } + + private readonly record struct NewUserSession(AuthenticatedUserInfo UserInfo, ImmutableArray<byte> Token); + + private sealed record UserSession(AuthenticatedUserInfo UserInfo, ImmutableList<ImmutableArray<byte>> Tokens) { + public static UserSession Create(Guid userGuid, NewUserSession newSession) { + return new UserSession(newSession.UserInfo, ImmutableList.Create(newSession.Token)); + } + + public static UserSession Add(Guid userGuid, UserSession oldSession, NewUserSession newSession) { + return new UserSession(newSession.UserInfo, oldSession.Tokens.Add(newSession.Token)); + } + + public UserSession RemoveToken(ImmutableArray<byte> token) { + return this with { Tokens = Tokens.Remove(token, TokenEqualityComparer.Instance) }; + } + + public bool Equals(UserSession? other) { + return ReferenceEquals(this, other); + } + + public override int GetHashCode() { + return RuntimeHelpers.GetHashCode(this); + } + } + + private sealed class TokenEqualityComparer : IEqualityComparer<ImmutableArray<byte>> { + public static TokenEqualityComparer Instance { get; } = new (); + + private TokenEqualityComparer() {} + + public bool Equals(ImmutableArray<byte> x, ImmutableArray<byte> y) { + return x.SequenceEqual(y); + } + + public int GetHashCode(ImmutableArray<byte> obj) { + throw new NotImplementedException(); + } + } } diff --git a/Web/Phantom.Web.Components/PhantomComponent.cs b/Web/Phantom.Web.Components/PhantomComponent.cs index 42f80f3..d593732 100644 --- a/Web/Phantom.Web.Components/PhantomComponent.cs +++ b/Web/Phantom.Web.Components/PhantomComponent.cs @@ -2,9 +2,8 @@ using Microsoft.AspNetCore.Components.Authorization; using Phantom.Common.Data.Web.Users; using Phantom.Utils.Logging; -using Phantom.Web.Services.Authorization; +using Phantom.Web.Services.Authentication; using ILogger = Serilog.ILogger; -using UserInfo = Phantom.Web.Services.Authentication.UserInfo; namespace Phantom.Web.Components; @@ -14,21 +13,18 @@ public abstract class PhantomComponent : ComponentBase, IDisposable { [CascadingParameter] public Task<AuthenticationState> AuthenticationStateTask { get; set; } = null!; - [Inject] - public PermissionManager PermissionManager { get; set; } = null!; - private readonly CancellationTokenSource cancellationTokenSource = new (); protected CancellationToken CancellationToken => cancellationTokenSource.Token; protected async Task<Guid?> GetUserGuid() { var authenticationState = await AuthenticationStateTask; - return UserInfo.TryGetGuid(authenticationState.User); + return authenticationState.TryGetGuid(); } protected async Task<bool> CheckPermission(Permission permission) { var authenticationState = await AuthenticationStateTask; - return PermissionManager.CheckPermission(authenticationState.User, permission); + return authenticationState.CheckPermission(permission); } protected void InvokeAsyncChecked(Func<Task> task) { diff --git a/Web/Phantom.Web.Services/Authentication/AuthenticationStateExtensions.cs b/Web/Phantom.Web.Services/Authentication/AuthenticationStateExtensions.cs new file mode 100644 index 0000000..18e7907 --- /dev/null +++ b/Web/Phantom.Web.Services/Authentication/AuthenticationStateExtensions.cs @@ -0,0 +1,27 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Components.Authorization; +using Phantom.Common.Data.Web.Users; + +namespace Phantom.Web.Services.Authentication; + +public static class AuthenticationStateExtensions { + public static Guid? TryGetGuid(this AuthenticationState authenticationState) { + return authenticationState.User is CustomClaimsPrincipal customUser ? customUser.UserInfo.Guid : null; + } + + public static PermissionSet GetPermissions(this ClaimsPrincipal user) { + return user is CustomClaimsPrincipal customUser ? customUser.UserInfo.Permissions : PermissionSet.None; + } + + public static bool CheckPermission(this ClaimsPrincipal user, Permission permission) { + return user.GetPermissions().Check(permission); + } + + public static PermissionSet GetPermissions(this AuthenticationState authenticationState) { + return authenticationState.User.GetPermissions(); + } + + public static bool CheckPermission(this AuthenticationState authenticationState, Permission permission) { + return authenticationState.User.CheckPermission(permission); + } +} diff --git a/Web/Phantom.Web.Services/Authentication/CustomAuthenticationStateProvider.cs b/Web/Phantom.Web.Services/Authentication/CustomAuthenticationStateProvider.cs index a53df81..c2ae175 100644 --- a/Web/Phantom.Web.Services/Authentication/CustomAuthenticationStateProvider.cs +++ b/Web/Phantom.Web.Services/Authentication/CustomAuthenticationStateProvider.cs @@ -1,26 +1,30 @@ using System.Security.Claims; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Server; +using Phantom.Common.Data; +using Phantom.Common.Data.Web.Users; +using Phantom.Common.Messages.Web.ToController; +using Phantom.Web.Services.Rpc; namespace Phantom.Web.Services.Authentication; public sealed class CustomAuthenticationStateProvider : ServerAuthenticationStateProvider { - private readonly UserSessionManager sessionManager; private readonly UserSessionBrowserStorage sessionBrowserStorage; + private readonly ControllerConnection controllerConnection; private bool isLoaded; - public CustomAuthenticationStateProvider(UserSessionManager sessionManager, UserSessionBrowserStorage sessionBrowserStorage) { - this.sessionManager = sessionManager; + public CustomAuthenticationStateProvider(UserSessionBrowserStorage sessionBrowserStorage, ControllerConnection controllerConnection) { this.sessionBrowserStorage = sessionBrowserStorage; + this.controllerConnection = controllerConnection; } public override async Task<AuthenticationState> GetAuthenticationStateAsync() { if (!isLoaded) { var stored = await sessionBrowserStorage.Get(); if (stored != null) { - var session = sessionManager.FindWithToken(stored.UserGuid, stored.Token); - if (session != null) { - SetLoadedSession(session); + var session = await controllerConnection.Send<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(new GetAuthenticatedUser(stored.UserGuid, stored.Token), TimeSpan.FromSeconds(30)); + if (session.Value is {} userInfo) { + SetLoadedSession(userInfo); } } } @@ -28,9 +32,9 @@ public sealed class CustomAuthenticationStateProvider : ServerAuthenticationStat return await base.GetAuthenticationStateAsync(); } - internal void SetLoadedSession(UserInfo user) { + internal void SetLoadedSession(AuthenticatedUserInfo user) { isLoaded = true; - SetAuthenticationState(Task.FromResult(new AuthenticationState(user.AsClaimsPrincipal))); + SetAuthenticationState(Task.FromResult(new AuthenticationState(new CustomClaimsPrincipal(user)))); } internal void SetUnloadedSession() { diff --git a/Web/Phantom.Web.Services/Authentication/CustomClaimsPrincipal.cs b/Web/Phantom.Web.Services/Authentication/CustomClaimsPrincipal.cs new file mode 100644 index 0000000..3a0188d --- /dev/null +++ b/Web/Phantom.Web.Services/Authentication/CustomClaimsPrincipal.cs @@ -0,0 +1,18 @@ +using System.Security.Claims; +using Phantom.Common.Data.Web.Users; + +namespace Phantom.Web.Services.Authentication; + +sealed class CustomClaimsPrincipal : ClaimsPrincipal { + internal AuthenticatedUserInfo UserInfo { get; } + + internal CustomClaimsPrincipal(AuthenticatedUserInfo userInfo) : base(GetIdentity(userInfo)) { + UserInfo = userInfo; + } + + private static ClaimsIdentity GetIdentity(AuthenticatedUserInfo userInfo) { + var identity = new ClaimsIdentity("Phantom"); + identity.AddClaim(new Claim(ClaimTypes.Name, userInfo.Name)); + return identity; + } +} diff --git a/Web/Phantom.Web.Services/Authentication/UserInfo.cs b/Web/Phantom.Web.Services/Authentication/UserInfo.cs deleted file mode 100644 index ceb441d..0000000 --- a/Web/Phantom.Web.Services/Authentication/UserInfo.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Security.Claims; -using Phantom.Common.Data.Web.Users; - -namespace Phantom.Web.Services.Authentication; - -public sealed record UserInfo(Guid UserGuid, string Username, PermissionSet Permissions) { - private const string AuthenticationType = "Phantom"; - - internal ClaimsPrincipal AsClaimsPrincipal { - get { - var identity = new ClaimsIdentity(AuthenticationType); - - identity.AddClaim(new Claim(ClaimTypes.Name, Username)); - identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, UserGuid.ToString())); - - return new ClaimsPrincipal(identity); - } - } - - public static Guid? TryGetGuid(ClaimsPrincipal principal) { - return principal.Identity is { IsAuthenticated: true, AuthenticationType: AuthenticationType } && principal.FindFirstValue(ClaimTypes.NameIdentifier) is {} guidStr && Guid.TryParse(guidStr, out var guid) ? guid : null; - } -} diff --git a/Web/Phantom.Web.Services/Authentication/UserLoginManager.cs b/Web/Phantom.Web.Services/Authentication/UserLoginManager.cs index f9d27d4..6a1c914 100644 --- a/Web/Phantom.Web.Services/Authentication/UserLoginManager.cs +++ b/Web/Phantom.Web.Services/Authentication/UserLoginManager.cs @@ -10,14 +10,12 @@ public sealed class UserLoginManager { private static readonly ILogger Logger = PhantomLogger.Create<UserLoginManager>(); private readonly Navigation navigation; - private readonly UserSessionManager sessionManager; private readonly UserSessionBrowserStorage sessionBrowserStorage; private readonly CustomAuthenticationStateProvider authenticationStateProvider; private readonly ControllerConnection controllerConnection; - public UserLoginManager(Navigation navigation, UserSessionManager sessionManager, UserSessionBrowserStorage sessionBrowserStorage, CustomAuthenticationStateProvider authenticationStateProvider, ControllerConnection controllerConnection) { + public UserLoginManager(Navigation navigation, UserSessionBrowserStorage sessionBrowserStorage, CustomAuthenticationStateProvider authenticationStateProvider, ControllerConnection controllerConnection) { this.navigation = navigation; - this.sessionManager = sessionManager; this.sessionBrowserStorage = sessionBrowserStorage; this.authenticationStateProvider = authenticationStateProvider; this.controllerConnection = controllerConnection; @@ -38,13 +36,9 @@ public sealed class UserLoginManager { Logger.Information("Successfully logged in {Username}.", username); - var userGuid = success.UserGuid; - var userInfo = new UserInfo(userGuid, username, success.Permissions); - var token = success.Token; + var userInfo = success.UserInfo; - await sessionBrowserStorage.Store(userGuid, token); - sessionManager.Add(userInfo, token); - + await sessionBrowserStorage.Store(userInfo.Guid, success.Token); authenticationStateProvider.SetLoadedSession(userInfo); await navigation.NavigateTo(returnUrl ?? string.Empty); @@ -53,7 +47,7 @@ public sealed class UserLoginManager { public async Task LogOut() { var stored = await sessionBrowserStorage.Delete(); - if (stored != null && sessionManager.Remove(stored.UserGuid, stored.Token)) { + if (stored != null) { await controllerConnection.Send(new LogOutMessage(stored.UserGuid, stored.Token)); } diff --git a/Web/Phantom.Web.Services/Authentication/UserSessionManager.cs b/Web/Phantom.Web.Services/Authentication/UserSessionManager.cs deleted file mode 100644 index 3c830a1..0000000 --- a/Web/Phantom.Web.Services/Authentication/UserSessionManager.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Collections.Concurrent; -using System.Collections.Immutable; - -namespace Phantom.Web.Services.Authentication; - -public sealed class UserSessionManager { - private readonly ConcurrentDictionary<Guid, UserSessions> userSessions = new (); - - internal void Add(UserInfo user, ImmutableArray<byte> token) { - userSessions.AddOrUpdate( - user.UserGuid, - static (_, u) => new UserSessions(u), - static (_, sessions, u) => sessions.WithUserInfo(u), - user - ).AddToken(token); - } - - internal UserInfo? Find(Guid userGuid) { - return userSessions.TryGetValue(userGuid, out var sessions) ? sessions.UserInfo : null; - } - - internal UserInfo? FindWithToken(Guid userGuid, ImmutableArray<byte> token) { - return userSessions.TryGetValue(userGuid, out var sessions) && sessions.HasToken(token) ? sessions.UserInfo : null; - } - - internal bool Remove(Guid userGuid, ImmutableArray<byte> token) { - if (userSessions.TryGetValue(userGuid, out var sessions)) { - sessions.RemoveToken(token); - return true; - } - else { - return false; - } - } -} diff --git a/Web/Phantom.Web.Services/Authentication/UserSessions.cs b/Web/Phantom.Web.Services/Authentication/UserSessions.cs deleted file mode 100644 index 64a82a8..0000000 --- a/Web/Phantom.Web.Services/Authentication/UserSessions.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Collections.Immutable; -using System.Security.Cryptography; - -namespace Phantom.Web.Services.Authentication; - -sealed class UserSessions { - public UserInfo UserInfo { get; } - - private readonly List<ImmutableArray<byte>> tokens = new (); - - public UserSessions(UserInfo userInfo) { - UserInfo = userInfo; - } - - private UserSessions(UserInfo userInfo, List<ImmutableArray<byte>> tokens) : this(userInfo) { - this.tokens.AddRange(tokens); - } - - public UserSessions WithUserInfo(UserInfo user) { - List<ImmutableArray<byte>> tokensCopy; - lock (tokens) { - tokensCopy = new List<ImmutableArray<byte>>(tokens); - } - - return new UserSessions(user, tokensCopy); - } - - public void AddToken(ImmutableArray<byte> token) { - lock (tokens) { - if (!HasToken(token)) { - tokens.Add(token); - } - } - } - - public bool HasToken(ImmutableArray<byte> token) { - return FindTokenIndex(token) != -1; - } - - private int FindTokenIndex(ImmutableArray<byte> token) { - lock (tokens) { - return tokens.FindIndex(t => CryptographicOperations.FixedTimeEquals(t.AsSpan(), token.AsSpan())); - } - } - - public void RemoveToken(ImmutableArray<byte> token) { - lock (tokens) { - int index = FindTokenIndex(token); - if (index != -1) { - tokens.RemoveAt(index); - } - } - } -} diff --git a/Web/Phantom.Web.Services/Authorization/PermissionBasedPolicyHandler.cs b/Web/Phantom.Web.Services/Authorization/PermissionBasedPolicyHandler.cs index 2e7e976..c2c08cc 100644 --- a/Web/Phantom.Web.Services/Authorization/PermissionBasedPolicyHandler.cs +++ b/Web/Phantom.Web.Services/Authorization/PermissionBasedPolicyHandler.cs @@ -1,16 +1,11 @@ using Microsoft.AspNetCore.Authorization; +using Phantom.Web.Services.Authentication; namespace Phantom.Web.Services.Authorization; sealed class PermissionBasedPolicyHandler : AuthorizationHandler<PermissionBasedPolicyRequirement> { - private readonly PermissionManager permissionManager; - - public PermissionBasedPolicyHandler(PermissionManager permissionManager) { - this.permissionManager = permissionManager; - } - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionBasedPolicyRequirement requirement) { - if (permissionManager.CheckPermission(context.User, requirement.Permission)) { + if (context.User.CheckPermission(requirement.Permission)) { context.Succeed(requirement); } else { diff --git a/Web/Phantom.Web.Services/Authorization/PermissionManager.cs b/Web/Phantom.Web.Services/Authorization/PermissionManager.cs deleted file mode 100644 index 5d5d953..0000000 --- a/Web/Phantom.Web.Services/Authorization/PermissionManager.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Security.Claims; -using Phantom.Common.Data.Web.Users; -using Phantom.Web.Services.Authentication; -using UserInfo = Phantom.Web.Services.Authentication.UserInfo; - -namespace Phantom.Web.Services.Authorization; - -public sealed class PermissionManager { - private readonly UserSessionManager sessionManager; - - public PermissionManager(UserSessionManager sessionManager) { - this.sessionManager = sessionManager; - } - - public PermissionSet GetPermissions(ClaimsPrincipal user) { - return UserInfo.TryGetGuid(user) is {} guid && sessionManager.Find(guid) is {} info ? info.Permissions : PermissionSet.None; - } - - public bool CheckPermission(ClaimsPrincipal user, Permission permission) { - return GetPermissions(user).Check(permission); - } -} diff --git a/Web/Phantom.Web.Services/Authorization/PermissionView.razor b/Web/Phantom.Web.Services/Authorization/PermissionView.razor index 23638f6..b205335 100644 --- a/Web/Phantom.Web.Services/Authorization/PermissionView.razor +++ b/Web/Phantom.Web.Services/Authorization/PermissionView.razor @@ -1,10 +1,9 @@ @using Microsoft.AspNetCore.Components.Authorization @using Phantom.Common.Data.Web.Users -@inject PermissionManager PermissionManager - +@using Phantom.Web.Services.Authentication <AuthorizeView> <Authorized> - @if (ChildContent != null && PermissionManager.CheckPermission(context.User, Permission)) { + @if (ChildContent != null && context.CheckPermission(Permission)) { @ChildContent(context) } </Authorized> diff --git a/Web/Phantom.Web.Services/PhantomWebServices.cs b/Web/Phantom.Web.Services/PhantomWebServices.cs index 374eed4..cb50001 100644 --- a/Web/Phantom.Web.Services/PhantomWebServices.cs +++ b/Web/Phantom.Web.Services/PhantomWebServices.cs @@ -22,7 +22,6 @@ public static class PhantomWebServices { services.AddSingleton<EventLogManager>(); services.AddSingleton<UserManager>(); - services.AddSingleton<UserSessionManager>(); services.AddSingleton<AuditLogManager>(); services.AddScoped<UserLoginManager>(); services.AddScoped<UserSessionBrowserStorage>(); @@ -34,7 +33,6 @@ public static class PhantomWebServices { services.AddScoped<AuthenticationStateProvider>(static services => services.GetRequiredService<CustomAuthenticationStateProvider>()); services.AddAuthorization(ConfigureAuthorization); - services.AddSingleton<PermissionManager>(); services.AddScoped<IAuthorizationHandler, PermissionBasedPolicyHandler>(); } diff --git a/Web/Phantom.Web/Layout/NavMenu.razor b/Web/Phantom.Web/Layout/NavMenu.razor index e92cf1b..d071e3a 100644 --- a/Web/Phantom.Web/Layout/NavMenu.razor +++ b/Web/Phantom.Web/Layout/NavMenu.razor @@ -1,8 +1,7 @@ -@using Phantom.Web.Services -@using Phantom.Web.Services.Authorization +@using Phantom.Web.Services.Authentication +@using Phantom.Web.Services @using Phantom.Common.Data.Web.Users @inject ApplicationProperties ApplicationProperties -@inject PermissionManager PermissionManager <div class="navbar navbar-dark"> <div class="container-fluid"> @@ -24,7 +23,7 @@ <NavMenuItem Label="Login" Icon="account-login" Href="login" /> </NotAuthorized> <Authorized> - @{ var permissions = PermissionManager.GetPermissions(context.User); } + @{ var permissions = context.GetPermissions(); } @if (permissions.Check(Permission.ViewInstances)) { <NavMenuItem Label="Instances" Icon="folder" Href="instances" /> diff --git a/Web/Phantom.Web/Pages/Users.razor b/Web/Phantom.Web/Pages/Users.razor index 8d82028..3dbbe0a 100644 --- a/Web/Phantom.Web/Pages/Users.razor +++ b/Web/Phantom.Web/Pages/Users.razor @@ -1,9 +1,10 @@ @page "/users" @attribute [Authorize(Permission.ViewUsersPolicy)] @using System.Collections.Immutable -@using Phantom.Common.Data.Web.Users -@using Phantom.Web.Services.Users +@using Phantom.Web.Services.Authentication @using Phantom.Web.Services.Authorization +@using Phantom.Web.Services.Users +@using Phantom.Common.Data.Web.Users @inherits Phantom.Web.Components.PhantomComponent @inject UserManager UserManager @inject RoleManager RoleManager @@ -17,7 +18,7 @@ <AuthorizeView> <Authorized> - @{ var canEdit = PermissionManager.CheckPermission(context.User, Permission.EditUsers); } + @{ var canEdit = context.CheckPermission(Permission.EditUsers); } <Table TItem="UserInfo" Items="allUsers"> <HeaderRow> <Column>Username</Column>