diff --git a/Common/Phantom.Common.Messages.Web/ToController/LogOutMessage.cs b/Common/Phantom.Common.Messages.Web/ToController/LogOutMessage.cs new file mode 100644 index 0000000..a89519a --- /dev/null +++ b/Common/Phantom.Common.Messages.Web/ToController/LogOutMessage.cs @@ -0,0 +1,10 @@ +using System.Collections.Immutable; +using MemoryPack; + +namespace Phantom.Common.Messages.Web.ToController; + +[MemoryPackable(GenerateType.VersionTolerant)] +public sealed partial record LogOutMessage( + [property: MemoryPackOrder(0)] Guid UserGuid, + [property: MemoryPackOrder(1)] ImmutableArray<byte> SessionToken +) : IMessageToController; diff --git a/Common/Phantom.Common.Messages.Web/WebMessageRegistries.cs b/Common/Phantom.Common.Messages.Web/WebMessageRegistries.cs index cb97742..bf48c78 100644 --- a/Common/Phantom.Common.Messages.Web/WebMessageRegistries.cs +++ b/Common/Phantom.Common.Messages.Web/WebMessageRegistries.cs @@ -24,21 +24,22 @@ public static class WebMessageRegistries { ToController.Add<RegisterWebMessage>(0); ToController.Add<UnregisterWebMessage>(1); ToController.Add<LogInMessage, LogInSuccess?>(2); - ToController.Add<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(3); - ToController.Add<CreateUserMessage, CreateUserResult>(4); - ToController.Add<DeleteUserMessage, DeleteUserResult>(5); - ToController.Add<GetUsersMessage, ImmutableArray<UserInfo>>(6); - ToController.Add<GetRolesMessage, ImmutableArray<RoleInfo>>(7); - ToController.Add<GetUserRolesMessage, ImmutableDictionary<Guid, ImmutableArray<Guid>>>(8); - ToController.Add<ChangeUserRolesMessage, ChangeUserRolesResult>(9); - ToController.Add<CreateOrUpdateInstanceMessage, InstanceActionResult<CreateOrUpdateInstanceResult>>(10); - ToController.Add<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(11); - ToController.Add<StopInstanceMessage, InstanceActionResult<StopInstanceResult>>(12); - ToController.Add<SendCommandToInstanceMessage, InstanceActionResult<SendCommandToInstanceResult>>(13); - ToController.Add<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(14); - ToController.Add<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(15); - ToController.Add<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(16); - ToController.Add<GetEventLogMessage, ImmutableArray<EventLogItem>>(17); + 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<ReplyMessage>(127); ToWeb.Add<RegisterWebResultMessage>(0); diff --git a/Controller/Phantom.Controller.Services/ControllerServices.cs b/Controller/Phantom.Controller.Services/ControllerServices.cs index 7e9abc2..6b70bb9 100644 --- a/Controller/Phantom.Controller.Services/ControllerServices.cs +++ b/Controller/Phantom.Controller.Services/ControllerServices.cs @@ -59,7 +59,7 @@ public sealed class ControllerServices : IDisposable { this.PermissionManager = new PermissionManager(dbProvider); this.UserRoleManager = new UserRoleManager(dbProvider); - this.UserLoginManager = new UserLoginManager(UserManager, PermissionManager); + this.UserLoginManager = new UserLoginManager(UserManager, PermissionManager, dbProvider); this.AuditLogManager = new AuditLogManager(dbProvider); this.EventLogManager = new EventLogManager(ActorSystem, dbProvider, shutdownCancellationToken); diff --git a/Controller/Phantom.Controller.Services/Rpc/WebMessageHandlerActor.cs b/Controller/Phantom.Controller.Services/Rpc/WebMessageHandlerActor.cs index 881de92..d429b4d 100644 --- a/Controller/Phantom.Controller.Services/Rpc/WebMessageHandlerActor.cs +++ b/Controller/Phantom.Controller.Services/Rpc/WebMessageHandlerActor.cs @@ -69,6 +69,8 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> { ReceiveAsync<RegisterWebMessage>(HandleRegisterWeb); Receive<UnregisterWebMessage>(HandleUnregisterWeb); + ReceiveAndReplyLater<LogInMessage, LogInSuccess?>(HandleLogIn); + Receive<LogOutMessage>(HandleLogOut); ReceiveAndReplyLater<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(HandleCreateOrUpdateAdministratorUser); ReceiveAndReplyLater<CreateUserMessage, CreateUserResult>(HandleCreateUser); ReceiveAndReplyLater<GetUsersMessage, ImmutableArray<UserInfo>>(HandleGetUsers); @@ -84,7 +86,6 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> { ReceiveAndReply<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(HandleGetAgentJavaRuntimes); ReceiveAndReplyLater<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(HandleGetAuditLog); ReceiveAndReplyLater<GetEventLogMessage, ImmutableArray<EventLogItem>>(HandleGetEventLog); - ReceiveAndReplyLater<LogInMessage, LogInSuccess?>(HandleLogIn); Receive<ReplyMessage>(HandleReply); } @@ -96,6 +97,14 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> { connection.Close(); } + private Task<LogInSuccess?> HandleLogIn(LogInMessage message) { + return userLoginManager.LogIn(message.Username, message.Password); + } + + private void HandleLogOut(LogOutMessage message) { + _ = userLoginManager.LogOut(message.UserGuid, message.SessionToken); + } + private Task<CreateOrUpdateAdministratorUserResult> HandleCreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUserMessage message) { return userManager.CreateOrUpdateAdministrator(message.Username, message.Password); } @@ -156,10 +165,6 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> { return eventLogManager.GetMostRecentItems(message.Count); } - private Task<LogInSuccess?> HandleLogIn(LogInMessage message) { - return userLoginManager.LogIn(message.Username, message.Password); - } - private void HandleReply(ReplyMessage message) { connection.Receive(message); } diff --git a/Controller/Phantom.Controller.Services/Users/UserLoginManager.cs b/Controller/Phantom.Controller.Services/Users/UserLoginManager.cs index 889949a..e9f27cc 100644 --- a/Controller/Phantom.Controller.Services/Users/UserLoginManager.cs +++ b/Controller/Phantom.Controller.Services/Users/UserLoginManager.cs @@ -2,19 +2,23 @@ using System.Collections.Immutable; using System.Security.Cryptography; using Phantom.Common.Data.Web.Users; +using Phantom.Controller.Database; +using Phantom.Controller.Database.Repositories; namespace Phantom.Controller.Services.Users; sealed class UserLoginManager { private const int SessionIdBytes = 20; - private readonly ConcurrentDictionary<string, List<ImmutableArray<byte>>> sessionTokensByUsername = new (); + private readonly ConcurrentDictionary<Guid, List<ImmutableArray<byte>>> sessionTokensByUserGuid = new (); private readonly UserManager userManager; private readonly PermissionManager permissionManager; + private readonly IDbContextProvider dbProvider; - public UserLoginManager(UserManager userManager, PermissionManager permissionManager) { + public UserLoginManager(UserManager userManager, PermissionManager permissionManager, IDbContextProvider dbProvider) { this.userManager = userManager; this.permissionManager = permissionManager; + this.dbProvider = dbProvider; } public async Task<LogInSuccess?> LogIn(string username, string password) { @@ -24,11 +28,37 @@ sealed class UserLoginManager { } var token = ImmutableArray.Create(RandomNumberGenerator.GetBytes(SessionIdBytes)); - var sessionTokens = sessionTokensByUsername.GetOrAdd(username, static _ => new List<ImmutableArray<byte>>()); + var sessionTokens = sessionTokensByUserGuid.GetOrAdd(user.UserGuid, static _ => new List<ImmutableArray<byte>>()); lock (sessionTokens) { sessionTokens.Add(token); } - + + await using (var db = dbProvider.Lazy()) { + var auditLogWriter = new AuditLogRepository(db).Writer(user.UserGuid); + auditLogWriter.UserLoggedIn(user); + + await db.Ctx.SaveChangesAsync(); + } + return new LogInSuccess(user.UserGuid, await permissionManager.FetchPermissionsForUserId(user.UserGuid), 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) { + return; + } + } + + await using var db = dbProvider.Lazy(); + + var auditLogWriter = new AuditLogRepository(db).Writer(userGuid); + auditLogWriter.UserLoggedOut(userGuid); + + await db.Ctx.SaveChangesAsync(); + } } diff --git a/Web/Phantom.Web.Services/Authentication/UserLoginManager.cs b/Web/Phantom.Web.Services/Authentication/UserLoginManager.cs index 3f6432a..f9d27d4 100644 --- a/Web/Phantom.Web.Services/Authentication/UserLoginManager.cs +++ b/Web/Phantom.Web.Services/Authentication/UserLoginManager.cs @@ -53,8 +53,8 @@ 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 && sessionManager.Remove(stored.UserGuid, stored.Token)) { + await controllerConnection.Send(new LogOutMessage(stored.UserGuid, stored.Token)); } await navigation.NavigateTo(string.Empty); diff --git a/Web/Phantom.Web.Services/Authentication/UserSessionManager.cs b/Web/Phantom.Web.Services/Authentication/UserSessionManager.cs index 3e5e424..3c830a1 100644 --- a/Web/Phantom.Web.Services/Authentication/UserSessionManager.cs +++ b/Web/Phantom.Web.Services/Authentication/UserSessionManager.cs @@ -23,9 +23,13 @@ public sealed class UserSessionManager { return userSessions.TryGetValue(userGuid, out var sessions) && sessions.HasToken(token) ? sessions.UserInfo : null; } - internal void Remove(Guid userGuid, ImmutableArray<byte> token) { + internal bool Remove(Guid userGuid, ImmutableArray<byte> token) { if (userSessions.TryGetValue(userGuid, out var sessions)) { sessions.RemoveToken(token); + return true; + } + else { + return false; } } }