mirror of
https://github.com/chylex/Minecraft-Phantom-Panel.git
synced 2025-04-22 10:15:48 +02:00
Remove login session management logic from Web
This commit is contained in:
parent
d591318340
commit
c99f5bc6bf
Common
Phantom.Common.Data.Web/Users
Phantom.Common.Data
Phantom.Common.Messages.Web
Controller/Phantom.Controller.Services
Web
Phantom.Web.Components
Phantom.Web.Services
Authentication
AuthenticationStateExtensions.csCustomAuthenticationStateProvider.csCustomClaimsPrincipal.csUserInfo.csUserLoginManager.csUserSessionManager.csUserSessions.cs
Authorization
PhantomWebServices.csPhantom.Web
@ -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
|
||||
);
|
@ -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
|
||||
);
|
||||
|
10
Common/Phantom.Common.Data/Optional.cs
Normal file
10
Common/Phantom.Common.Data/Optional.cs
Normal file
@ -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);
|
||||
}
|
||||
}
|
@ -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>>;
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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() {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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>();
|
||||
}
|
||||
|
||||
|
@ -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" />
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user