mirror of
https://github.com/chylex/Minecraft-Phantom-Panel.git
synced 2025-04-30 06:34:06 +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)]
|
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||||
public sealed partial record LogInSuccess(
|
public sealed partial record LogInSuccess(
|
||||||
[property: MemoryPackOrder(0)] Guid UserGuid,
|
[property: MemoryPackOrder(0)] AuthenticatedUserInfo UserInfo,
|
||||||
[property: MemoryPackOrder(1)] PermissionSet Permissions,
|
[property: MemoryPackOrder(1)] ImmutableArray<byte> Token
|
||||||
[property: MemoryPackOrder(2)] 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 System.Collections.Immutable;
|
||||||
|
using Phantom.Common.Data;
|
||||||
using Phantom.Common.Data.Java;
|
using Phantom.Common.Data.Java;
|
||||||
using Phantom.Common.Data.Minecraft;
|
using Phantom.Common.Data.Minecraft;
|
||||||
using Phantom.Common.Data.Replies;
|
using Phantom.Common.Data.Replies;
|
||||||
@ -25,21 +26,22 @@ public static class WebMessageRegistries {
|
|||||||
ToController.Add<UnregisterWebMessage>(1);
|
ToController.Add<UnregisterWebMessage>(1);
|
||||||
ToController.Add<LogInMessage, LogInSuccess?>(2);
|
ToController.Add<LogInMessage, LogInSuccess?>(2);
|
||||||
ToController.Add<LogOutMessage>(3);
|
ToController.Add<LogOutMessage>(3);
|
||||||
ToController.Add<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(4);
|
ToController.Add<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(4);
|
||||||
ToController.Add<CreateUserMessage, CreateUserResult>(5);
|
ToController.Add<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(5);
|
||||||
ToController.Add<DeleteUserMessage, DeleteUserResult>(6);
|
ToController.Add<CreateUserMessage, CreateUserResult>(6);
|
||||||
ToController.Add<GetUsersMessage, ImmutableArray<UserInfo>>(7);
|
ToController.Add<DeleteUserMessage, DeleteUserResult>(7);
|
||||||
ToController.Add<GetRolesMessage, ImmutableArray<RoleInfo>>(8);
|
ToController.Add<GetUsersMessage, ImmutableArray<UserInfo>>(8);
|
||||||
ToController.Add<GetUserRolesMessage, ImmutableDictionary<Guid, ImmutableArray<Guid>>>(9);
|
ToController.Add<GetRolesMessage, ImmutableArray<RoleInfo>>(9);
|
||||||
ToController.Add<ChangeUserRolesMessage, ChangeUserRolesResult>(10);
|
ToController.Add<GetUserRolesMessage, ImmutableDictionary<Guid, ImmutableArray<Guid>>>(10);
|
||||||
ToController.Add<CreateOrUpdateInstanceMessage, InstanceActionResult<CreateOrUpdateInstanceResult>>(11);
|
ToController.Add<ChangeUserRolesMessage, ChangeUserRolesResult>(11);
|
||||||
ToController.Add<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(12);
|
ToController.Add<CreateOrUpdateInstanceMessage, InstanceActionResult<CreateOrUpdateInstanceResult>>(12);
|
||||||
ToController.Add<StopInstanceMessage, InstanceActionResult<StopInstanceResult>>(13);
|
ToController.Add<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(13);
|
||||||
ToController.Add<SendCommandToInstanceMessage, InstanceActionResult<SendCommandToInstanceResult>>(14);
|
ToController.Add<StopInstanceMessage, InstanceActionResult<StopInstanceResult>>(14);
|
||||||
ToController.Add<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(15);
|
ToController.Add<SendCommandToInstanceMessage, InstanceActionResult<SendCommandToInstanceResult>>(15);
|
||||||
ToController.Add<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(16);
|
ToController.Add<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(16);
|
||||||
ToController.Add<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(17);
|
ToController.Add<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(17);
|
||||||
ToController.Add<GetEventLogMessage, ImmutableArray<EventLogItem>>(18);
|
ToController.Add<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(18);
|
||||||
|
ToController.Add<GetEventLogMessage, ImmutableArray<EventLogItem>>(19);
|
||||||
ToController.Add<ReplyMessage>(127);
|
ToController.Add<ReplyMessage>(127);
|
||||||
|
|
||||||
ToWeb.Add<RegisterWebResultMessage>(0);
|
ToWeb.Add<RegisterWebResultMessage>(0);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
|
using Phantom.Common.Data;
|
||||||
using Phantom.Common.Data.Java;
|
using Phantom.Common.Data.Java;
|
||||||
using Phantom.Common.Data.Minecraft;
|
using Phantom.Common.Data.Minecraft;
|
||||||
using Phantom.Common.Data.Replies;
|
using Phantom.Common.Data.Replies;
|
||||||
@ -71,6 +72,7 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
|
|||||||
Receive<UnregisterWebMessage>(HandleUnregisterWeb);
|
Receive<UnregisterWebMessage>(HandleUnregisterWeb);
|
||||||
ReceiveAndReplyLater<LogInMessage, LogInSuccess?>(HandleLogIn);
|
ReceiveAndReplyLater<LogInMessage, LogInSuccess?>(HandleLogIn);
|
||||||
Receive<LogOutMessage>(HandleLogOut);
|
Receive<LogOutMessage>(HandleLogOut);
|
||||||
|
ReceiveAndReply<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(GetAuthenticatedUser);
|
||||||
ReceiveAndReplyLater<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(HandleCreateOrUpdateAdministratorUser);
|
ReceiveAndReplyLater<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(HandleCreateOrUpdateAdministratorUser);
|
||||||
ReceiveAndReplyLater<CreateUserMessage, CreateUserResult>(HandleCreateUser);
|
ReceiveAndReplyLater<CreateUserMessage, CreateUserResult>(HandleCreateUser);
|
||||||
ReceiveAndReplyLater<GetUsersMessage, ImmutableArray<UserInfo>>(HandleGetUsers);
|
ReceiveAndReplyLater<GetUsersMessage, ImmutableArray<UserInfo>>(HandleGetUsers);
|
||||||
@ -104,7 +106,11 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
|
|||||||
private void HandleLogOut(LogOutMessage message) {
|
private void HandleLogOut(LogOutMessage message) {
|
||||||
_ = userLoginManager.LogOut(message.UserGuid, message.SessionToken);
|
_ = 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) {
|
private Task<CreateOrUpdateAdministratorUserResult> HandleCreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUserMessage message) {
|
||||||
return userManager.CreateOrUpdateAdministrator(message.Username, message.Password);
|
return userManager.CreateOrUpdateAdministrator(message.Username, message.Password);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using Phantom.Common.Data.Web.Users;
|
using Phantom.Common.Data.Web.Users;
|
||||||
using Phantom.Controller.Database;
|
using Phantom.Controller.Database;
|
||||||
@ -9,7 +10,7 @@ namespace Phantom.Controller.Services.Users;
|
|||||||
|
|
||||||
sealed class UserLoginManager {
|
sealed class UserLoginManager {
|
||||||
private const int SessionIdBytes = 20;
|
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 UserManager userManager;
|
||||||
private readonly PermissionManager permissionManager;
|
private readonly PermissionManager permissionManager;
|
||||||
@ -27,11 +28,11 @@ sealed class UserLoginManager {
|
|||||||
return null;
|
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 token = ImmutableArray.Create(RandomNumberGenerator.GetBytes(SessionIdBytes));
|
||||||
var sessionTokens = sessionTokensByUserGuid.GetOrAdd(user.UserGuid, static _ => new List<ImmutableArray<byte>>());
|
|
||||||
lock (sessionTokens) {
|
sessionsByUserGuid.AddOrUpdate(user.UserGuid, UserSession.Create, UserSession.Add, new NewUserSession(userInfo, token));
|
||||||
sessionTokens.Add(token);
|
|
||||||
}
|
|
||||||
|
|
||||||
await using (var db = dbProvider.Lazy()) {
|
await using (var db = dbProvider.Lazy()) {
|
||||||
var auditLogWriter = new AuditLogRepository(db).Writer(user.UserGuid);
|
var auditLogWriter = new AuditLogRepository(db).Writer(user.UserGuid);
|
||||||
@ -40,18 +41,18 @@ sealed class UserLoginManager {
|
|||||||
await db.Ctx.SaveChangesAsync();
|
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) {
|
public async Task LogOut(Guid userGuid, ImmutableArray<byte> token) {
|
||||||
if (!sessionTokensByUserGuid.TryGetValue(userGuid, out var sessionTokens)) {
|
while (true) {
|
||||||
return;
|
if (!sessionsByUserGuid.TryGetValue(userGuid, out var oldSession)) {
|
||||||
}
|
|
||||||
|
|
||||||
lock (sessionTokens) {
|
|
||||||
if (sessionTokens.RemoveAll(token => token.SequenceEqual(sessionToken)) == 0) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sessionsByUserGuid.TryUpdate(userGuid, oldSession.RemoveToken(token), oldSession)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await using var db = dbProvider.Lazy();
|
await using var db = dbProvider.Lazy();
|
||||||
@ -61,4 +62,46 @@ sealed class UserLoginManager {
|
|||||||
|
|
||||||
await db.Ctx.SaveChangesAsync();
|
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 Microsoft.AspNetCore.Components.Authorization;
|
||||||
using Phantom.Common.Data.Web.Users;
|
using Phantom.Common.Data.Web.Users;
|
||||||
using Phantom.Utils.Logging;
|
using Phantom.Utils.Logging;
|
||||||
using Phantom.Web.Services.Authorization;
|
using Phantom.Web.Services.Authentication;
|
||||||
using ILogger = Serilog.ILogger;
|
using ILogger = Serilog.ILogger;
|
||||||
using UserInfo = Phantom.Web.Services.Authentication.UserInfo;
|
|
||||||
|
|
||||||
namespace Phantom.Web.Components;
|
namespace Phantom.Web.Components;
|
||||||
|
|
||||||
@ -14,21 +13,18 @@ public abstract class PhantomComponent : ComponentBase, IDisposable {
|
|||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
public Task<AuthenticationState> AuthenticationStateTask { get; set; } = null!;
|
public Task<AuthenticationState> AuthenticationStateTask { get; set; } = null!;
|
||||||
|
|
||||||
[Inject]
|
|
||||||
public PermissionManager PermissionManager { get; set; } = null!;
|
|
||||||
|
|
||||||
private readonly CancellationTokenSource cancellationTokenSource = new ();
|
private readonly CancellationTokenSource cancellationTokenSource = new ();
|
||||||
|
|
||||||
protected CancellationToken CancellationToken => cancellationTokenSource.Token;
|
protected CancellationToken CancellationToken => cancellationTokenSource.Token;
|
||||||
|
|
||||||
protected async Task<Guid?> GetUserGuid() {
|
protected async Task<Guid?> GetUserGuid() {
|
||||||
var authenticationState = await AuthenticationStateTask;
|
var authenticationState = await AuthenticationStateTask;
|
||||||
return UserInfo.TryGetGuid(authenticationState.User);
|
return authenticationState.TryGetGuid();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task<bool> CheckPermission(Permission permission) {
|
protected async Task<bool> CheckPermission(Permission permission) {
|
||||||
var authenticationState = await AuthenticationStateTask;
|
var authenticationState = await AuthenticationStateTask;
|
||||||
return PermissionManager.CheckPermission(authenticationState.User, permission);
|
return authenticationState.CheckPermission(permission);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void InvokeAsyncChecked(Func<Task> task) {
|
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 System.Security.Claims;
|
||||||
using Microsoft.AspNetCore.Components.Authorization;
|
using Microsoft.AspNetCore.Components.Authorization;
|
||||||
using Microsoft.AspNetCore.Components.Server;
|
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;
|
namespace Phantom.Web.Services.Authentication;
|
||||||
|
|
||||||
public sealed class CustomAuthenticationStateProvider : ServerAuthenticationStateProvider {
|
public sealed class CustomAuthenticationStateProvider : ServerAuthenticationStateProvider {
|
||||||
private readonly UserSessionManager sessionManager;
|
|
||||||
private readonly UserSessionBrowserStorage sessionBrowserStorage;
|
private readonly UserSessionBrowserStorage sessionBrowserStorage;
|
||||||
|
private readonly ControllerConnection controllerConnection;
|
||||||
private bool isLoaded;
|
private bool isLoaded;
|
||||||
|
|
||||||
public CustomAuthenticationStateProvider(UserSessionManager sessionManager, UserSessionBrowserStorage sessionBrowserStorage) {
|
public CustomAuthenticationStateProvider(UserSessionBrowserStorage sessionBrowserStorage, ControllerConnection controllerConnection) {
|
||||||
this.sessionManager = sessionManager;
|
|
||||||
this.sessionBrowserStorage = sessionBrowserStorage;
|
this.sessionBrowserStorage = sessionBrowserStorage;
|
||||||
|
this.controllerConnection = controllerConnection;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<AuthenticationState> GetAuthenticationStateAsync() {
|
public override async Task<AuthenticationState> GetAuthenticationStateAsync() {
|
||||||
if (!isLoaded) {
|
if (!isLoaded) {
|
||||||
var stored = await sessionBrowserStorage.Get();
|
var stored = await sessionBrowserStorage.Get();
|
||||||
if (stored != null) {
|
if (stored != null) {
|
||||||
var session = sessionManager.FindWithToken(stored.UserGuid, stored.Token);
|
var session = await controllerConnection.Send<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(new GetAuthenticatedUser(stored.UserGuid, stored.Token), TimeSpan.FromSeconds(30));
|
||||||
if (session != null) {
|
if (session.Value is {} userInfo) {
|
||||||
SetLoadedSession(session);
|
SetLoadedSession(userInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -28,9 +32,9 @@ public sealed class CustomAuthenticationStateProvider : ServerAuthenticationStat
|
|||||||
return await base.GetAuthenticationStateAsync();
|
return await base.GetAuthenticationStateAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void SetLoadedSession(UserInfo user) {
|
internal void SetLoadedSession(AuthenticatedUserInfo user) {
|
||||||
isLoaded = true;
|
isLoaded = true;
|
||||||
SetAuthenticationState(Task.FromResult(new AuthenticationState(user.AsClaimsPrincipal)));
|
SetAuthenticationState(Task.FromResult(new AuthenticationState(new CustomClaimsPrincipal(user))));
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void SetUnloadedSession() {
|
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 static readonly ILogger Logger = PhantomLogger.Create<UserLoginManager>();
|
||||||
|
|
||||||
private readonly Navigation navigation;
|
private readonly Navigation navigation;
|
||||||
private readonly UserSessionManager sessionManager;
|
|
||||||
private readonly UserSessionBrowserStorage sessionBrowserStorage;
|
private readonly UserSessionBrowserStorage sessionBrowserStorage;
|
||||||
private readonly CustomAuthenticationStateProvider authenticationStateProvider;
|
private readonly CustomAuthenticationStateProvider authenticationStateProvider;
|
||||||
private readonly ControllerConnection controllerConnection;
|
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.navigation = navigation;
|
||||||
this.sessionManager = sessionManager;
|
|
||||||
this.sessionBrowserStorage = sessionBrowserStorage;
|
this.sessionBrowserStorage = sessionBrowserStorage;
|
||||||
this.authenticationStateProvider = authenticationStateProvider;
|
this.authenticationStateProvider = authenticationStateProvider;
|
||||||
this.controllerConnection = controllerConnection;
|
this.controllerConnection = controllerConnection;
|
||||||
@ -38,13 +36,9 @@ public sealed class UserLoginManager {
|
|||||||
|
|
||||||
Logger.Information("Successfully logged in {Username}.", username);
|
Logger.Information("Successfully logged in {Username}.", username);
|
||||||
|
|
||||||
var userGuid = success.UserGuid;
|
var userInfo = success.UserInfo;
|
||||||
var userInfo = new UserInfo(userGuid, username, success.Permissions);
|
|
||||||
var token = success.Token;
|
|
||||||
|
|
||||||
await sessionBrowserStorage.Store(userGuid, token);
|
await sessionBrowserStorage.Store(userInfo.Guid, success.Token);
|
||||||
sessionManager.Add(userInfo, token);
|
|
||||||
|
|
||||||
authenticationStateProvider.SetLoadedSession(userInfo);
|
authenticationStateProvider.SetLoadedSession(userInfo);
|
||||||
await navigation.NavigateTo(returnUrl ?? string.Empty);
|
await navigation.NavigateTo(returnUrl ?? string.Empty);
|
||||||
|
|
||||||
@ -53,7 +47,7 @@ public sealed class UserLoginManager {
|
|||||||
|
|
||||||
public async Task LogOut() {
|
public async Task LogOut() {
|
||||||
var stored = await sessionBrowserStorage.Delete();
|
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));
|
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 Microsoft.AspNetCore.Authorization;
|
||||||
|
using Phantom.Web.Services.Authentication;
|
||||||
|
|
||||||
namespace Phantom.Web.Services.Authorization;
|
namespace Phantom.Web.Services.Authorization;
|
||||||
|
|
||||||
sealed class PermissionBasedPolicyHandler : AuthorizationHandler<PermissionBasedPolicyRequirement> {
|
sealed class PermissionBasedPolicyHandler : AuthorizationHandler<PermissionBasedPolicyRequirement> {
|
||||||
private readonly PermissionManager permissionManager;
|
|
||||||
|
|
||||||
public PermissionBasedPolicyHandler(PermissionManager permissionManager) {
|
|
||||||
this.permissionManager = permissionManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionBasedPolicyRequirement requirement) {
|
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionBasedPolicyRequirement requirement) {
|
||||||
if (permissionManager.CheckPermission(context.User, requirement.Permission)) {
|
if (context.User.CheckPermission(requirement.Permission)) {
|
||||||
context.Succeed(requirement);
|
context.Succeed(requirement);
|
||||||
}
|
}
|
||||||
else {
|
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 Microsoft.AspNetCore.Components.Authorization
|
||||||
@using Phantom.Common.Data.Web.Users
|
@using Phantom.Common.Data.Web.Users
|
||||||
@inject PermissionManager PermissionManager
|
@using Phantom.Web.Services.Authentication
|
||||||
|
|
||||||
<AuthorizeView>
|
<AuthorizeView>
|
||||||
<Authorized>
|
<Authorized>
|
||||||
@if (ChildContent != null && PermissionManager.CheckPermission(context.User, Permission)) {
|
@if (ChildContent != null && context.CheckPermission(Permission)) {
|
||||||
@ChildContent(context)
|
@ChildContent(context)
|
||||||
}
|
}
|
||||||
</Authorized>
|
</Authorized>
|
||||||
|
@ -22,7 +22,6 @@ public static class PhantomWebServices {
|
|||||||
services.AddSingleton<EventLogManager>();
|
services.AddSingleton<EventLogManager>();
|
||||||
|
|
||||||
services.AddSingleton<UserManager>();
|
services.AddSingleton<UserManager>();
|
||||||
services.AddSingleton<UserSessionManager>();
|
|
||||||
services.AddSingleton<AuditLogManager>();
|
services.AddSingleton<AuditLogManager>();
|
||||||
services.AddScoped<UserLoginManager>();
|
services.AddScoped<UserLoginManager>();
|
||||||
services.AddScoped<UserSessionBrowserStorage>();
|
services.AddScoped<UserSessionBrowserStorage>();
|
||||||
@ -34,7 +33,6 @@ public static class PhantomWebServices {
|
|||||||
services.AddScoped<AuthenticationStateProvider>(static services => services.GetRequiredService<CustomAuthenticationStateProvider>());
|
services.AddScoped<AuthenticationStateProvider>(static services => services.GetRequiredService<CustomAuthenticationStateProvider>());
|
||||||
|
|
||||||
services.AddAuthorization(ConfigureAuthorization);
|
services.AddAuthorization(ConfigureAuthorization);
|
||||||
services.AddSingleton<PermissionManager>();
|
|
||||||
services.AddScoped<IAuthorizationHandler, PermissionBasedPolicyHandler>();
|
services.AddScoped<IAuthorizationHandler, PermissionBasedPolicyHandler>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
@using Phantom.Web.Services
|
@using Phantom.Web.Services.Authentication
|
||||||
@using Phantom.Web.Services.Authorization
|
@using Phantom.Web.Services
|
||||||
@using Phantom.Common.Data.Web.Users
|
@using Phantom.Common.Data.Web.Users
|
||||||
@inject ApplicationProperties ApplicationProperties
|
@inject ApplicationProperties ApplicationProperties
|
||||||
@inject PermissionManager PermissionManager
|
|
||||||
|
|
||||||
<div class="navbar navbar-dark">
|
<div class="navbar navbar-dark">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
@ -24,7 +23,7 @@
|
|||||||
<NavMenuItem Label="Login" Icon="account-login" Href="login" />
|
<NavMenuItem Label="Login" Icon="account-login" Href="login" />
|
||||||
</NotAuthorized>
|
</NotAuthorized>
|
||||||
<Authorized>
|
<Authorized>
|
||||||
@{ var permissions = PermissionManager.GetPermissions(context.User); }
|
@{ var permissions = context.GetPermissions(); }
|
||||||
|
|
||||||
@if (permissions.Check(Permission.ViewInstances)) {
|
@if (permissions.Check(Permission.ViewInstances)) {
|
||||||
<NavMenuItem Label="Instances" Icon="folder" Href="instances" />
|
<NavMenuItem Label="Instances" Icon="folder" Href="instances" />
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
@page "/users"
|
@page "/users"
|
||||||
@attribute [Authorize(Permission.ViewUsersPolicy)]
|
@attribute [Authorize(Permission.ViewUsersPolicy)]
|
||||||
@using System.Collections.Immutable
|
@using System.Collections.Immutable
|
||||||
@using Phantom.Common.Data.Web.Users
|
@using Phantom.Web.Services.Authentication
|
||||||
@using Phantom.Web.Services.Users
|
|
||||||
@using Phantom.Web.Services.Authorization
|
@using Phantom.Web.Services.Authorization
|
||||||
|
@using Phantom.Web.Services.Users
|
||||||
|
@using Phantom.Common.Data.Web.Users
|
||||||
@inherits Phantom.Web.Components.PhantomComponent
|
@inherits Phantom.Web.Components.PhantomComponent
|
||||||
@inject UserManager UserManager
|
@inject UserManager UserManager
|
||||||
@inject RoleManager RoleManager
|
@inject RoleManager RoleManager
|
||||||
@ -17,7 +18,7 @@
|
|||||||
|
|
||||||
<AuthorizeView>
|
<AuthorizeView>
|
||||||
<Authorized>
|
<Authorized>
|
||||||
@{ var canEdit = PermissionManager.CheckPermission(context.User, Permission.EditUsers); }
|
@{ var canEdit = context.CheckPermission(Permission.EditUsers); }
|
||||||
<Table TItem="UserInfo" Items="allUsers">
|
<Table TItem="UserInfo" Items="allUsers">
|
||||||
<HeaderRow>
|
<HeaderRow>
|
||||||
<Column>Username</Column>
|
<Column>Username</Column>
|
||||||
|
Loading…
Reference in New Issue
Block a user