diff --git a/Common/Phantom.Common.Data.Web/Users/LogInSuccess.cs b/Common/Phantom.Common.Data.Web/Users/LogInSuccess.cs index 28d12c0..ecd921d 100644 --- a/Common/Phantom.Common.Data.Web/Users/LogInSuccess.cs +++ b/Common/Phantom.Common.Data.Web/Users/LogInSuccess.cs @@ -6,5 +6,5 @@ namespace Phantom.Common.Data.Web.Users; [MemoryPackable(GenerateType.VersionTolerant)] public sealed partial record LogInSuccess( [property: MemoryPackOrder(0)] AuthenticatedUserInfo UserInfo, - [property: MemoryPackOrder(1)] ImmutableArray<byte> Token + [property: MemoryPackOrder(1)] ImmutableArray<byte> AuthToken ); diff --git a/Common/Phantom.Common.Data.Web/Users/UserActionFailure.cs b/Common/Phantom.Common.Data.Web/Users/UserActionFailure.cs new file mode 100644 index 0000000..3790d1e --- /dev/null +++ b/Common/Phantom.Common.Data.Web/Users/UserActionFailure.cs @@ -0,0 +1,5 @@ +namespace Phantom.Common.Data.Web.Users; + +public enum UserActionFailure { + NotAuthorized +} diff --git a/Common/Phantom.Common.Data.Web/Users/UserInstanceActionFailure.cs b/Common/Phantom.Common.Data.Web/Users/UserInstanceActionFailure.cs new file mode 100644 index 0000000..95789d5 --- /dev/null +++ b/Common/Phantom.Common.Data.Web/Users/UserInstanceActionFailure.cs @@ -0,0 +1,25 @@ +using MemoryPack; +using Phantom.Common.Data.Replies; + +namespace Phantom.Common.Data.Web.Users; + +[MemoryPackable] +[MemoryPackUnion(0, typeof(OfUserActionFailure))] +[MemoryPackUnion(1, typeof(OfInstanceActionFailure))] +public abstract partial record UserInstanceActionFailure { + internal UserInstanceActionFailure() {} + + public static implicit operator UserInstanceActionFailure(UserActionFailure failure) { + return new OfUserActionFailure(failure); + } + + public static implicit operator UserInstanceActionFailure(InstanceActionFailure failure) { + return new OfInstanceActionFailure(failure); + } +} + +[MemoryPackable(GenerateType.VersionTolerant)] +public sealed partial record OfUserActionFailure([property: MemoryPackOrder(0)] UserActionFailure Failure) : UserInstanceActionFailure; + +[MemoryPackable(GenerateType.VersionTolerant)] +public sealed partial record OfInstanceActionFailure([property: MemoryPackOrder(0)] InstanceActionFailure Failure) : UserInstanceActionFailure; diff --git a/Common/Phantom.Common.Data/Result.cs b/Common/Phantom.Common.Data/Result.cs index 5fb88f8..507680e 100644 --- a/Common/Phantom.Common.Data/Result.cs +++ b/Common/Phantom.Common.Data/Result.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using MemoryPack; +using Phantom.Utils.Result; namespace Phantom.Common.Data; @@ -33,10 +34,18 @@ public sealed partial class Result<TValue, TError> { return hasValue && EqualityComparer<TValue>.Default.Equals(value, expectedValue); } - public TOutput Map<TOutput>(Func<TValue, TOutput> valueConverter, Func<TError, TOutput> errorConverter) { + public TOutput Into<TOutput>(Func<TValue, TOutput> valueConverter, Func<TError, TOutput> errorConverter) { return hasValue ? valueConverter(value!) : errorConverter(error!); } + public Result<TValue, TNewError> MapError<TNewError>(Func<TError, TNewError> errorConverter) { + return hasValue ? value! : errorConverter(error!); + } + + public Utils.Result.Result Variant() { + return hasValue ? new Ok<TValue>(Value) : new Err<TError>(Error); + } + public static implicit operator Result<TValue, TError>(TValue value) { return new Result<TValue, TError>(hasValue: true, value, default); } diff --git a/Common/Phantom.Common.Messages.Web/ToController/ChangeUserRolesMessage.cs b/Common/Phantom.Common.Messages.Web/ToController/ChangeUserRolesMessage.cs index 20b267e..63aa15b 100644 --- a/Common/Phantom.Common.Messages.Web/ToController/ChangeUserRolesMessage.cs +++ b/Common/Phantom.Common.Messages.Web/ToController/ChangeUserRolesMessage.cs @@ -1,5 +1,6 @@ using System.Collections.Immutable; using MemoryPack; +using Phantom.Common.Data; using Phantom.Common.Data.Web.Users; using Phantom.Utils.Actor; @@ -7,8 +8,8 @@ namespace Phantom.Common.Messages.Web.ToController; [MemoryPackable(GenerateType.VersionTolerant)] public sealed partial record ChangeUserRolesMessage( - [property: MemoryPackOrder(0)] Guid LoggedInUserGuid, + [property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken, [property: MemoryPackOrder(1)] Guid SubjectUserGuid, [property: MemoryPackOrder(2)] ImmutableHashSet<Guid> AddToRoleGuids, [property: MemoryPackOrder(3)] ImmutableHashSet<Guid> RemoveFromRoleGuids -) : IMessageToController, ICanReply<ChangeUserRolesResult>; +) : IMessageToController, ICanReply<Result<ChangeUserRolesResult, UserActionFailure>>; diff --git a/Common/Phantom.Common.Messages.Web/ToController/CreateOrUpdateInstanceMessage.cs b/Common/Phantom.Common.Messages.Web/ToController/CreateOrUpdateInstanceMessage.cs index a35cd78..180e6ff 100644 --- a/Common/Phantom.Common.Messages.Web/ToController/CreateOrUpdateInstanceMessage.cs +++ b/Common/Phantom.Common.Messages.Web/ToController/CreateOrUpdateInstanceMessage.cs @@ -1,15 +1,16 @@ -using MemoryPack; +using System.Collections.Immutable; +using MemoryPack; using Phantom.Common.Data; using Phantom.Common.Data.Instance; -using Phantom.Common.Data.Replies; using Phantom.Common.Data.Web.Instance; +using Phantom.Common.Data.Web.Users; using Phantom.Utils.Actor; -namespace Phantom.Common.Messages.Web.ToController; +namespace Phantom.Common.Messages.Web.ToController; [MemoryPackable(GenerateType.VersionTolerant)] public sealed partial record CreateOrUpdateInstanceMessage( - [property: MemoryPackOrder(0)] Guid LoggedInUserGuid, + [property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken, [property: MemoryPackOrder(1)] Guid InstanceGuid, [property: MemoryPackOrder(2)] InstanceConfiguration Configuration -) : IMessageToController, ICanReply<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>; +) : IMessageToController, ICanReply<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>; diff --git a/Common/Phantom.Common.Messages.Web/ToController/CreateUserMessage.cs b/Common/Phantom.Common.Messages.Web/ToController/CreateUserMessage.cs index dbbdc86..1b0384a 100644 --- a/Common/Phantom.Common.Messages.Web/ToController/CreateUserMessage.cs +++ b/Common/Phantom.Common.Messages.Web/ToController/CreateUserMessage.cs @@ -1,4 +1,6 @@ -using MemoryPack; +using System.Collections.Immutable; +using MemoryPack; +using Phantom.Common.Data; using Phantom.Common.Data.Web.Users; using Phantom.Utils.Actor; @@ -6,7 +8,7 @@ namespace Phantom.Common.Messages.Web.ToController; [MemoryPackable(GenerateType.VersionTolerant)] public sealed partial record CreateUserMessage( - [property: MemoryPackOrder(0)] Guid LoggedInUserGuid, + [property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken, [property: MemoryPackOrder(1)] string Username, [property: MemoryPackOrder(2)] string Password -) : IMessageToController, ICanReply<CreateUserResult>; +) : IMessageToController, ICanReply<Result<CreateUserResult, UserActionFailure>>; diff --git a/Common/Phantom.Common.Messages.Web/ToController/DeleteUserMessage.cs b/Common/Phantom.Common.Messages.Web/ToController/DeleteUserMessage.cs index 378904b..bc6bfca 100644 --- a/Common/Phantom.Common.Messages.Web/ToController/DeleteUserMessage.cs +++ b/Common/Phantom.Common.Messages.Web/ToController/DeleteUserMessage.cs @@ -1,4 +1,6 @@ -using MemoryPack; +using System.Collections.Immutable; +using MemoryPack; +using Phantom.Common.Data; using Phantom.Common.Data.Web.Users; using Phantom.Utils.Actor; @@ -6,6 +8,6 @@ namespace Phantom.Common.Messages.Web.ToController; [MemoryPackable(GenerateType.VersionTolerant)] public sealed partial record DeleteUserMessage( - [property: MemoryPackOrder(0)] Guid LoggedInUserGuid, + [property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken, [property: MemoryPackOrder(1)] Guid SubjectUserGuid -) : IMessageToController, ICanReply<DeleteUserResult>; +) : IMessageToController, ICanReply<Result<DeleteUserResult, UserActionFailure>>; diff --git a/Common/Phantom.Common.Messages.Web/ToController/GetAuditLogMessage.cs b/Common/Phantom.Common.Messages.Web/ToController/GetAuditLogMessage.cs index 2e09594..6135740 100644 --- a/Common/Phantom.Common.Messages.Web/ToController/GetAuditLogMessage.cs +++ b/Common/Phantom.Common.Messages.Web/ToController/GetAuditLogMessage.cs @@ -1,11 +1,14 @@ using System.Collections.Immutable; using MemoryPack; +using Phantom.Common.Data; using Phantom.Common.Data.Web.AuditLog; +using Phantom.Common.Data.Web.Users; using Phantom.Utils.Actor; namespace Phantom.Common.Messages.Web.ToController; [MemoryPackable(GenerateType.VersionTolerant)] public sealed partial record GetAuditLogMessage( - [property: MemoryPackOrder(0)] int Count -) : IMessageToController, ICanReply<ImmutableArray<AuditLogItem>>; + [property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken, + [property: MemoryPackOrder(1)] int Count +) : IMessageToController, ICanReply<Result<ImmutableArray<AuditLogItem>, UserActionFailure>>; diff --git a/Common/Phantom.Common.Messages.Web/ToController/GetAuthenticatedUser.cs b/Common/Phantom.Common.Messages.Web/ToController/GetAuthenticatedUser.cs index 27ccec9..e68fee0 100644 --- a/Common/Phantom.Common.Messages.Web/ToController/GetAuthenticatedUser.cs +++ b/Common/Phantom.Common.Messages.Web/ToController/GetAuthenticatedUser.cs @@ -9,5 +9,5 @@ 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 + [property: MemoryPackOrder(1)] ImmutableArray<byte> AuthToken ) : IMessageToController, ICanReply<Optional<AuthenticatedUserInfo>>; diff --git a/Common/Phantom.Common.Messages.Web/ToController/GetEventLogMessage.cs b/Common/Phantom.Common.Messages.Web/ToController/GetEventLogMessage.cs index 58a1dbb..8f50f48 100644 --- a/Common/Phantom.Common.Messages.Web/ToController/GetEventLogMessage.cs +++ b/Common/Phantom.Common.Messages.Web/ToController/GetEventLogMessage.cs @@ -1,11 +1,14 @@ using System.Collections.Immutable; using MemoryPack; +using Phantom.Common.Data; using Phantom.Common.Data.Web.EventLog; +using Phantom.Common.Data.Web.Users; using Phantom.Utils.Actor; namespace Phantom.Common.Messages.Web.ToController; [MemoryPackable(GenerateType.VersionTolerant)] public sealed partial record GetEventLogMessage( - [property: MemoryPackOrder(0)] int Count -) : IMessageToController, ICanReply<ImmutableArray<EventLogItem>>; + [property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken, + [property: MemoryPackOrder(1)] int Count +) : IMessageToController, ICanReply<Result<ImmutableArray<EventLogItem>, UserActionFailure>>; diff --git a/Common/Phantom.Common.Messages.Web/ToController/LaunchInstanceMessage.cs b/Common/Phantom.Common.Messages.Web/ToController/LaunchInstanceMessage.cs index eebcdfa..aa706a6 100644 --- a/Common/Phantom.Common.Messages.Web/ToController/LaunchInstanceMessage.cs +++ b/Common/Phantom.Common.Messages.Web/ToController/LaunchInstanceMessage.cs @@ -1,13 +1,15 @@ -using MemoryPack; +using System.Collections.Immutable; +using MemoryPack; using Phantom.Common.Data; using Phantom.Common.Data.Replies; +using Phantom.Common.Data.Web.Users; using Phantom.Utils.Actor; namespace Phantom.Common.Messages.Web.ToController; [MemoryPackable(GenerateType.VersionTolerant)] public sealed partial record LaunchInstanceMessage( - [property: MemoryPackOrder(0)] Guid LoggedInUserGuid, + [property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken, [property: MemoryPackOrder(1)] Guid AgentGuid, [property: MemoryPackOrder(2)] Guid InstanceGuid -) : IMessageToController, ICanReply<Result<LaunchInstanceResult, InstanceActionFailure>>; +) : IMessageToController, ICanReply<Result<LaunchInstanceResult, UserInstanceActionFailure>>; diff --git a/Common/Phantom.Common.Messages.Web/ToController/SendCommandToInstanceMessage.cs b/Common/Phantom.Common.Messages.Web/ToController/SendCommandToInstanceMessage.cs index 485cc6f..800a405 100644 --- a/Common/Phantom.Common.Messages.Web/ToController/SendCommandToInstanceMessage.cs +++ b/Common/Phantom.Common.Messages.Web/ToController/SendCommandToInstanceMessage.cs @@ -1,14 +1,16 @@ -using MemoryPack; +using System.Collections.Immutable; +using MemoryPack; using Phantom.Common.Data; using Phantom.Common.Data.Replies; +using Phantom.Common.Data.Web.Users; using Phantom.Utils.Actor; namespace Phantom.Common.Messages.Web.ToController; [MemoryPackable(GenerateType.VersionTolerant)] public sealed partial record SendCommandToInstanceMessage( - [property: MemoryPackOrder(0)] Guid LoggedInUserGuid, + [property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken, [property: MemoryPackOrder(1)] Guid AgentGuid, [property: MemoryPackOrder(2)] Guid InstanceGuid, [property: MemoryPackOrder(3)] string Command -) : IMessageToController, ICanReply<Result<SendCommandToInstanceResult, InstanceActionFailure>>; +) : IMessageToController, ICanReply<Result<SendCommandToInstanceResult, UserInstanceActionFailure>>; diff --git a/Common/Phantom.Common.Messages.Web/ToController/StopInstanceMessage.cs b/Common/Phantom.Common.Messages.Web/ToController/StopInstanceMessage.cs index 1f0a94a..2ec3a52 100644 --- a/Common/Phantom.Common.Messages.Web/ToController/StopInstanceMessage.cs +++ b/Common/Phantom.Common.Messages.Web/ToController/StopInstanceMessage.cs @@ -1,15 +1,17 @@ -using MemoryPack; +using System.Collections.Immutable; +using MemoryPack; using Phantom.Common.Data; using Phantom.Common.Data.Minecraft; using Phantom.Common.Data.Replies; +using Phantom.Common.Data.Web.Users; using Phantom.Utils.Actor; namespace Phantom.Common.Messages.Web.ToController; [MemoryPackable(GenerateType.VersionTolerant)] public sealed partial record StopInstanceMessage( - [property: MemoryPackOrder(0)] Guid LoggedInUserGuid, + [property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken, [property: MemoryPackOrder(1)] Guid AgentGuid, [property: MemoryPackOrder(2)] Guid InstanceGuid, [property: MemoryPackOrder(3)] MinecraftStopStrategy StopStrategy -) : IMessageToController, ICanReply<Result<StopInstanceResult, InstanceActionFailure>>; +) : IMessageToController, ICanReply<Result<StopInstanceResult, UserInstanceActionFailure>>; diff --git a/Common/Phantom.Common.Messages.Web/WebMessageRegistries.cs b/Common/Phantom.Common.Messages.Web/WebMessageRegistries.cs index ba0b45c..b154994 100644 --- a/Common/Phantom.Common.Messages.Web/WebMessageRegistries.cs +++ b/Common/Phantom.Common.Messages.Web/WebMessageRegistries.cs @@ -28,20 +28,20 @@ public static class WebMessageRegistries { ToController.Add<LogOutMessage>(3); ToController.Add<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(4); ToController.Add<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(5); - ToController.Add<CreateUserMessage, CreateUserResult>(6); - ToController.Add<DeleteUserMessage, DeleteUserResult>(7); + ToController.Add<CreateUserMessage, Result<CreateUserResult, UserActionFailure>>(6); + ToController.Add<DeleteUserMessage, Result<DeleteUserResult, UserActionFailure>>(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, Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(12); - ToController.Add<LaunchInstanceMessage, Result<LaunchInstanceResult, InstanceActionFailure>>(13); - ToController.Add<StopInstanceMessage, Result<StopInstanceResult, InstanceActionFailure>>(14); - ToController.Add<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, InstanceActionFailure>>(15); + ToController.Add<ChangeUserRolesMessage, Result<ChangeUserRolesResult, UserActionFailure>>(11); + ToController.Add<CreateOrUpdateInstanceMessage, Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>(12); + ToController.Add<LaunchInstanceMessage, Result<LaunchInstanceResult, UserInstanceActionFailure>>(13); + ToController.Add<StopInstanceMessage, Result<StopInstanceResult, UserInstanceActionFailure>>(14); + ToController.Add<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, UserInstanceActionFailure>>(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<GetAuditLogMessage, Result<ImmutableArray<AuditLogItem>, UserActionFailure>>(18); + ToController.Add<GetEventLogMessage, Result<ImmutableArray<EventLogItem>, UserActionFailure>>(19); ToController.Add<ReplyMessage>(127); ToWeb.Add<RegisterWebResultMessage>(0); diff --git a/Controller/Phantom.Controller.Database/Repositories/PermissionRepository.cs b/Controller/Phantom.Controller.Database/Repositories/PermissionRepository.cs new file mode 100644 index 0000000..463f755 --- /dev/null +++ b/Controller/Phantom.Controller.Database/Repositories/PermissionRepository.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore; +using Phantom.Common.Data.Web.Users; +using Phantom.Controller.Database.Entities; +using Phantom.Utils.Collections; + +namespace Phantom.Controller.Database.Repositories; + +public sealed class PermissionRepository { + private readonly ILazyDbContext db; + + public PermissionRepository(ILazyDbContext db) { + this.db = db; + } + + public async Task<PermissionSet> GetAllUserPermissions(UserEntity user) { + var userPermissions = db.Ctx.UserPermissions + .Where(up => up.UserGuid == user.UserGuid) + .Select(static up => up.PermissionId); + + var rolePermissions = db.Ctx.UserRoles + .Where(ur => ur.UserGuid == user.UserGuid) + .Join(db.Ctx.RolePermissions, static ur => ur.RoleGuid, static rp => rp.RoleGuid, static (ur, rp) => rp.PermissionId); + + return new PermissionSet(await userPermissions.Union(rolePermissions).AsAsyncEnumerable().ToImmutableSetAsync()); + } +} diff --git a/Controller/Phantom.Controller.Services/Agents/AgentActor.cs b/Controller/Phantom.Controller.Services/Agents/AgentActor.cs index 2bac9ad..6dd6760 100644 --- a/Controller/Phantom.Controller.Services/Agents/AgentActor.cs +++ b/Controller/Phantom.Controller.Services/Agents/AgentActor.cs @@ -10,12 +10,14 @@ using Phantom.Common.Data.Replies; using Phantom.Common.Data.Web.Agent; using Phantom.Common.Data.Web.Instance; using Phantom.Common.Data.Web.Minecraft; +using Phantom.Common.Data.Web.Users; using Phantom.Common.Messages.Agent; using Phantom.Common.Messages.Agent.ToAgent; using Phantom.Controller.Database; using Phantom.Controller.Database.Entities; using Phantom.Controller.Minecraft; using Phantom.Controller.Services.Instances; +using Phantom.Controller.Services.Users.Sessions; using Phantom.Utils.Actor; using Phantom.Utils.Actor.Mailbox; using Phantom.Utils.Actor.Tasks; @@ -32,7 +34,7 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> { private static readonly TimeSpan DisconnectionRecheckInterval = TimeSpan.FromSeconds(5); private static readonly TimeSpan DisconnectionThreshold = TimeSpan.FromSeconds(12); - public readonly record struct Init(Guid AgentGuid, AgentConfiguration AgentConfiguration, ControllerState ControllerState, MinecraftVersions MinecraftVersions, IDbContextProvider DbProvider, CancellationToken CancellationToken); + public readonly record struct Init(Guid AgentGuid, AgentConfiguration AgentConfiguration, ControllerState ControllerState, MinecraftVersions MinecraftVersions, UserLoginManager UserLoginManager, IDbContextProvider DbProvider, CancellationToken CancellationToken); public static Props<ICommand> Factory(Init init) { return Props<ICommand>.Create(() => new AgentActor(init), new ActorConfiguration { SupervisorStrategy = SupervisorStrategies.Resume, MailboxType = UnboundedJumpAheadMailbox.Name }); @@ -40,6 +42,7 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> { private readonly ControllerState controllerState; private readonly MinecraftVersions minecraftVersions; + private readonly UserLoginManager userLoginManager; private readonly IDbContextProvider dbProvider; private readonly CancellationToken cancellationToken; @@ -76,6 +79,7 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> { private AgentActor(Init init) { this.controllerState = init.ControllerState; this.minecraftVersions = init.MinecraftVersions; + this.userLoginManager = init.UserLoginManager; this.dbProvider = init.DbProvider; this.cancellationToken = init.CancellationToken; @@ -94,11 +98,11 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> { Receive<NotifyIsAliveCommand>(NotifyIsAlive); Receive<UpdateStatsCommand>(UpdateStats); Receive<UpdateJavaRuntimesCommand>(UpdateJavaRuntimes); - ReceiveAndReplyLater<CreateOrUpdateInstanceCommand, Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(CreateOrUpdateInstance); + ReceiveAndReplyLater<CreateOrUpdateInstanceCommand, Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>(CreateOrUpdateInstance); Receive<UpdateInstanceStatusCommand>(UpdateInstanceStatus); - ReceiveAndReplyLater<LaunchInstanceCommand, Result<LaunchInstanceResult, InstanceActionFailure>>(LaunchInstance); - ReceiveAndReplyLater<StopInstanceCommand, Result<StopInstanceResult, InstanceActionFailure>>(StopInstance); - ReceiveAndReplyLater<SendCommandToInstanceCommand, Result<SendCommandToInstanceResult, InstanceActionFailure>>(SendMinecraftCommand); + ReceiveAndReplyLater<LaunchInstanceCommand, Result<LaunchInstanceResult, UserInstanceActionFailure>>(LaunchInstance); + ReceiveAndReplyLater<StopInstanceCommand, Result<StopInstanceResult, UserInstanceActionFailure>>(StopInstance); + ReceiveAndReplyLater<SendCommandToInstanceCommand, Result<SendCommandToInstanceResult, UserInstanceActionFailure>>(SendMinecraftCommand); Receive<ReceiveInstanceDataCommand>(ReceiveInstanceData); } @@ -146,13 +150,21 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> { } } - private Task<Result<TReply, InstanceActionFailure>> RequestInstance<TCommand, TReply>(Guid instanceGuid, TCommand command) where TCommand : InstanceActor.ICommand, ICanReply<Result<TReply, InstanceActionFailure>> { + private async Task<Result<TReply, UserInstanceActionFailure>> RequestInstance<TCommand, TReply>(ImmutableArray<byte> authToken, Guid instanceGuid, Func<Guid, TCommand> commandFactoryFromLoggedInUserGuid) where TCommand : InstanceActor.ICommand, ICanReply<Result<TReply, InstanceActionFailure>> { + var loggedInUser = userLoginManager.GetLoggedInUser(authToken); + if (!loggedInUser.CheckPermission(Permission.ControlInstances)) { + return (UserInstanceActionFailure) UserActionFailure.NotAuthorized; + } + + var command = commandFactoryFromLoggedInUserGuid(loggedInUser.Guid!.Value); + if (instanceActorByGuid.TryGetValue(instanceGuid, out var instance)) { - return instance.Request(command, cancellationToken); + var result = await instance.Request(command, cancellationToken); + return result.MapError(static error => (UserInstanceActionFailure) error); } else { Logger.Warning("Could not deliver command {CommandType} to instance {InstanceGuid}, instance not found.", command.GetType().Name, instanceGuid); - return Task.FromResult<Result<TReply, InstanceActionFailure>>(InstanceActionFailure.InstanceDoesNotExist); + return (UserInstanceActionFailure) InstanceActionFailure.InstanceDoesNotExist; } } @@ -183,15 +195,15 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> { public sealed record UpdateJavaRuntimesCommand(ImmutableArray<TaggedJavaRuntime> JavaRuntimes) : ICommand; - public sealed record CreateOrUpdateInstanceCommand(Guid AuditLogUserGuid, Guid InstanceGuid, InstanceConfiguration Configuration) : ICommand, ICanReply<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>; + public sealed record CreateOrUpdateInstanceCommand(ImmutableArray<byte> AuthToken, Guid InstanceGuid, InstanceConfiguration Configuration) : ICommand, ICanReply<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>; public sealed record UpdateInstanceStatusCommand(Guid InstanceGuid, IInstanceStatus Status) : ICommand; - public sealed record LaunchInstanceCommand(Guid InstanceGuid, Guid AuditLogUserGuid) : ICommand, ICanReply<Result<LaunchInstanceResult, InstanceActionFailure>>; + public sealed record LaunchInstanceCommand(ImmutableArray<byte> AuthToken, Guid InstanceGuid) : ICommand, ICanReply<Result<LaunchInstanceResult, UserInstanceActionFailure>>; - public sealed record StopInstanceCommand(Guid InstanceGuid, Guid AuditLogUserGuid, MinecraftStopStrategy StopStrategy) : ICommand, ICanReply<Result<StopInstanceResult, InstanceActionFailure>>; + public sealed record StopInstanceCommand(ImmutableArray<byte> AuthToken, Guid InstanceGuid, MinecraftStopStrategy StopStrategy) : ICommand, ICanReply<Result<StopInstanceResult, UserInstanceActionFailure>>; - public sealed record SendCommandToInstanceCommand(Guid InstanceGuid, Guid AuditLogUserGuid, string Command) : ICommand, ICanReply<Result<SendCommandToInstanceResult, InstanceActionFailure>>; + public sealed record SendCommandToInstanceCommand(ImmutableArray<byte> AuthToken, Guid InstanceGuid, string Command) : ICommand, ICanReply<Result<SendCommandToInstanceResult, UserInstanceActionFailure>>; public sealed record ReceiveInstanceDataCommand(Instance Instance) : ICommand, IJumpAhead; @@ -280,25 +292,30 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> { controllerState.UpdateAgentJavaRuntimes(agentGuid, javaRuntimes); } - private Task<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>> CreateOrUpdateInstance(CreateOrUpdateInstanceCommand command) { + private Task<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>> CreateOrUpdateInstance(CreateOrUpdateInstanceCommand command) { + var loggedInUser = userLoginManager.GetLoggedInUser(command.AuthToken); + if (!loggedInUser.CheckPermission(Permission.CreateInstances)) { + return Task.FromResult<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>((UserInstanceActionFailure) UserActionFailure.NotAuthorized); + } + var instanceConfiguration = command.Configuration; if (string.IsNullOrWhiteSpace(instanceConfiguration.InstanceName)) { - return Task.FromResult<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(CreateOrUpdateInstanceResult.InstanceNameMustNotBeEmpty); + return Task.FromResult<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>(CreateOrUpdateInstanceResult.InstanceNameMustNotBeEmpty); } if (instanceConfiguration.MemoryAllocation <= RamAllocationUnits.Zero) { - return Task.FromResult<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(CreateOrUpdateInstanceResult.InstanceMemoryMustNotBeZero); + return Task.FromResult<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>(CreateOrUpdateInstanceResult.InstanceMemoryMustNotBeZero); } return minecraftVersions.GetServerExecutableInfo(instanceConfiguration.MinecraftVersion, cancellationToken) - .ContinueOnActor(CreateOrUpdateInstance1, command) + .ContinueOnActor(CreateOrUpdateInstance1, loggedInUser.Guid!.Value, command) .Unwrap(); } - private Task<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>> CreateOrUpdateInstance1(FileDownloadInfo? serverExecutableInfo, CreateOrUpdateInstanceCommand command) { + private Task<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>> CreateOrUpdateInstance1(FileDownloadInfo? serverExecutableInfo, Guid loggedInUserGuid, CreateOrUpdateInstanceCommand command) { if (serverExecutableInfo == null) { - return Task.FromResult<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(CreateOrUpdateInstanceResult.MinecraftVersionDownloadInfoNotFound); + return Task.FromResult<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>(CreateOrUpdateInstanceResult.MinecraftVersionDownloadInfoNotFound); } var instanceConfiguration = command.Configuration; @@ -308,13 +325,13 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> { instanceActorRef = CreateNewInstance(Instance.Offline(command.InstanceGuid, instanceConfiguration)); } - var configureInstanceCommand = new InstanceActor.ConfigureInstanceCommand(command.AuditLogUserGuid, command.InstanceGuid, instanceConfiguration, new InstanceLaunchProperties(serverExecutableInfo), isCreatingInstance); + var configureInstanceCommand = new InstanceActor.ConfigureInstanceCommand(loggedInUserGuid, command.InstanceGuid, instanceConfiguration, new InstanceLaunchProperties(serverExecutableInfo), isCreatingInstance); return instanceActorRef.Request(configureInstanceCommand, cancellationToken) .ContinueOnActor(CreateOrUpdateInstance2, configureInstanceCommand); } - private Result<CreateOrUpdateInstanceResult, InstanceActionFailure> CreateOrUpdateInstance2(Result<ConfigureInstanceResult, InstanceActionFailure> result, InstanceActor.ConfigureInstanceCommand command) { + private Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure> CreateOrUpdateInstance2(Result<ConfigureInstanceResult, InstanceActionFailure> result, InstanceActor.ConfigureInstanceCommand command) { var instanceGuid = command.InstanceGuid; var instanceName = command.Configuration.InstanceName; var isCreating = command.IsCreatingInstance; @@ -330,7 +347,7 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> { else { string action = isCreating ? "adding" : "editing"; string relation = isCreating ? "to agent" : "in agent"; - string reason = result.Map(ConfigureInstanceResultExtensions.ToSentence, InstanceActionFailureExtensions.ToSentence); + string reason = result.Into(ConfigureInstanceResultExtensions.ToSentence, InstanceActionFailureExtensions.ToSentence); Logger.Information("Failed " + action + " instance \"{InstanceName}\" (GUID {InstanceGuid}) " + relation + " \"{AgentName}\". {ErrorMessage}", instanceName, instanceGuid, configuration.AgentName, reason); @@ -342,16 +359,16 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> { TellInstance(command.InstanceGuid, new InstanceActor.SetStatusCommand(command.Status)); } - private Task<Result<LaunchInstanceResult, InstanceActionFailure>> LaunchInstance(LaunchInstanceCommand command) { - return RequestInstance<InstanceActor.LaunchInstanceCommand, LaunchInstanceResult>(command.InstanceGuid, new InstanceActor.LaunchInstanceCommand(command.AuditLogUserGuid)); + private Task<Result<LaunchInstanceResult, UserInstanceActionFailure>> LaunchInstance(LaunchInstanceCommand command) { + return RequestInstance<InstanceActor.LaunchInstanceCommand, LaunchInstanceResult>(command.AuthToken, command.InstanceGuid, static loggedInUserGuid => new InstanceActor.LaunchInstanceCommand(loggedInUserGuid)); } - private Task<Result<StopInstanceResult, InstanceActionFailure>> StopInstance(StopInstanceCommand command) { - return RequestInstance<InstanceActor.StopInstanceCommand, StopInstanceResult>(command.InstanceGuid, new InstanceActor.StopInstanceCommand(command.AuditLogUserGuid, command.StopStrategy)); + private Task<Result<StopInstanceResult, UserInstanceActionFailure>> StopInstance(StopInstanceCommand command) { + return RequestInstance<InstanceActor.StopInstanceCommand, StopInstanceResult>(command.AuthToken, command.InstanceGuid, loggedInUserGuid => new InstanceActor.StopInstanceCommand(loggedInUserGuid, command.StopStrategy)); } - private Task<Result<SendCommandToInstanceResult, InstanceActionFailure>> SendMinecraftCommand(SendCommandToInstanceCommand command) { - return RequestInstance<InstanceActor.SendCommandToInstanceCommand, SendCommandToInstanceResult>(command.InstanceGuid, new InstanceActor.SendCommandToInstanceCommand(command.AuditLogUserGuid, command.Command)); + private Task<Result<SendCommandToInstanceResult, UserInstanceActionFailure>> SendMinecraftCommand(SendCommandToInstanceCommand command) { + return RequestInstance<InstanceActor.SendCommandToInstanceCommand, SendCommandToInstanceResult>(command.AuthToken, command.InstanceGuid, loggedInUserGuid => new InstanceActor.SendCommandToInstanceCommand(loggedInUserGuid, command.Command)); } private void ReceiveInstanceData(ReceiveInstanceDataCommand command) { diff --git a/Controller/Phantom.Controller.Services/Agents/AgentManager.cs b/Controller/Phantom.Controller.Services/Agents/AgentManager.cs index 874f6bf..168d32f 100644 --- a/Controller/Phantom.Controller.Services/Agents/AgentManager.cs +++ b/Controller/Phantom.Controller.Services/Agents/AgentManager.cs @@ -4,10 +4,12 @@ using Phantom.Common.Data; using Phantom.Common.Data.Agent; using Phantom.Common.Data.Replies; using Phantom.Common.Data.Web.Agent; +using Phantom.Common.Data.Web.Users; using Phantom.Common.Messages.Agent; using Phantom.Common.Messages.Agent.ToAgent; using Phantom.Controller.Database; using Phantom.Controller.Minecraft; +using Phantom.Controller.Services.Users.Sessions; using Phantom.Utils.Actor; using Phantom.Utils.Logging; using Phantom.Utils.Rpc.Runtime; @@ -22,17 +24,19 @@ sealed class AgentManager { private readonly AuthToken authToken; private readonly ControllerState controllerState; private readonly MinecraftVersions minecraftVersions; + private readonly UserLoginManager userLoginManager; private readonly IDbContextProvider dbProvider; private readonly CancellationToken cancellationToken; private readonly ConcurrentDictionary<Guid, ActorRef<AgentActor.ICommand>> agentsByGuid = new (); private readonly Func<Guid, AgentConfiguration, ActorRef<AgentActor.ICommand>> addAgentActorFactory; - public AgentManager(IActorRefFactory actorSystem, AuthToken authToken, ControllerState controllerState, MinecraftVersions minecraftVersions, IDbContextProvider dbProvider, CancellationToken cancellationToken) { + public AgentManager(IActorRefFactory actorSystem, AuthToken authToken, ControllerState controllerState, MinecraftVersions minecraftVersions, UserLoginManager userLoginManager, IDbContextProvider dbProvider, CancellationToken cancellationToken) { this.actorSystem = actorSystem; this.authToken = authToken; this.controllerState = controllerState; this.minecraftVersions = minecraftVersions; + this.userLoginManager = userLoginManager; this.dbProvider = dbProvider; this.cancellationToken = cancellationToken; @@ -40,7 +44,7 @@ sealed class AgentManager { } private ActorRef<AgentActor.ICommand> CreateAgentActor(Guid agentGuid, AgentConfiguration agentConfiguration) { - var init = new AgentActor.Init(agentGuid, agentConfiguration, controllerState, minecraftVersions, dbProvider, cancellationToken); + var init = new AgentActor.Init(agentGuid, agentConfiguration, controllerState, minecraftVersions, userLoginManager, dbProvider, cancellationToken); var name = "Agent:" + agentGuid; return actorSystem.ActorOf(AgentActor.Factory(init), name); } @@ -83,7 +87,7 @@ sealed class AgentManager { } } - public async Task<Result<TReply, InstanceActionFailure>> DoInstanceAction<TCommand, TReply>(Guid agentGuid, TCommand command) where TCommand : class, AgentActor.ICommand, ICanReply<Result<TReply, InstanceActionFailure>> { - return agentsByGuid.TryGetValue(agentGuid, out var agent) ? await agent.Request(command, cancellationToken) : InstanceActionFailure.AgentDoesNotExist; + public async Task<Result<TReply, UserInstanceActionFailure>> DoInstanceAction<TCommand, TReply>(Guid agentGuid, TCommand command) where TCommand : class, AgentActor.ICommand, ICanReply<Result<TReply, UserInstanceActionFailure>> { + return agentsByGuid.TryGetValue(agentGuid, out var agent) ? await agent.Request(command, cancellationToken) : (UserInstanceActionFailure) InstanceActionFailure.AgentDoesNotExist; } } diff --git a/Controller/Phantom.Controller.Services/ControllerServices.cs b/Controller/Phantom.Controller.Services/ControllerServices.cs index 6b70bb9..bc21293 100644 --- a/Controller/Phantom.Controller.Services/ControllerServices.cs +++ b/Controller/Phantom.Controller.Services/ControllerServices.cs @@ -11,6 +11,7 @@ using Phantom.Controller.Services.Events; using Phantom.Controller.Services.Instances; using Phantom.Controller.Services.Rpc; using Phantom.Controller.Services.Users; +using Phantom.Controller.Services.Users.Sessions; using Phantom.Utils.Actor; using Phantom.Utils.Rpc.Runtime; using IMessageFromAgentToController = Phantom.Common.Messages.Agent.IMessageToController; @@ -24,17 +25,18 @@ public sealed class ControllerServices : IDisposable { private ControllerState ControllerState { get; } private MinecraftVersions MinecraftVersions { get; } - private AgentManager AgentManager { get; } - private InstanceLogManager InstanceLogManager { get; } - private EventLogManager EventLogManager { get; } - + private AuthenticatedUserCache AuthenticatedUserCache { get; } private UserManager UserManager { get; } private RoleManager RoleManager { get; } - private PermissionManager PermissionManager { get; } - private UserRoleManager UserRoleManager { get; } private UserLoginManager UserLoginManager { get; } + private PermissionManager PermissionManager { get; } + + private AgentManager AgentManager { get; } + private InstanceLogManager InstanceLogManager { get; } + private AuditLogManager AuditLogManager { get; } + private EventLogManager EventLogManager { get; } public IRegistrationHandler<IMessageToAgent, IMessageFromAgentToController, RegisterAgentMessage> AgentRegistrationHandler { get; } public IRegistrationHandler<IMessageToWeb, IMessageFromWebToController, RegisterWebMessage> WebRegistrationHandler { get; } @@ -51,15 +53,16 @@ public sealed class ControllerServices : IDisposable { this.ControllerState = new ControllerState(); this.MinecraftVersions = new MinecraftVersions(); - this.AgentManager = new AgentManager(ActorSystem, agentAuthToken, ControllerState, MinecraftVersions, dbProvider, cancellationToken); + this.AuthenticatedUserCache = new AuthenticatedUserCache(); + this.UserManager = new UserManager(AuthenticatedUserCache, dbProvider); + this.RoleManager = new RoleManager(dbProvider); + this.UserRoleManager = new UserRoleManager(AuthenticatedUserCache, dbProvider); + this.UserLoginManager = new UserLoginManager(AuthenticatedUserCache, UserManager, dbProvider); + this.PermissionManager = new PermissionManager(dbProvider); + + this.AgentManager = new AgentManager(ActorSystem, agentAuthToken, ControllerState, MinecraftVersions, UserLoginManager, dbProvider, cancellationToken); this.InstanceLogManager = new InstanceLogManager(); - this.UserManager = new UserManager(dbProvider); - this.RoleManager = new RoleManager(dbProvider); - this.PermissionManager = new PermissionManager(dbProvider); - - this.UserRoleManager = new UserRoleManager(dbProvider); - 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/Events/EventLogManager.cs b/Controller/Phantom.Controller.Services/Events/EventLogManager.cs index 7bad97e..2e129e9 100644 --- a/Controller/Phantom.Controller.Services/Events/EventLogManager.cs +++ b/Controller/Phantom.Controller.Services/Events/EventLogManager.cs @@ -1,8 +1,11 @@ using System.Collections.Immutable; using Akka.Actor; +using Phantom.Common.Data; using Phantom.Common.Data.Web.EventLog; +using Phantom.Common.Data.Web.Users; using Phantom.Controller.Database; using Phantom.Controller.Database.Repositories; +using Phantom.Controller.Services.Users.Sessions; using Phantom.Utils.Actor; namespace Phantom.Controller.Services.Events; @@ -22,7 +25,11 @@ sealed partial class EventLogManager { databaseStorageActor.Tell(new EventLogDatabaseStorageActor.StoreEventCommand(eventGuid, utcTime, agentGuid, eventType, subjectId, extra)); } - public async Task<ImmutableArray<EventLogItem>> GetMostRecentItems(int count) { + public async Task<Result<ImmutableArray<EventLogItem>, UserActionFailure>> GetMostRecentItems(LoggedInUser loggedInUser, int count) { + if (!loggedInUser.CheckPermission(Permission.ViewEvents)) { + return UserActionFailure.NotAuthorized; + } + await using var db = dbProvider.Lazy(); return await new EventLogRepository(db).GetMostRecentItems(count, cancellationToken); } diff --git a/Controller/Phantom.Controller.Services/Rpc/WebMessageHandlerActor.cs b/Controller/Phantom.Controller.Services/Rpc/WebMessageHandlerActor.cs index 0ce7093..29d2cd8 100644 --- a/Controller/Phantom.Controller.Services/Rpc/WebMessageHandlerActor.cs +++ b/Controller/Phantom.Controller.Services/Rpc/WebMessageHandlerActor.cs @@ -15,6 +15,7 @@ using Phantom.Controller.Services.Agents; using Phantom.Controller.Services.Events; using Phantom.Controller.Services.Instances; using Phantom.Controller.Services.Users; +using Phantom.Controller.Services.Users.Sessions; using Phantom.Utils.Actor; using Phantom.Utils.Rpc.Runtime; @@ -67,27 +68,27 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> { var senderActorInit = new WebMessageDataUpdateSenderActor.Init(connection, controllerState, init.InstanceLogManager); Context.ActorOf(WebMessageDataUpdateSenderActor.Factory(senderActorInit), "DataUpdateSender"); - + ReceiveAsync<RegisterWebMessage>(HandleRegisterWeb); Receive<UnregisterWebMessage>(HandleUnregisterWeb); ReceiveAndReplyLater<LogInMessage, LogInSuccess?>(HandleLogIn); Receive<LogOutMessage>(HandleLogOut); ReceiveAndReply<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(GetAuthenticatedUser); ReceiveAndReplyLater<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(HandleCreateOrUpdateAdministratorUser); - ReceiveAndReplyLater<CreateUserMessage, CreateUserResult>(HandleCreateUser); + ReceiveAndReplyLater<CreateUserMessage, Result<CreateUserResult, UserActionFailure>>(HandleCreateUser); ReceiveAndReplyLater<GetUsersMessage, ImmutableArray<UserInfo>>(HandleGetUsers); ReceiveAndReplyLater<GetRolesMessage, ImmutableArray<RoleInfo>>(HandleGetRoles); ReceiveAndReplyLater<GetUserRolesMessage, ImmutableDictionary<Guid, ImmutableArray<Guid>>>(HandleGetUserRoles); - ReceiveAndReplyLater<ChangeUserRolesMessage, ChangeUserRolesResult>(HandleChangeUserRoles); - ReceiveAndReplyLater<DeleteUserMessage, DeleteUserResult>(HandleDeleteUser); - ReceiveAndReplyLater<CreateOrUpdateInstanceMessage, Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(HandleCreateOrUpdateInstance); - ReceiveAndReplyLater<LaunchInstanceMessage, Result<LaunchInstanceResult, InstanceActionFailure>>(HandleLaunchInstance); - ReceiveAndReplyLater<StopInstanceMessage, Result<StopInstanceResult, InstanceActionFailure>>(HandleStopInstance); - ReceiveAndReplyLater<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, InstanceActionFailure>>(HandleSendCommandToInstance); - ReceiveAndReplyLater<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(HandleGetMinecraftVersions); + ReceiveAndReplyLater<ChangeUserRolesMessage, Result<ChangeUserRolesResult, UserActionFailure>>(HandleChangeUserRoles); + ReceiveAndReplyLater<DeleteUserMessage, Result<DeleteUserResult, UserActionFailure>>(HandleDeleteUser); + ReceiveAndReplyLater<CreateOrUpdateInstanceMessage, Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>(HandleCreateOrUpdateInstance); + ReceiveAndReplyLater<LaunchInstanceMessage, Result<LaunchInstanceResult, UserInstanceActionFailure>>(HandleLaunchInstance); + ReceiveAndReplyLater<StopInstanceMessage, Result<StopInstanceResult, UserInstanceActionFailure>>(HandleStopInstance); + ReceiveAndReplyLater<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, UserInstanceActionFailure>>(HandleSendCommandToInstance); + ReceiveAndReplyLater<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(HandleGetMinecraftVersions); ReceiveAndReply<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(HandleGetAgentJavaRuntimes); - ReceiveAndReplyLater<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(HandleGetAuditLog); - ReceiveAndReplyLater<GetEventLogMessage, ImmutableArray<EventLogItem>>(HandleGetEventLog); + ReceiveAndReplyLater<GetAuditLogMessage, Result<ImmutableArray<AuditLogItem>, UserActionFailure>>(HandleGetAuditLog); + ReceiveAndReplyLater<GetEventLogMessage, Result<ImmutableArray<EventLogItem>, UserActionFailure>>(HandleGetEventLog); Receive<ReplyMessage>(HandleReply); } @@ -108,15 +109,15 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> { } private Optional<AuthenticatedUserInfo> GetAuthenticatedUser(GetAuthenticatedUser message) { - return userLoginManager.GetAuthenticatedUser(message.UserGuid, message.SessionToken); + return userLoginManager.GetAuthenticatedUser(message.UserGuid, message.AuthToken); } - + private Task<CreateOrUpdateAdministratorUserResult> HandleCreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUserMessage message) { return userManager.CreateOrUpdateAdministrator(message.Username, message.Password); } - private Task<CreateUserResult> HandleCreateUser(CreateUserMessage message) { - return userManager.Create(message.LoggedInUserGuid, message.Username, message.Password); + private Task<Result<CreateUserResult, UserActionFailure>> HandleCreateUser(CreateUserMessage message) { + return userManager.Create(userLoginManager.GetLoggedInUser(message.AuthToken), message.Username, message.Password); } private Task<ImmutableArray<UserInfo>> HandleGetUsers(GetUsersMessage message) { @@ -131,28 +132,28 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> { return userRoleManager.GetUserRoles(message.UserGuids); } - private Task<ChangeUserRolesResult> HandleChangeUserRoles(ChangeUserRolesMessage message) { - return userRoleManager.ChangeUserRoles(message.LoggedInUserGuid, message.SubjectUserGuid, message.AddToRoleGuids, message.RemoveFromRoleGuids); + private Task<Result<ChangeUserRolesResult, UserActionFailure>> HandleChangeUserRoles(ChangeUserRolesMessage message) { + return userRoleManager.ChangeUserRoles(userLoginManager.GetLoggedInUser(message.AuthToken), message.SubjectUserGuid, message.AddToRoleGuids, message.RemoveFromRoleGuids); } - private Task<DeleteUserResult> HandleDeleteUser(DeleteUserMessage message) { - return userManager.DeleteByGuid(message.LoggedInUserGuid, message.SubjectUserGuid); + private Task<Result<DeleteUserResult, UserActionFailure>> HandleDeleteUser(DeleteUserMessage message) { + return userManager.DeleteByGuid(userLoginManager.GetLoggedInUser(message.AuthToken), message.SubjectUserGuid); } - private Task<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>> HandleCreateOrUpdateInstance(CreateOrUpdateInstanceMessage message) { - return agentManager.DoInstanceAction<AgentActor.CreateOrUpdateInstanceCommand, CreateOrUpdateInstanceResult>(message.Configuration.AgentGuid, new AgentActor.CreateOrUpdateInstanceCommand(message.LoggedInUserGuid, message.InstanceGuid, message.Configuration)); + private Task<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>> HandleCreateOrUpdateInstance(CreateOrUpdateInstanceMessage message) { + return agentManager.DoInstanceAction<AgentActor.CreateOrUpdateInstanceCommand, CreateOrUpdateInstanceResult>(message.Configuration.AgentGuid, new AgentActor.CreateOrUpdateInstanceCommand(message.AuthToken, message.InstanceGuid, message.Configuration)); } - private Task<Result<LaunchInstanceResult, InstanceActionFailure>> HandleLaunchInstance(LaunchInstanceMessage message) { - return agentManager.DoInstanceAction<AgentActor.LaunchInstanceCommand, LaunchInstanceResult>(message.AgentGuid, new AgentActor.LaunchInstanceCommand(message.InstanceGuid, message.LoggedInUserGuid)); + private Task<Result<LaunchInstanceResult, UserInstanceActionFailure>> HandleLaunchInstance(LaunchInstanceMessage message) { + return agentManager.DoInstanceAction<AgentActor.LaunchInstanceCommand, LaunchInstanceResult>(message.AgentGuid, new AgentActor.LaunchInstanceCommand(message.AuthToken, message.InstanceGuid)); } - private Task<Result<StopInstanceResult, InstanceActionFailure>> HandleStopInstance(StopInstanceMessage message) { - return agentManager.DoInstanceAction<AgentActor.StopInstanceCommand, StopInstanceResult>(message.AgentGuid, new AgentActor.StopInstanceCommand(message.InstanceGuid, message.LoggedInUserGuid, message.StopStrategy)); + private Task<Result<StopInstanceResult, UserInstanceActionFailure>> HandleStopInstance(StopInstanceMessage message) { + return agentManager.DoInstanceAction<AgentActor.StopInstanceCommand, StopInstanceResult>(message.AgentGuid, new AgentActor.StopInstanceCommand(message.AuthToken, message.InstanceGuid, message.StopStrategy)); } - private Task<Result<SendCommandToInstanceResult, InstanceActionFailure>> HandleSendCommandToInstance(SendCommandToInstanceMessage message) { - return agentManager.DoInstanceAction<AgentActor.SendCommandToInstanceCommand, SendCommandToInstanceResult>(message.AgentGuid, new AgentActor.SendCommandToInstanceCommand(message.InstanceGuid, message.LoggedInUserGuid, message.Command)); + private Task<Result<SendCommandToInstanceResult, UserInstanceActionFailure>> HandleSendCommandToInstance(SendCommandToInstanceMessage message) { + return agentManager.DoInstanceAction<AgentActor.SendCommandToInstanceCommand, SendCommandToInstanceResult>(message.AgentGuid, new AgentActor.SendCommandToInstanceCommand(message.AuthToken, message.InstanceGuid, message.Command)); } private Task<ImmutableArray<MinecraftVersion>> HandleGetMinecraftVersions(GetMinecraftVersionsMessage message) { @@ -163,12 +164,12 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> { return controllerState.AgentJavaRuntimesByGuid; } - private Task<ImmutableArray<AuditLogItem>> HandleGetAuditLog(GetAuditLogMessage message) { - return auditLogManager.GetMostRecentItems(message.Count); + private Task<Result<ImmutableArray<AuditLogItem>, UserActionFailure>> HandleGetAuditLog(GetAuditLogMessage message) { + return auditLogManager.GetMostRecentItems(userLoginManager.GetLoggedInUser(message.AuthToken), message.Count); } - private Task<ImmutableArray<EventLogItem>> HandleGetEventLog(GetEventLogMessage message) { - return eventLogManager.GetMostRecentItems(message.Count); + private Task<Result<ImmutableArray<EventLogItem>, UserActionFailure>> HandleGetEventLog(GetEventLogMessage message) { + return eventLogManager.GetMostRecentItems(userLoginManager.GetLoggedInUser(message.AuthToken), message.Count); } private void HandleReply(ReplyMessage message) { diff --git a/Controller/Phantom.Controller.Services/Rpc/WebRegistrationHandler.cs b/Controller/Phantom.Controller.Services/Rpc/WebRegistrationHandler.cs index 3c3319c..7217592 100644 --- a/Controller/Phantom.Controller.Services/Rpc/WebRegistrationHandler.cs +++ b/Controller/Phantom.Controller.Services/Rpc/WebRegistrationHandler.cs @@ -7,6 +7,7 @@ using Phantom.Controller.Services.Agents; using Phantom.Controller.Services.Events; using Phantom.Controller.Services.Instances; using Phantom.Controller.Services.Users; +using Phantom.Controller.Services.Users.Sessions; using Phantom.Utils.Actor; using Phantom.Utils.Logging; using Phantom.Utils.Rpc.Runtime; diff --git a/Controller/Phantom.Controller.Services/Users/AuditLogManager.cs b/Controller/Phantom.Controller.Services/Users/AuditLogManager.cs index 1511802..bf9d589 100644 --- a/Controller/Phantom.Controller.Services/Users/AuditLogManager.cs +++ b/Controller/Phantom.Controller.Services/Users/AuditLogManager.cs @@ -1,7 +1,10 @@ using System.Collections.Immutable; +using Phantom.Common.Data; using Phantom.Common.Data.Web.AuditLog; +using Phantom.Common.Data.Web.Users; using Phantom.Controller.Database; using Phantom.Controller.Database.Repositories; +using Phantom.Controller.Services.Users.Sessions; namespace Phantom.Controller.Services.Users; @@ -12,7 +15,11 @@ sealed class AuditLogManager { this.dbProvider = dbProvider; } - public async Task<ImmutableArray<AuditLogItem>> GetMostRecentItems(int count) { + public async Task<Result<ImmutableArray<AuditLogItem>, UserActionFailure>> GetMostRecentItems(LoggedInUser loggedInUser, int count) { + if (!loggedInUser.CheckPermission(Permission.ViewAudit)) { + return UserActionFailure.NotAuthorized; + } + await using var db = dbProvider.Lazy(); return await new AuditLogRepository(db).GetMostRecentItems(count, CancellationToken.None); } diff --git a/Controller/Phantom.Controller.Services/Users/PermissionManager.cs b/Controller/Phantom.Controller.Services/Users/PermissionManager.cs index 5427c3e..848608e 100644 --- a/Controller/Phantom.Controller.Services/Users/PermissionManager.cs +++ b/Controller/Phantom.Controller.Services/Users/PermissionManager.cs @@ -36,34 +36,6 @@ sealed class PermissionManager { } } - public async Task<PermissionSet> FetchPermissionsForAllUsers(Guid userId) { - await using var ctx = dbProvider.Eager(); - - var userPermissions = ctx.UserPermissions - .Where(up => up.UserGuid == userId) - .Select(static up => up.PermissionId); - - var rolePermissions = ctx.UserRoles - .Where(ur => ur.UserGuid == userId) - .Join(ctx.RolePermissions, static ur => ur.RoleGuid, static rp => rp.RoleGuid, static (ur, rp) => rp.PermissionId); - - return new PermissionSet(await userPermissions.Union(rolePermissions).AsAsyncEnumerable().ToImmutableSetAsync()); - } - - public async Task<PermissionSet> FetchPermissionsForUserId(Guid userId) { - await using var ctx = dbProvider.Eager(); - - var userPermissions = ctx.UserPermissions - .Where(up => up.UserGuid == userId) - .Select(static up => up.PermissionId); - - var rolePermissions = ctx.UserRoles - .Where(ur => ur.UserGuid == userId) - .Join(ctx.RolePermissions, static ur => ur.RoleGuid, static rp => rp.RoleGuid, static (ur, rp) => rp.PermissionId); - - return new PermissionSet(await userPermissions.Union(rolePermissions).AsAsyncEnumerable().ToImmutableSetAsync()); - } - public static ImmutableArray<string> GetMissingPermissionsOrdered(IEnumerable<Permission> allPermissions, ImmutableHashSet<string> existingPermissionIds) { return allPermissions.Select(static permission => permission.Id).Except(existingPermissionIds).Order().ToImmutableArray(); } diff --git a/Controller/Phantom.Controller.Services/Users/Sessions/AuthenticatedUserCache.cs b/Controller/Phantom.Controller.Services/Users/Sessions/AuthenticatedUserCache.cs new file mode 100644 index 0000000..7a77c4c --- /dev/null +++ b/Controller/Phantom.Controller.Services/Users/Sessions/AuthenticatedUserCache.cs @@ -0,0 +1,26 @@ +using System.Collections.Concurrent; +using Phantom.Common.Data.Web.Users; +using Phantom.Controller.Database; +using Phantom.Controller.Database.Entities; +using Phantom.Controller.Database.Repositories; + +namespace Phantom.Controller.Services.Users.Sessions; + +sealed class AuthenticatedUserCache { + private readonly ConcurrentDictionary<Guid, AuthenticatedUserInfo> authenticatedUsersByGuid = new (); + + public bool TryGet(Guid userGuid, out AuthenticatedUserInfo? userInfo) { + return authenticatedUsersByGuid.TryGetValue(userGuid, out userInfo); + } + + public async Task<AuthenticatedUserInfo?> Update(UserEntity user, ILazyDbContext db) { + var userGuid = user.UserGuid; + var userPermissions = await new PermissionRepository(db).GetAllUserPermissions(user); + var userInfo = new AuthenticatedUserInfo(userGuid, user.Name, userPermissions); + return authenticatedUsersByGuid[userGuid] = userInfo; + } + + public void Remove(Guid userGuid) { + authenticatedUsersByGuid.Remove(userGuid, out _); + } +} diff --git a/Controller/Phantom.Controller.Services/Users/Sessions/LoggedInUser.cs b/Controller/Phantom.Controller.Services/Users/Sessions/LoggedInUser.cs new file mode 100644 index 0000000..583c03b --- /dev/null +++ b/Controller/Phantom.Controller.Services/Users/Sessions/LoggedInUser.cs @@ -0,0 +1,11 @@ +using Phantom.Common.Data.Web.Users; + +namespace Phantom.Controller.Services.Users.Sessions; + +readonly record struct LoggedInUser(AuthenticatedUserInfo? AuthenticatedUserInfo) { + public Guid? Guid => AuthenticatedUserInfo?.Guid; + + public bool CheckPermission(Permission permission) { + return AuthenticatedUserInfo != null && AuthenticatedUserInfo.Permissions.Check(permission); + } +} diff --git a/Controller/Phantom.Controller.Services/Users/Sessions/UserLoginManager.cs b/Controller/Phantom.Controller.Services/Users/Sessions/UserLoginManager.cs new file mode 100644 index 0000000..aa3157a --- /dev/null +++ b/Controller/Phantom.Controller.Services/Users/Sessions/UserLoginManager.cs @@ -0,0 +1,139 @@ +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.Sessions; + +sealed class UserLoginManager { + private const int SessionIdBytes = 20; + + private readonly AuthenticatedUserCache authenticatedUserCache; + private readonly UserManager userManager; + private readonly IDbContextProvider dbProvider; + + private readonly UserSessionBucket[] sessionBuckets = new UserSessionBucket[256]; + + public UserLoginManager(AuthenticatedUserCache authenticatedUserCache, UserManager userManager, IDbContextProvider dbProvider) { + this.authenticatedUserCache = authenticatedUserCache; + this.userManager = userManager; + this.dbProvider = dbProvider; + + for (int i = 0; i < sessionBuckets.GetLength(0); i++) { + sessionBuckets[i] = new UserSessionBucket(); + } + } + + private UserSessionBucket GetSessionBucket(ImmutableArray<byte> token) { + return sessionBuckets[token[0]]; + } + + public async Task<LogInSuccess?> LogIn(string username, string password) { + Guid userGuid; + AuthenticatedUserInfo? authenticatedUserInfo; + + await using (var db = dbProvider.Lazy()) { + var userRepository = new UserRepository(db); + + var user = await userRepository.GetByName(username); + if (user == null || !UserPasswords.Verify(password, user.PasswordHash)) { + return null; + } + + authenticatedUserInfo = await authenticatedUserCache.Update(user, db); + if (authenticatedUserInfo == null) { + return null; + } + + userGuid = user.UserGuid; + + var auditLogWriter = new AuditLogRepository(db).Writer(userGuid); + auditLogWriter.UserLoggedIn(user); + + await db.Ctx.SaveChangesAsync(); + } + + var authToken = ImmutableArray.Create(RandomNumberGenerator.GetBytes(SessionIdBytes)); + GetSessionBucket(authToken).Add(userGuid, authToken); + + return new LogInSuccess(authenticatedUserInfo, authToken); + } + + public async Task LogOut(Guid userGuid, ImmutableArray<byte> authToken) { + if (!GetSessionBucket(authToken).Remove(userGuid, authToken)) { + return; + } + + await using var db = dbProvider.Lazy(); + + var auditLogWriter = new AuditLogRepository(db).Writer(userGuid); + auditLogWriter.UserLoggedOut(userGuid); + + await db.Ctx.SaveChangesAsync(); + } + + public LoggedInUser GetLoggedInUser(ImmutableArray<byte> authToken) { + var userGuid = GetSessionBucket(authToken).FindUserGuid(authToken); + return userGuid != null && authenticatedUserCache.TryGet(userGuid.Value, out var userInfo) ? new LoggedInUser(userInfo) : default; + } + + public AuthenticatedUserInfo? GetAuthenticatedUser(Guid userGuid, ImmutableArray<byte> authToken) { + return authenticatedUserCache.TryGet(userGuid, out var userInfo) && GetSessionBucket(authToken).Contains(userGuid, authToken) ? userInfo : null; + } + + private sealed class UserSessionBucket { + private ImmutableList<UserSession> sessions = ImmutableList<UserSession>.Empty; + + public void Add(Guid userGuid, ImmutableArray<byte> authToken) { + lock (this) { + var session = new UserSession(userGuid, authToken); + if (!sessions.Contains(session)) { + sessions = sessions.Add(session); + } + } + } + + public bool Contains(Guid userGuid, ImmutableArray<byte> authToken) { + lock (this) { + return sessions.Contains(new UserSession(userGuid, authToken)); + } + } + + public Guid? FindUserGuid(ImmutableArray<byte> authToken) { + lock (this) { + return sessions.Find(session => session.AuthTokenEquals(authToken))?.UserGuid; + } + } + + public bool Remove(Guid userGuid, ImmutableArray<byte> authToken) { + lock (this) { + int index = sessions.IndexOf(new UserSession(userGuid, authToken)); + if (index == -1) { + return false; + } + + sessions = sessions.RemoveAt(index); + return true; + } + } + } + + private sealed record UserSession(Guid UserGuid, ImmutableArray<byte> AuthToken) { + public bool AuthTokenEquals(ImmutableArray<byte> other) { + return CryptographicOperations.FixedTimeEquals(AuthToken.AsSpan(), other.AsSpan()); + } + + public bool Equals(UserSession? other) { + if (ReferenceEquals(null, other)) { + return false; + } + + return UserGuid.Equals(other.UserGuid) && AuthTokenEquals(other.AuthToken); + } + + public override int GetHashCode() { + throw new NotImplementedException(); + } + } +} diff --git a/Controller/Phantom.Controller.Services/Users/UserLoginManager.cs b/Controller/Phantom.Controller.Services/Users/UserLoginManager.cs deleted file mode 100644 index 6584908..0000000 --- a/Controller/Phantom.Controller.Services/Users/UserLoginManager.cs +++ /dev/null @@ -1,107 +0,0 @@ -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; -using Phantom.Controller.Database.Repositories; - -namespace Phantom.Controller.Services.Users; - -sealed class UserLoginManager { - private const int SessionIdBytes = 20; - private readonly ConcurrentDictionary<Guid, UserSession> sessionsByUserGuid = new (); - - private readonly UserManager userManager; - private readonly PermissionManager permissionManager; - private readonly IDbContextProvider dbProvider; - - 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) { - var user = await userManager.GetAuthenticated(username, password); - if (user == 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)); - - 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); - auditLogWriter.UserLoggedIn(user); - - await db.Ctx.SaveChangesAsync(); - } - - return new LogInSuccess(userInfo, token); - } - - 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(); - - var auditLogWriter = new AuditLogRepository(db).Writer(userGuid); - auditLogWriter.UserLoggedOut(userGuid); - - 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/Controller/Phantom.Controller.Services/Users/UserManager.cs b/Controller/Phantom.Controller.Services/Users/UserManager.cs index 571c533..5045f59 100644 --- a/Controller/Phantom.Controller.Services/Users/UserManager.cs +++ b/Controller/Phantom.Controller.Services/Users/UserManager.cs @@ -1,8 +1,10 @@ using System.Collections.Immutable; +using Phantom.Common.Data; using Phantom.Common.Data.Web.Users; using Phantom.Controller.Database; using Phantom.Controller.Database.Entities; using Phantom.Controller.Database.Repositories; +using Phantom.Controller.Services.Users.Sessions; using Phantom.Utils.Logging; using Serilog; @@ -11,9 +13,11 @@ namespace Phantom.Controller.Services.Users; sealed class UserManager { private static readonly ILogger Logger = PhantomLogger.Create<UserManager>(); + private readonly AuthenticatedUserCache authenticatedUserCache; private readonly IDbContextProvider dbProvider; - public UserManager(IDbContextProvider dbProvider) { + public UserManager(AuthenticatedUserCache authenticatedUserCache, IDbContextProvider dbProvider) { + this.authenticatedUserCache = authenticatedUserCache; this.dbProvider = dbProvider; } @@ -85,10 +89,14 @@ sealed class UserManager { } } - public async Task<CreateUserResult> Create(Guid loggedInUserGuid, string username, string password) { + public async Task<Result<CreateUserResult, UserActionFailure>> Create(LoggedInUser loggedInUser, string username, string password) { + if (!loggedInUser.CheckPermission(Permission.EditUsers)) { + return UserActionFailure.NotAuthorized; + } + await using var db = dbProvider.Lazy(); var userRepository = new UserRepository(db); - var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUserGuid); + var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUser.Guid); try { var result = await userRepository.CreateUser(username, password); @@ -109,7 +117,11 @@ sealed class UserManager { } } - public async Task<DeleteUserResult> DeleteByGuid(Guid loggedInUserGuid, Guid userGuid) { + public async Task<Result<DeleteUserResult, UserActionFailure>> DeleteByGuid(LoggedInUser loggedInUser, Guid userGuid) { + if (!loggedInUser.CheckPermission(Permission.EditUsers)) { + return UserActionFailure.NotAuthorized; + } + await using var db = dbProvider.Lazy(); var userRepository = new UserRepository(db); @@ -118,11 +130,16 @@ sealed class UserManager { return DeleteUserResult.NotFound; } - var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUserGuid); + authenticatedUserCache.Remove(userGuid); + + var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUser.Guid); try { userRepository.DeleteUser(user); auditLogWriter.UserDeleted(user); await db.Ctx.SaveChangesAsync(); + + // In case the user logged in during deletion. + authenticatedUserCache.Remove(userGuid); Logger.Information("Deleted user \"{Username}\" (GUID {Guid}).", user.Name, user.UserGuid); return DeleteUserResult.Deleted; diff --git a/Controller/Phantom.Controller.Services/Users/UserRoleManager.cs b/Controller/Phantom.Controller.Services/Users/UserRoleManager.cs index 56e0b13..b71e870 100644 --- a/Controller/Phantom.Controller.Services/Users/UserRoleManager.cs +++ b/Controller/Phantom.Controller.Services/Users/UserRoleManager.cs @@ -1,7 +1,9 @@ using System.Collections.Immutable; +using Phantom.Common.Data; using Phantom.Common.Data.Web.Users; using Phantom.Controller.Database; using Phantom.Controller.Database.Repositories; +using Phantom.Controller.Services.Users.Sessions; using Phantom.Utils.Logging; using Serilog; @@ -9,10 +11,12 @@ namespace Phantom.Controller.Services.Users; sealed class UserRoleManager { private static readonly ILogger Logger = PhantomLogger.Create<UserRoleManager>(); - + + private readonly AuthenticatedUserCache authenticatedUserCache; private readonly IDbContextProvider dbProvider; - public UserRoleManager(IDbContextProvider dbProvider) { + public UserRoleManager(AuthenticatedUserCache authenticatedUserCache, IDbContextProvider dbProvider) { + this.authenticatedUserCache = authenticatedUserCache; this.dbProvider = dbProvider; } @@ -21,7 +25,11 @@ sealed class UserRoleManager { return await new UserRoleRepository(db).GetRoleGuidsByUserGuid(userGuids); } - public async Task<ChangeUserRolesResult> ChangeUserRoles(Guid loggedInUserGuid, Guid subjectUserGuid, ImmutableHashSet<Guid> addToRoleGuids, ImmutableHashSet<Guid> removeFromRoleGuids) { + public async Task<Result<ChangeUserRolesResult, UserActionFailure>> ChangeUserRoles(LoggedInUser loggedInUser, Guid subjectUserGuid, ImmutableHashSet<Guid> addToRoleGuids, ImmutableHashSet<Guid> removeFromRoleGuids) { + if (!loggedInUser.CheckPermission(Permission.EditUsers)) { + return UserActionFailure.NotAuthorized; + } + await using var db = dbProvider.Lazy(); var userRepository = new UserRepository(db); @@ -32,7 +40,7 @@ sealed class UserRoleManager { var roleRepository = new RoleRepository(db); var userRoleRepository = new UserRoleRepository(db); - var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUserGuid); + var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUser.Guid); var rolesByGuid = await roleRepository.GetByGuids(addToRoleGuids.Union(removeFromRoleGuids)); @@ -62,6 +70,8 @@ sealed class UserRoleManager { auditLogWriter.UserRolesChanged(user, addedToRoleNames, removedFromRoleNames); await db.Ctx.SaveChangesAsync(); + await authenticatedUserCache.Update(user, db); + Logger.Information("Changed roles for user \"{Username}\" (GUID {Guid}).", user.Name, user.UserGuid); return new ChangeUserRolesResult(addedToRoleGuids.ToImmutable(), removedFromRoleGuids.ToImmutable()); } catch (Exception e) { diff --git a/Utils/Phantom.Utils.Actor/Tasks/TaskExtensions.cs b/Utils/Phantom.Utils.Actor/Tasks/TaskExtensions.cs index 4aa01b9..14a5895 100644 --- a/Utils/Phantom.Utils.Actor/Tasks/TaskExtensions.cs +++ b/Utils/Phantom.Utils.Actor/Tasks/TaskExtensions.cs @@ -17,6 +17,10 @@ public static class TaskExtensions { return task.ContinueOnActor(result => mapper(result, arg)); } + public static Task<TResult> ContinueOnActor<TSource, TArg1, TArg2, TResult>(this Task<TSource> task, Func<TSource, TArg1, TArg2, TResult> mapper, TArg1 arg1, TArg2 arg2) { + return task.ContinueOnActor(result => mapper(result, arg1, arg2)); + } + private static Task<TResult> MapResult<TSource, TResult>(Task<TSource> task, Func<TSource, TResult> mapper, TaskCompletionSource<TResult> completionSource) { if (task.IsFaulted) { completionSource.SetException(task.Exception.InnerExceptions); diff --git a/Utils/Phantom.Utils/Result/Err.cs b/Utils/Phantom.Utils/Result/Err.cs new file mode 100644 index 0000000..839bec8 --- /dev/null +++ b/Utils/Phantom.Utils/Result/Err.cs @@ -0,0 +1,3 @@ +namespace Phantom.Utils.Result; + +public sealed record Err<T>(T Error) : Result; diff --git a/Utils/Phantom.Utils/Result/Ok.cs b/Utils/Phantom.Utils/Result/Ok.cs new file mode 100644 index 0000000..8f47b2f --- /dev/null +++ b/Utils/Phantom.Utils/Result/Ok.cs @@ -0,0 +1,3 @@ +namespace Phantom.Utils.Result; + +public sealed record Ok<T>(T Value) : Result; diff --git a/Utils/Phantom.Utils/Result/Result.cs b/Utils/Phantom.Utils/Result/Result.cs new file mode 100644 index 0000000..50f2fb4 --- /dev/null +++ b/Utils/Phantom.Utils/Result/Result.cs @@ -0,0 +1,5 @@ +namespace Phantom.Utils.Result; + +public abstract record Result { + private protected Result() {} +} diff --git a/Web/Phantom.Web.Components/PhantomComponent.cs b/Web/Phantom.Web.Components/PhantomComponent.cs index d593732..ba42188 100644 --- a/Web/Phantom.Web.Components/PhantomComponent.cs +++ b/Web/Phantom.Web.Components/PhantomComponent.cs @@ -17,11 +17,11 @@ public abstract class PhantomComponent : ComponentBase, IDisposable { protected CancellationToken CancellationToken => cancellationTokenSource.Token; - protected async Task<Guid?> GetUserGuid() { + protected async Task<AuthenticatedUser?> GetAuthenticatedUser() { var authenticationState = await AuthenticationStateTask; - return authenticationState.TryGetGuid(); + return authenticationState.GetAuthenticatedUser(); } - + protected async Task<bool> CheckPermission(Permission permission) { var authenticationState = await AuthenticationStateTask; return authenticationState.CheckPermission(permission); diff --git a/Web/Phantom.Web.Services/Authentication/AuthenticatedUser.cs b/Web/Phantom.Web.Services/Authentication/AuthenticatedUser.cs new file mode 100644 index 0000000..8f024b6 --- /dev/null +++ b/Web/Phantom.Web.Services/Authentication/AuthenticatedUser.cs @@ -0,0 +1,10 @@ +using System.Collections.Immutable; +using Phantom.Common.Data.Web.Users; + +namespace Phantom.Web.Services.Authentication; + +public sealed record AuthenticatedUser(AuthenticatedUserInfo Info, ImmutableArray<byte> Token) { + public bool CheckPermission(Permission permission) { + return Info.Permissions.Check(permission); + } +} diff --git a/Web/Phantom.Web.Services/Authentication/AuthenticationStateExtensions.cs b/Web/Phantom.Web.Services/Authentication/AuthenticationStateExtensions.cs index 18e7907..4a9603e 100644 --- a/Web/Phantom.Web.Services/Authentication/AuthenticationStateExtensions.cs +++ b/Web/Phantom.Web.Services/Authentication/AuthenticationStateExtensions.cs @@ -5,23 +5,27 @@ 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 AuthenticatedUser? GetAuthenticatedUser(this AuthenticationState authenticationState) { + return authenticationState.User.GetAuthenticatedUser(); } - 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 AuthenticatedUser? GetAuthenticatedUser(this ClaimsPrincipal claimsPrincipal) { + return claimsPrincipal is CustomClaimsPrincipal principal ? principal.User : null; } public static PermissionSet GetPermissions(this AuthenticationState authenticationState) { return authenticationState.User.GetPermissions(); } + + public static PermissionSet GetPermissions(this ClaimsPrincipal claimsPrincipal) { + return claimsPrincipal.GetAuthenticatedUser() is {} user ? user.Info.Permissions : PermissionSet.None; + } public static bool CheckPermission(this AuthenticationState authenticationState, Permission permission) { return authenticationState.User.CheckPermission(permission); } + + public static bool CheckPermission(this ClaimsPrincipal claimsPrincipal, Permission permission) { + return claimsPrincipal.GetPermissions().Check(permission); + } } diff --git a/Web/Phantom.Web.Services/Authentication/CustomAuthenticationStateProvider.cs b/Web/Phantom.Web.Services/Authentication/CustomAuthenticationStateProvider.cs index c2ae175..f4fdfb0 100644 --- a/Web/Phantom.Web.Services/Authentication/CustomAuthenticationStateProvider.cs +++ b/Web/Phantom.Web.Services/Authentication/CustomAuthenticationStateProvider.cs @@ -22,9 +22,10 @@ public sealed class CustomAuthenticationStateProvider : ServerAuthenticationStat if (!isLoaded) { var stored = await sessionBrowserStorage.Get(); if (stored != null) { - var session = await controllerConnection.Send<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(new GetAuthenticatedUser(stored.UserGuid, stored.Token), TimeSpan.FromSeconds(30)); + var authToken = stored.Token; + var session = await controllerConnection.Send<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(new GetAuthenticatedUser(stored.UserGuid, authToken), TimeSpan.FromSeconds(30)); if (session.Value is {} userInfo) { - SetLoadedSession(userInfo); + SetLoadedSession(new AuthenticatedUser(userInfo, authToken)); } } } @@ -32,9 +33,9 @@ public sealed class CustomAuthenticationStateProvider : ServerAuthenticationStat return await base.GetAuthenticationStateAsync(); } - internal void SetLoadedSession(AuthenticatedUserInfo user) { + internal void SetLoadedSession(AuthenticatedUser authenticatedUser) { isLoaded = true; - SetAuthenticationState(Task.FromResult(new AuthenticationState(new CustomClaimsPrincipal(user)))); + SetAuthenticationState(Task.FromResult(new AuthenticationState(new CustomClaimsPrincipal(authenticatedUser)))); } internal void SetUnloadedSession() { diff --git a/Web/Phantom.Web.Services/Authentication/CustomClaimsPrincipal.cs b/Web/Phantom.Web.Services/Authentication/CustomClaimsPrincipal.cs index 3a0188d..4946287 100644 --- a/Web/Phantom.Web.Services/Authentication/CustomClaimsPrincipal.cs +++ b/Web/Phantom.Web.Services/Authentication/CustomClaimsPrincipal.cs @@ -4,10 +4,10 @@ using Phantom.Common.Data.Web.Users; namespace Phantom.Web.Services.Authentication; sealed class CustomClaimsPrincipal : ClaimsPrincipal { - internal AuthenticatedUserInfo UserInfo { get; } + internal AuthenticatedUser User { get; } - internal CustomClaimsPrincipal(AuthenticatedUserInfo userInfo) : base(GetIdentity(userInfo)) { - UserInfo = userInfo; + internal CustomClaimsPrincipal(AuthenticatedUser user) : base(GetIdentity(user.Info)) { + User = user; } private static ClaimsIdentity GetIdentity(AuthenticatedUserInfo userInfo) { diff --git a/Web/Phantom.Web.Services/Authentication/UserLoginManager.cs b/Web/Phantom.Web.Services/Authentication/UserLoginManager.cs index 6a1c914..a90aef0 100644 --- a/Web/Phantom.Web.Services/Authentication/UserLoginManager.cs +++ b/Web/Phantom.Web.Services/Authentication/UserLoginManager.cs @@ -37,9 +37,10 @@ public sealed class UserLoginManager { Logger.Information("Successfully logged in {Username}.", username); var userInfo = success.UserInfo; - - await sessionBrowserStorage.Store(userInfo.Guid, success.Token); - authenticationStateProvider.SetLoadedSession(userInfo); + var authToken = success.AuthToken; + + await sessionBrowserStorage.Store(userInfo.Guid, authToken); + authenticationStateProvider.SetLoadedSession(new AuthenticatedUser(userInfo, authToken)); await navigation.NavigateTo(returnUrl ?? string.Empty); return true; diff --git a/Web/Phantom.Web.Services/Events/EventLogManager.cs b/Web/Phantom.Web.Services/Events/EventLogManager.cs index 588b2dc..0bb5f55 100644 --- a/Web/Phantom.Web.Services/Events/EventLogManager.cs +++ b/Web/Phantom.Web.Services/Events/EventLogManager.cs @@ -1,6 +1,9 @@ using System.Collections.Immutable; +using Phantom.Common.Data; using Phantom.Common.Data.Web.EventLog; +using Phantom.Common.Data.Web.Users; using Phantom.Common.Messages.Web.ToController; +using Phantom.Web.Services.Authentication; using Phantom.Web.Services.Rpc; namespace Phantom.Web.Services.Events; @@ -12,8 +15,13 @@ public sealed class EventLogManager { this.controllerConnection = controllerConnection; } - public Task<ImmutableArray<EventLogItem>> GetMostRecentItems(int count, CancellationToken cancellationToken) { - var message = new GetEventLogMessage(count); - return controllerConnection.Send<GetEventLogMessage, ImmutableArray<EventLogItem>>(message, cancellationToken); + public async Task<Result<ImmutableArray<EventLogItem>, UserActionFailure>> GetMostRecentItems(AuthenticatedUser? authenticatedUser, int count, CancellationToken cancellationToken) { + if (authenticatedUser != null && authenticatedUser.CheckPermission(Permission.ViewEvents)) { + var message = new GetEventLogMessage(authenticatedUser.Token, count); + return await controllerConnection.Send<GetEventLogMessage, Result<ImmutableArray<EventLogItem>, UserActionFailure>>(message, cancellationToken); + } + else { + return UserActionFailure.NotAuthorized; + } } } diff --git a/Web/Phantom.Web.Services/Instances/InstanceManager.cs b/Web/Phantom.Web.Services/Instances/InstanceManager.cs index 2794299..98f1633 100644 --- a/Web/Phantom.Web.Services/Instances/InstanceManager.cs +++ b/Web/Phantom.Web.Services/Instances/InstanceManager.cs @@ -4,9 +4,11 @@ using Phantom.Common.Data.Instance; using Phantom.Common.Data.Minecraft; using Phantom.Common.Data.Replies; using Phantom.Common.Data.Web.Instance; +using Phantom.Common.Data.Web.Users; using Phantom.Common.Messages.Web.ToController; using Phantom.Utils.Events; using Phantom.Utils.Logging; +using Phantom.Web.Services.Authentication; using Phantom.Web.Services.Rpc; namespace Phantom.Web.Services.Instances; @@ -35,23 +37,43 @@ public sealed class InstanceManager { return instances.Value.GetValueOrDefault(instanceGuid); } - public Task<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>> CreateOrUpdateInstance(Guid loggedInUserGuid, Guid instanceGuid, InstanceConfiguration configuration, CancellationToken cancellationToken) { - var message = new CreateOrUpdateInstanceMessage(loggedInUserGuid, instanceGuid, configuration); - return controllerConnection.Send<CreateOrUpdateInstanceMessage, Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(message, cancellationToken); + public async Task<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>> CreateOrUpdateInstance(AuthenticatedUser? authenticatedUser, Guid instanceGuid, InstanceConfiguration configuration, CancellationToken cancellationToken) { + if (authenticatedUser != null && authenticatedUser.CheckPermission(Permission.CreateInstances)) { + var message = new CreateOrUpdateInstanceMessage(authenticatedUser.Token, instanceGuid, configuration); + return await controllerConnection.Send<CreateOrUpdateInstanceMessage, Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>(message, cancellationToken); + } + else { + return (UserInstanceActionFailure) UserActionFailure.NotAuthorized; + } } - public Task<Result<LaunchInstanceResult, InstanceActionFailure>> LaunchInstance(Guid loggedInUserGuid, Guid agentGuid, Guid instanceGuid, CancellationToken cancellationToken) { - var message = new LaunchInstanceMessage(loggedInUserGuid, agentGuid, instanceGuid); - return controllerConnection.Send<LaunchInstanceMessage, Result<LaunchInstanceResult, InstanceActionFailure>>(message, cancellationToken); + public async Task<Result<LaunchInstanceResult, UserInstanceActionFailure>> LaunchInstance(AuthenticatedUser? authenticatedUser, Guid agentGuid, Guid instanceGuid, CancellationToken cancellationToken) { + if (authenticatedUser != null && authenticatedUser.CheckPermission(Permission.ControlInstances)) { + var message = new LaunchInstanceMessage(authenticatedUser.Token, agentGuid, instanceGuid); + return await controllerConnection.Send<LaunchInstanceMessage, Result<LaunchInstanceResult, UserInstanceActionFailure>>(message, cancellationToken); + } + else { + return (UserInstanceActionFailure) UserActionFailure.NotAuthorized; + } } - public Task<Result<StopInstanceResult, InstanceActionFailure>> StopInstance(Guid loggedInUserGuid, Guid agentGuid, Guid instanceGuid, MinecraftStopStrategy stopStrategy, CancellationToken cancellationToken) { - var message = new StopInstanceMessage(loggedInUserGuid, agentGuid, instanceGuid, stopStrategy); - return controllerConnection.Send<StopInstanceMessage, Result<StopInstanceResult, InstanceActionFailure>>(message, cancellationToken); + public async Task<Result<StopInstanceResult, UserInstanceActionFailure>> StopInstance(AuthenticatedUser? authenticatedUser, Guid agentGuid, Guid instanceGuid, MinecraftStopStrategy stopStrategy, CancellationToken cancellationToken) { + if (authenticatedUser != null && authenticatedUser.CheckPermission(Permission.ControlInstances)) { + var message = new StopInstanceMessage(authenticatedUser.Token, agentGuid, instanceGuid, stopStrategy); + return await controllerConnection.Send<StopInstanceMessage, Result<StopInstanceResult, UserInstanceActionFailure>>(message, cancellationToken); + } + else { + return (UserInstanceActionFailure) UserActionFailure.NotAuthorized; + } } - public Task<Result<SendCommandToInstanceResult, InstanceActionFailure>> SendCommandToInstance(Guid loggedInUserGuid, Guid agentGuid, Guid instanceGuid, string command, CancellationToken cancellationToken) { - var message = new SendCommandToInstanceMessage(loggedInUserGuid, agentGuid, instanceGuid, command); - return controllerConnection.Send<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, InstanceActionFailure>>(message, cancellationToken); + public async Task<Result<SendCommandToInstanceResult, UserInstanceActionFailure>> SendCommandToInstance(AuthenticatedUser? authenticatedUser, Guid agentGuid, Guid instanceGuid, string command, CancellationToken cancellationToken) { + if (authenticatedUser != null && authenticatedUser.CheckPermission(Permission.ControlInstances)) { + var message = new SendCommandToInstanceMessage(authenticatedUser.Token, agentGuid, instanceGuid, command); + return await controllerConnection.Send<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, UserInstanceActionFailure>>(message, cancellationToken); + } + else { + return (UserInstanceActionFailure) UserActionFailure.NotAuthorized; + } } } diff --git a/Web/Phantom.Web.Services/Users/AuditLogManager.cs b/Web/Phantom.Web.Services/Users/AuditLogManager.cs index 9f521e7..ff20c4c 100644 --- a/Web/Phantom.Web.Services/Users/AuditLogManager.cs +++ b/Web/Phantom.Web.Services/Users/AuditLogManager.cs @@ -1,6 +1,9 @@ using System.Collections.Immutable; +using Phantom.Common.Data; using Phantom.Common.Data.Web.AuditLog; +using Phantom.Common.Data.Web.Users; using Phantom.Common.Messages.Web.ToController; +using Phantom.Web.Services.Authentication; using Phantom.Web.Services.Rpc; namespace Phantom.Web.Services.Users; @@ -12,8 +15,13 @@ public sealed class AuditLogManager { this.controllerConnection = controllerConnection; } - public Task<ImmutableArray<AuditLogItem>> GetMostRecentItems(int count, CancellationToken cancellationToken) { - var message = new GetAuditLogMessage(count); - return controllerConnection.Send<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(message, cancellationToken); + public async Task<Result<ImmutableArray<AuditLogItem>, UserActionFailure>> GetMostRecentItems(AuthenticatedUser? authenticatedUser, int count, CancellationToken cancellationToken) { + if (authenticatedUser != null && authenticatedUser.CheckPermission(Permission.ViewAudit)) { + var message = new GetAuditLogMessage(authenticatedUser.Token, count); + return await controllerConnection.Send<GetAuditLogMessage, Result<ImmutableArray<AuditLogItem>, UserActionFailure>>(message, cancellationToken); + } + else { + return UserActionFailure.NotAuthorized; + } } } diff --git a/Web/Phantom.Web.Services/Users/UserManager.cs b/Web/Phantom.Web.Services/Users/UserManager.cs index eb0502a..30edf57 100644 --- a/Web/Phantom.Web.Services/Users/UserManager.cs +++ b/Web/Phantom.Web.Services/Users/UserManager.cs @@ -1,6 +1,8 @@ using System.Collections.Immutable; +using Phantom.Common.Data; using Phantom.Common.Data.Web.Users; using Phantom.Common.Messages.Web.ToController; +using Phantom.Web.Services.Authentication; using Phantom.Web.Services.Rpc; namespace Phantom.Web.Services.Users; @@ -16,11 +18,21 @@ public sealed class UserManager { return controllerConnection.Send<GetUsersMessage, ImmutableArray<UserInfo>>(new GetUsersMessage(), cancellationToken); } - public Task<CreateUserResult> Create(Guid loggedInUserGuid, string username, string password, CancellationToken cancellationToken) { - return controllerConnection.Send<CreateUserMessage, CreateUserResult>(new CreateUserMessage(loggedInUserGuid, username, password), cancellationToken); + public async Task<Result<CreateUserResult, UserActionFailure>> Create(AuthenticatedUser? authenticatedUser, string username, string password, CancellationToken cancellationToken) { + if (authenticatedUser != null && authenticatedUser.CheckPermission(Permission.EditUsers)) { + return await controllerConnection.Send<CreateUserMessage, Result<CreateUserResult, UserActionFailure>>(new CreateUserMessage(authenticatedUser.Token, username, password), cancellationToken); + } + else { + return UserActionFailure.NotAuthorized; + } } - public Task<DeleteUserResult> DeleteByGuid(Guid loggedInUserGuid, Guid userGuid, CancellationToken cancellationToken) { - return controllerConnection.Send<DeleteUserMessage, DeleteUserResult>(new DeleteUserMessage(loggedInUserGuid, userGuid), cancellationToken); + public async Task<Result<DeleteUserResult, UserActionFailure>> DeleteByGuid(AuthenticatedUser? authenticatedUser, Guid userGuid, CancellationToken cancellationToken) { + if (authenticatedUser != null && authenticatedUser.CheckPermission(Permission.EditUsers)) { + return await controllerConnection.Send<DeleteUserMessage, Result<DeleteUserResult, UserActionFailure>>(new DeleteUserMessage(authenticatedUser.Token, userGuid), cancellationToken); + } + else { + return UserActionFailure.NotAuthorized; + } } } diff --git a/Web/Phantom.Web.Services/Users/UserRoleManager.cs b/Web/Phantom.Web.Services/Users/UserRoleManager.cs index 06a5f7d..2fd20b1 100644 --- a/Web/Phantom.Web.Services/Users/UserRoleManager.cs +++ b/Web/Phantom.Web.Services/Users/UserRoleManager.cs @@ -1,6 +1,8 @@ using System.Collections.Immutable; +using Phantom.Common.Data; using Phantom.Common.Data.Web.Users; using Phantom.Common.Messages.Web.ToController; +using Phantom.Web.Services.Authentication; using Phantom.Web.Services.Rpc; namespace Phantom.Web.Services.Users; @@ -20,7 +22,12 @@ public sealed class UserRoleManager { return (await GetUserRoles(ImmutableHashSet.Create(userGuid), cancellationToken)).GetValueOrDefault(userGuid, ImmutableArray<Guid>.Empty); } - public Task<ChangeUserRolesResult> ChangeUserRoles(Guid loggedInUserGuid, Guid subjectUserGuid, ImmutableHashSet<Guid> addToRoleGuids, ImmutableHashSet<Guid> removeFromRoleGuids, CancellationToken cancellationToken) { - return controllerConnection.Send<ChangeUserRolesMessage, ChangeUserRolesResult>(new ChangeUserRolesMessage(loggedInUserGuid, subjectUserGuid, addToRoleGuids, removeFromRoleGuids), cancellationToken); + public async Task<Result<ChangeUserRolesResult, UserActionFailure>> ChangeUserRoles(AuthenticatedUser? authenticatedUser, Guid subjectUserGuid, ImmutableHashSet<Guid> addToRoleGuids, ImmutableHashSet<Guid> removeFromRoleGuids, CancellationToken cancellationToken) { + if (authenticatedUser != null && authenticatedUser.CheckPermission(Permission.EditUsers)) { + return await controllerConnection.Send<ChangeUserRolesMessage, Result<ChangeUserRolesResult, UserActionFailure>>(new ChangeUserRolesMessage(authenticatedUser.Token, subjectUserGuid, addToRoleGuids, removeFromRoleGuids), cancellationToken); + } + else { + return UserActionFailure.NotAuthorized; + } } } diff --git a/Web/Phantom.Web/Pages/Audit.razor b/Web/Phantom.Web/Pages/Audit.razor index 478dcff..f1b471b 100644 --- a/Web/Phantom.Web/Pages/Audit.razor +++ b/Web/Phantom.Web/Pages/Audit.razor @@ -5,13 +5,18 @@ @using Phantom.Common.Data.Web.Users @using Phantom.Web.Services.Users @using Phantom.Web.Services.Instances -@inherits Phantom.Web.Components.PhantomComponent +@inherits PhantomComponent @inject AuditLogManager AuditLogManager @inject InstanceManager InstanceManager @inject UserManager UserManager <h1>Audit Log</h1> +@if (loadError is {} error) { + <p>@error</p> + return; +} + <Table TItem="AuditLogItem" Items="logItems"> <HeaderRow> <Column Class="text-end" MinWidth="200px">Time</Column> @@ -46,21 +51,25 @@ @code { - private CancellationTokenSource? initializationCancellationTokenSource; private ImmutableArray<AuditLogItem>? logItems; + private string? loadError; + private ImmutableDictionary<Guid, string>? userNamesByGuid; private ImmutableDictionary<Guid, string> instanceNamesByGuid = ImmutableDictionary<Guid, string>.Empty; protected override async Task OnInitializedAsync() { - initializationCancellationTokenSource = new CancellationTokenSource(); - var cancellationToken = initializationCancellationTokenSource.Token; - - try { - logItems = await AuditLogManager.GetMostRecentItems(50, cancellationToken); - userNamesByGuid = (await UserManager.GetAll(cancellationToken)).ToImmutableDictionary(static user => user.Guid, static user => user.Name); + var result = await AuditLogManager.GetMostRecentItems(await GetAuthenticatedUser(), 50, CancellationToken); + if (result) { + logItems = result.Value; + userNamesByGuid = (await UserManager.GetAll(CancellationToken)).ToImmutableDictionary(static user => user.Guid, static user => user.Name); instanceNamesByGuid = InstanceManager.GetAll().Values.ToImmutableDictionary(static instance => instance.InstanceGuid, static instance => instance.Configuration.InstanceName); - } finally { - initializationCancellationTokenSource.Dispose(); + } + else { + logItems = ImmutableArray<AuditLogItem>.Empty; + loadError = result.Error switch { + UserActionFailure.NotAuthorized => "You do not have permission to view the audit log.", + _ => "Unknown error." + }; } } @@ -72,10 +81,4 @@ }; } - protected override void OnDisposed() { - try { - initializationCancellationTokenSource?.Cancel(); - } catch (ObjectDisposedException) {} - } - } diff --git a/Web/Phantom.Web/Pages/Events.razor b/Web/Phantom.Web/Pages/Events.razor index 11f91a8..7ab5f21 100644 --- a/Web/Phantom.Web/Pages/Events.razor +++ b/Web/Phantom.Web/Pages/Events.razor @@ -13,6 +13,11 @@ <h1>Event Log</h1> +@if (loadError is {} error) { + <p>@error</p> + return; +} + <Table TItem="EventLogItem" Items="logItems"> <HeaderRow> <Column Class="text-end" MinWidth="200px">Time</Column> @@ -50,21 +55,25 @@ @code { - private CancellationTokenSource? initializationCancellationTokenSource; private ImmutableArray<EventLogItem>? logItems; + private string? loadError; + private ImmutableDictionary<Guid, string> agentNamesByGuid = ImmutableDictionary<Guid, string>.Empty; private ImmutableDictionary<Guid, string> instanceNamesByGuid = ImmutableDictionary<Guid, string>.Empty; protected override async Task OnInitializedAsync() { - initializationCancellationTokenSource = new CancellationTokenSource(); - var cancellationToken = initializationCancellationTokenSource.Token; - - try { - logItems = await EventLogManager.GetMostRecentItems(50, cancellationToken); + var result = await EventLogManager.GetMostRecentItems(await GetAuthenticatedUser(), 50, CancellationToken); + if (result) { + logItems = result.Value; agentNamesByGuid = AgentManager.GetAll().ToImmutableDictionary(static kvp => kvp.AgentGuid, static kvp => kvp.Configuration.AgentName); instanceNamesByGuid = InstanceManager.GetAll().Values.ToImmutableDictionary(static instance => instance.InstanceGuid, static instance => instance.Configuration.InstanceName); - } finally { - initializationCancellationTokenSource.Dispose(); + } + else { + logItems = ImmutableArray<EventLogItem>.Empty; + loadError = result.Error switch { + UserActionFailure.NotAuthorized => "You do not have permission to view the event log.", + _ => "Unknown error." + }; } } @@ -79,10 +88,4 @@ }; } - protected override void OnDisposed() { - try { - initializationCancellationTokenSource?.Cancel(); - } catch (ObjectDisposedException) {} - } - } diff --git a/Web/Phantom.Web/Pages/InstanceDetail.razor b/Web/Phantom.Web/Pages/InstanceDetail.razor index b232938..752ffae 100644 --- a/Web/Phantom.Web/Pages/InstanceDetail.razor +++ b/Web/Phantom.Web/Pages/InstanceDetail.razor @@ -1,9 +1,10 @@ @page "/instances/{InstanceGuid:guid}" @attribute [Authorize(Permission.ViewInstancesPolicy)] -@using Phantom.Common.Data.Instance @using Phantom.Common.Data.Replies @using Phantom.Common.Data.Web.Instance @using Phantom.Common.Data.Web.Users +@using Phantom.Utils.Result +@using Phantom.Common.Data.Instance @using Phantom.Web.Services.Instances @using Phantom.Web.Services.Authorization @inherits Phantom.Web.Components.PhantomComponent @@ -12,42 +13,42 @@ @if (Instance == null) { <h1>Instance Not Found</h1> <p>Return to <a href="instances">all instances</a>.</p> + return; } -else { - <div class="d-flex flex-row align-items-center gap-3 mb-3"> - <h1 class="mb-0">Instance: @Instance.Configuration.InstanceName</h1> - <span class="fs-4 text-muted">//</span> - <div class="mt-2"> - <InstanceStatusText Status="Instance.Status" /> - </div> - </div> - <div class="d-flex flex-row align-items-center gap-2"> - <PermissionView Permission="Permission.ControlInstances"> - <button type="button" class="btn btn-success" @onclick="LaunchInstance" disabled="@(isLaunchingInstance || !Instance.Status.CanLaunch())">Launch</button> - <button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#stop-instance" disabled="@(!Instance.Status.CanStop())">Stop...</button> - <span><!-- extra spacing --></span> - </PermissionView> - <PermissionView Permission="Permission.CreateInstances"> - <a href="instances/@InstanceGuid/edit" class="btn btn-warning ms-auto">Edit Configuration</a> - </PermissionView> - </div> - @if (lastError != null) { - <p class="text-danger mt-2">@lastError</p> - } - - <PermissionView Permission="Permission.ViewInstanceLogs"> - <InstanceLog InstanceGuid="InstanceGuid" /> - </PermissionView> +<div class="d-flex flex-row align-items-center gap-3 mb-3"> + <h1 class="mb-0">Instance: @Instance.Configuration.InstanceName</h1> + <span class="fs-4 text-muted">//</span> + <div class="mt-2"> + <InstanceStatusText Status="Instance.Status" /> + </div> +</div> +<div class="d-flex flex-row align-items-center gap-2"> <PermissionView Permission="Permission.ControlInstances"> - <div class="mb-3"> - <InstanceCommandInput AgentGuid="Instance.Configuration.AgentGuid" InstanceGuid="InstanceGuid" Disabled="@(!Instance.Status.CanSendCommand())" /> - </div> - - <InstanceStopDialog AgentGuid="Instance.Configuration.AgentGuid" InstanceGuid="InstanceGuid" ModalId="stop-instance" Disabled="@(!Instance.Status.CanStop())" /> + <button type="button" class="btn btn-success" @onclick="LaunchInstance" disabled="@(isLaunchingInstance || !Instance.Status.CanLaunch())">Launch</button> + <button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#stop-instance" disabled="@(!Instance.Status.CanStop())">Stop...</button> + <span><!-- extra spacing --></span> </PermissionView> + <PermissionView Permission="Permission.CreateInstances"> + <a href="instances/@InstanceGuid/edit" class="btn btn-warning ms-auto">Edit Configuration</a> + </PermissionView> +</div> +@if (lastError != null) { + <p class="text-danger mt-2">@lastError</p> } +<PermissionView Permission="Permission.ViewInstanceLogs"> + <InstanceLog InstanceGuid="InstanceGuid" /> +</PermissionView> + +<PermissionView Permission="Permission.ControlInstances"> + <div class="mb-3"> + <InstanceCommandInput AgentGuid="Instance.Configuration.AgentGuid" InstanceGuid="InstanceGuid" Disabled="@(!Instance.Status.CanSendCommand())" /> + </div> + + <InstanceStopDialog AgentGuid="Instance.Configuration.AgentGuid" InstanceGuid="InstanceGuid" ModalId="stop-instance" Disabled="@(!Instance.Status.CanStop())" /> +</PermissionView> + @code { [Parameter] @@ -73,20 +74,32 @@ else { lastError = null; try { - var loggedInUserGuid = await GetUserGuid(); - if (loggedInUserGuid == null || !await CheckPermission(Permission.ControlInstances)) { - lastError = "You do not have permission to launch instances."; - return; - } - if (Instance == null) { lastError = "Instance not found."; return; } - var result = await InstanceManager.LaunchInstance(loggedInUserGuid.Value, Instance.Configuration.AgentGuid, InstanceGuid, CancellationToken); - if (!result.Is(LaunchInstanceResult.LaunchInitiated)) { - lastError = result.Map(Messages.ToSentence, InstanceActionFailureExtensions.ToSentence); + var result = await InstanceManager.LaunchInstance(await GetAuthenticatedUser(), Instance.Configuration.AgentGuid, InstanceGuid, CancellationToken); + + switch (result.Variant()) { + case Ok<LaunchInstanceResult>(LaunchInstanceResult.LaunchInitiated): + break; + + case Ok<LaunchInstanceResult>(var launchInstanceResult): + lastError = launchInstanceResult.ToSentence(); + break; + + case Err<UserInstanceActionFailure>(OfInstanceActionFailure(var failure)): + lastError = failure.ToSentence(); + break; + + case Err<UserInstanceActionFailure>(OfUserActionFailure(UserActionFailure.NotAuthorized)): + lastError = "You do not have permission to launch this instance."; + break; + + default: + lastError = "Unknown error."; + break; } } finally { isLaunchingInstance = false; diff --git a/Web/Phantom.Web/Pages/Users.razor b/Web/Phantom.Web/Pages/Users.razor index 3dbbe0a..aeedf75 100644 --- a/Web/Phantom.Web/Pages/Users.razor +++ b/Web/Phantom.Web/Pages/Users.razor @@ -60,7 +60,7 @@ @code { - private Guid? me = Guid.Empty; + private Guid? me = null; private ImmutableArray<UserInfo>? allUsers; private ImmutableDictionary<Guid, RoleInfo> allRolesByGuid = ImmutableDictionary<Guid, RoleInfo>.Empty; private readonly Dictionary<Guid, string> userGuidToRoleDescription = new (); @@ -71,7 +71,7 @@ private UserDeleteDialog userDeleteDialog = null!; protected override async Task OnInitializedAsync() { - me = await GetUserGuid(); + me = (await GetAuthenticatedUser())?.Info.Guid; allUsers = (await UserManager.GetAll(CancellationToken)).Sort(static (a, b) => a.Name.CompareTo(b.Name)); allRolesByGuid = (await RoleManager.GetAll(CancellationToken)).ToImmutableDictionary(static role => role.Guid, static role => role); diff --git a/Web/Phantom.Web/Shared/InstanceAddOrEditForm.razor b/Web/Phantom.Web/Shared/InstanceAddOrEditForm.razor index 8913d78..97a676e 100644 --- a/Web/Phantom.Web/Shared/InstanceAddOrEditForm.razor +++ b/Web/Phantom.Web/Shared/InstanceAddOrEditForm.razor @@ -2,13 +2,14 @@ @using System.Collections.Immutable @using System.ComponentModel.DataAnnotations @using System.Diagnostics.CodeAnalysis -@using Phantom.Common.Data.Minecraft -@using Phantom.Common.Data.Replies -@using Phantom.Common.Data.Web.Agent @using Phantom.Common.Data.Web.Instance @using Phantom.Common.Data.Web.Minecraft @using Phantom.Common.Data.Web.Users @using Phantom.Common.Messages.Web.ToController +@using Phantom.Utils.Result +@using Phantom.Common.Data.Replies +@using Phantom.Common.Data.Web.Agent +@using Phantom.Common.Data.Minecraft @using Phantom.Common.Data.Java @using Phantom.Common.Data @using Phantom.Common.Data.Instance @@ -29,13 +30,14 @@ @{ static RenderFragment GetAgentOption(Agent agent) { var configuration = agent.Configuration; - return @<option value="@agent.AgentGuid"> - @configuration.AgentName - • - @(agent.Stats?.RunningInstanceCount.ToString() ?? "?")/@(configuration.MaxInstances) @(configuration.MaxInstances == 1 ? "Instance" : "Instances") - • - @(agent.Stats?.RunningInstanceMemory.InMegabytes.ToString() ?? "?")/@(configuration.MaxMemory.InMegabytes) MB RAM - </option>; + return + @<option value="@agent.AgentGuid"> + @configuration.AgentName + • + @(agent.Stats?.RunningInstanceCount.ToString() ?? "?")/@(configuration.MaxInstances) @(configuration.MaxInstances == 1 ? "Instance" : "Instances") + • + @(agent.Stats?.RunningInstanceMemory.InMegabytes.ToString() ?? "?")/@(configuration.MaxMemory.InMegabytes) MB RAM + </option>; } } @if (EditedInstance == null) { @@ -168,12 +170,12 @@ [Parameter, EditorRequired] public Instance? EditedInstance { get; init; } - + private ConfigureInstanceFormModel form = null!; private ImmutableDictionary<Guid, Agent> allAgentsByGuid = ImmutableDictionary<Guid, Agent>.Empty; private ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>> allAgentJavaRuntimes = ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>.Empty; - + private MinecraftVersionType minecraftVersionType = MinecraftVersionType.Release; private ImmutableArray<MinecraftVersion> allMinecraftVersions = ImmutableArray<MinecraftVersion>.Empty; private ImmutableArray<MinecraftVersion> availableMinecraftVersions = ImmutableArray<MinecraftVersion>.Empty; @@ -278,7 +280,7 @@ protected override async Task OnInitializedAsync() { var agentJavaRuntimesTask = ControllerConnection.Send<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(new GetAgentJavaRuntimesMessage(), TimeSpan.FromSeconds(30)); var minecraftVersionsTask = ControllerConnection.Send<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(new GetMinecraftVersionsMessage(), TimeSpan.FromSeconds(30)); - + allAgentsByGuid = AgentManager.ToDictionaryByGuid(); allAgentJavaRuntimes = await agentJavaRuntimesTask; allMinecraftVersions = await minecraftVersionsTask; @@ -294,7 +296,7 @@ form.MemoryUnits = configuration.MemoryAllocation.RawValue; form.JavaRuntimeGuid = configuration.JavaRuntimeGuid; form.JvmArguments = JvmArgumentsHelper.Join(configuration.JvmArguments); - + minecraftVersionType = allMinecraftVersions.FirstOrDefault(version => version.Id == configuration.MinecraftVersion)?.Type ?? minecraftVersionType; } @@ -303,7 +305,7 @@ form.EditContext.RevalidateWhenFieldChanges(tracked: nameof(ConfigureInstanceFormModel.SelectedAgentGuid), revalidated: nameof(ConfigureInstanceFormModel.ServerPort)); form.EditContext.RevalidateWhenFieldChanges(tracked: nameof(ConfigureInstanceFormModel.SelectedAgentGuid), revalidated: nameof(ConfigureInstanceFormModel.RconPort)); form.EditContext.RevalidateWhenFieldChanges(tracked: nameof(ConfigureInstanceFormModel.ServerPort), revalidated: nameof(ConfigureInstanceFormModel.RconPort)); - + SetMinecraftVersionType(minecraftVersionType); } @@ -324,12 +326,6 @@ await form.SubmitModel.StartSubmitting(); - var loggedInUserGuid = await GetUserGuid(); - if (loggedInUserGuid == null || !await CheckPermission(Permission.CreateInstances)) { - form.SubmitModel.StopSubmitting("You do not have permission to edit instances."); - return; - } - var instanceGuid = EditedInstance?.InstanceGuid ?? Guid.NewGuid(); var instanceConfiguration = new InstanceConfiguration( EditedInstance?.Configuration.AgentGuid ?? selectedAgent.AgentGuid, @@ -343,12 +339,28 @@ JvmArgumentsHelper.Split(form.JvmArguments) ); - var result = await InstanceManager.CreateOrUpdateInstance(loggedInUserGuid.Value, instanceGuid, instanceConfiguration, CancellationToken); - if (result.Is(CreateOrUpdateInstanceResult.Success)) { - await Navigation.NavigateTo("instances/" + instanceGuid); - } - else { - form.SubmitModel.StopSubmitting(result.Map(CreateOrUpdateInstanceResultExtensions.ToSentence, InstanceActionFailureExtensions.ToSentence)); + var result = await InstanceManager.CreateOrUpdateInstance(await GetAuthenticatedUser(), instanceGuid, instanceConfiguration, CancellationToken); + + switch (result.Variant()) { + case Ok<CreateOrUpdateInstanceResult>(CreateOrUpdateInstanceResult.Success): + await Navigation.NavigateTo("instances/" + instanceGuid); + break; + + case Ok<CreateOrUpdateInstanceResult>(var createOrUpdateInstanceResult): + form.SubmitModel.StopSubmitting(createOrUpdateInstanceResult.ToSentence()); + break; + + case Err<UserInstanceActionFailure>(OfInstanceActionFailure(var failure)): + form.SubmitModel.StopSubmitting(failure.ToSentence()); + break; + + case Err<UserInstanceActionFailure>(OfUserActionFailure(UserActionFailure.NotAuthorized)): + form.SubmitModel.StopSubmitting("You do not have permission to create or edit instances."); + break; + + default: + form.SubmitModel.StopSubmitting("Unknown error."); + break; } } diff --git a/Web/Phantom.Web/Shared/InstanceCommandInput.razor b/Web/Phantom.Web/Shared/InstanceCommandInput.razor index 1c6b0ab..fd6266b 100644 --- a/Web/Phantom.Web/Shared/InstanceCommandInput.razor +++ b/Web/Phantom.Web/Shared/InstanceCommandInput.razor @@ -1,6 +1,7 @@ -@using Phantom.Web.Services.Instances +@using Phantom.Common.Data.Replies @using Phantom.Common.Data.Web.Users -@using Phantom.Common.Data.Replies +@using Phantom.Utils.Result +@using Phantom.Web.Services.Instances @inherits Phantom.Web.Components.PhantomComponent @inject InstanceManager InstanceManager @@ -18,7 +19,7 @@ [Parameter, EditorRequired] public Guid AgentGuid { get; set; } - + [Parameter, EditorRequired] public Guid InstanceGuid { get; set; } @@ -36,19 +37,29 @@ private async Task ExecuteCommand(EditContext context) { await form.SubmitModel.StartSubmitting(); - var loggedInUserGuid = await GetUserGuid(); - if (loggedInUserGuid == null || !await CheckPermission(Permission.ControlInstances)) { - form.SubmitModel.StopSubmitting("You do not have permission to execute commands."); - return; - } + var result = await InstanceManager.SendCommandToInstance(await GetAuthenticatedUser(), AgentGuid, InstanceGuid, form.Command, CancellationToken); - var result = await InstanceManager.SendCommandToInstance(loggedInUserGuid.Value, AgentGuid, InstanceGuid, form.Command, CancellationToken); - if (result.Is(SendCommandToInstanceResult.Success)) { - form.Command = string.Empty; - form.SubmitModel.StopSubmitting(); - } - else { - form.SubmitModel.StopSubmitting(result.Map(Messages.ToSentence, InstanceActionFailureExtensions.ToSentence)); + switch (result.Variant()) { + case Ok<SendCommandToInstanceResult>(SendCommandToInstanceResult.Success): + form.Command = string.Empty; + form.SubmitModel.StopSubmitting(); + break; + + case Ok<SendCommandToInstanceResult>(var sendCommandToInstanceResult): + form.SubmitModel.StopSubmitting(sendCommandToInstanceResult.ToSentence()); + break; + + case Err<UserInstanceActionFailure>(OfInstanceActionFailure(var failure)): + form.SubmitModel.StopSubmitting(failure.ToSentence()); + break; + + case Err<UserInstanceActionFailure>(OfUserActionFailure(UserActionFailure.NotAuthorized)): + form.SubmitModel.StopSubmitting("You do not have permission to send commands to this instance."); + break; + + default: + form.SubmitModel.StopSubmitting("Unknown error."); + break; } StateHasChanged(); diff --git a/Web/Phantom.Web/Shared/InstanceStopDialog.razor b/Web/Phantom.Web/Shared/InstanceStopDialog.razor index f2ff23c..761888a 100644 --- a/Web/Phantom.Web/Shared/InstanceStopDialog.razor +++ b/Web/Phantom.Web/Shared/InstanceStopDialog.razor @@ -1,8 +1,9 @@ -@using Phantom.Web.Services.Instances -@using System.ComponentModel.DataAnnotations +@using Phantom.Common.Data.Replies @using Phantom.Common.Data.Web.Users +@using Phantom.Utils.Result +@using Phantom.Web.Services.Instances +@using System.ComponentModel.DataAnnotations @using Phantom.Common.Data.Minecraft -@using Phantom.Common.Data.Replies @inherits Phantom.Web.Components.PhantomComponent @inject IJSRuntime Js; @inject InstanceManager InstanceManager; @@ -33,7 +34,7 @@ [Parameter, EditorRequired] public Guid AgentGuid { get; init; } - + [Parameter, EditorRequired] public Guid InstanceGuid { get; init; } @@ -53,19 +54,29 @@ private async Task StopInstance(EditContext context) { await form.SubmitModel.StartSubmitting(); - var loggedInUserGuid = await GetUserGuid(); - if (loggedInUserGuid == null || !await CheckPermission(Permission.ControlInstances)) { - form.SubmitModel.StopSubmitting("You do not have permission to stop instances."); - return; - } + var result = await InstanceManager.StopInstance(await GetAuthenticatedUser(), AgentGuid, InstanceGuid, new MinecraftStopStrategy(form.StopInSeconds), CancellationToken); - var result = await InstanceManager.StopInstance(loggedInUserGuid.Value, AgentGuid, InstanceGuid, new MinecraftStopStrategy(form.StopInSeconds), CancellationToken); - if (result.Is(StopInstanceResult.StopInitiated)) { - await Js.InvokeVoidAsync("closeModal", ModalId); - form.SubmitModel.StopSubmitting(); - } - else { - form.SubmitModel.StopSubmitting(result.Map(Messages.ToSentence, InstanceActionFailureExtensions.ToSentence)); + switch (result.Variant()) { + case Ok<StopInstanceResult>(StopInstanceResult.StopInitiated): + await Js.InvokeVoidAsync("closeModal", ModalId); + form.SubmitModel.StopSubmitting(); + break; + + case Ok<StopInstanceResult>(var stopInstanceResult): + form.SubmitModel.StopSubmitting(stopInstanceResult.ToSentence()); + break; + + case Err<UserInstanceActionFailure>(OfInstanceActionFailure(var failure)): + form.SubmitModel.StopSubmitting(failure.ToSentence()); + break; + + case Err<UserInstanceActionFailure>(OfUserActionFailure(UserActionFailure.NotAuthorized)): + form.SubmitModel.StopSubmitting("You do not have permission to stop this instance."); + break; + + default: + form.SubmitModel.StopSubmitting("Unknown error."); + break; } } diff --git a/Web/Phantom.Web/Shared/UserAddDialog.razor b/Web/Phantom.Web/Shared/UserAddDialog.razor index 264b678..79e0a6a 100644 --- a/Web/Phantom.Web/Shared/UserAddDialog.razor +++ b/Web/Phantom.Web/Shared/UserAddDialog.razor @@ -1,5 +1,6 @@ @using Phantom.Common.Data.Web.Users @using Phantom.Common.Data.Web.Users.CreateUserResults +@using Phantom.Utils.Result @using Phantom.Web.Services.Users @using System.ComponentModel.DataAnnotations @inherits Phantom.Web.Components.PhantomComponent @@ -39,7 +40,7 @@ [Parameter] public EventCallback<UserInfo> UserAdded { get; set; } - private readonly AddUserFormModel form = new(); + private readonly AddUserFormModel form = new (); private sealed class AddUserFormModel : FormModel { [Required] @@ -52,23 +53,23 @@ private async Task AddUser(EditContext context) { await form.SubmitModel.StartSubmitting(); - var loggedInUserGuid = await GetUserGuid(); - if (loggedInUserGuid == null || !await CheckPermission(Permission.EditUsers)) { - form.SubmitModel.StopSubmitting("You do not have permission to add users."); - return; - } + var result = await UserManager.Create(await GetAuthenticatedUser(), form.Username, form.Password, CancellationToken); - switch (await UserManager.Create(loggedInUserGuid.Value, form.Username, form.Password, CancellationToken)) { - case Success success: + switch (result.Variant()) { + case Ok<CreateUserResult>(Success success): await UserAdded.InvokeAsync(success.User); await Js.InvokeVoidAsync("closeModal", ModalId); form.SubmitModel.StopSubmitting(); break; - case CreationFailed fail: + case Ok<CreateUserResult>(CreationFailed fail): form.SubmitModel.StopSubmitting(fail.Error.ToSentences("\n")); break; + case Err<UserActionFailure>(UserActionFailure.NotAuthorized): + form.SubmitModel.StopSubmitting("You do not have permission to add users."); + break; + default: form.SubmitModel.StopSubmitting("Unknown error."); break; diff --git a/Web/Phantom.Web/Shared/UserDeleteDialog.razor b/Web/Phantom.Web/Shared/UserDeleteDialog.razor index 78440e9..b87dde4 100644 --- a/Web/Phantom.Web/Shared/UserDeleteDialog.razor +++ b/Web/Phantom.Web/Shared/UserDeleteDialog.razor @@ -1,4 +1,5 @@ @using Phantom.Common.Data.Web.Users +@using Phantom.Web.Services.Authentication @using Phantom.Web.Services.Users @inherits UserEditDialogBase @inject UserManager UserManager @@ -17,8 +18,13 @@ @code { - protected override async Task DoEdit(Guid loggedInUserGuid, UserInfo user) { - switch (await UserManager.DeleteByGuid(loggedInUserGuid, user.Guid, CancellationToken)) { + protected override async Task<UserActionFailure?> DoEdit(AuthenticatedUser? authenticatedUser, UserInfo editedUser) { + var result = await UserManager.DeleteByGuid(authenticatedUser, editedUser.Guid, CancellationToken); + if (!result) { + return result.Error; + } + + switch (result.Value) { case DeleteUserResult.Deleted: case DeleteUserResult.NotFound: await OnEditSuccess(); @@ -28,6 +34,8 @@ OnEditFailure("Could not delete user."); break; } + + return null; } } diff --git a/Web/Phantom.Web/Shared/UserEditDialogBase.cs b/Web/Phantom.Web/Shared/UserEditDialogBase.cs index be676f2..c2a564c 100644 --- a/Web/Phantom.Web/Shared/UserEditDialogBase.cs +++ b/Web/Phantom.Web/Shared/UserEditDialogBase.cs @@ -3,6 +3,7 @@ using Microsoft.JSInterop; using Phantom.Common.Data.Web.Users; using Phantom.Web.Components; using Phantom.Web.Components.Forms; +using Phantom.Web.Services.Authentication; namespace Phantom.Web.Shared; @@ -16,7 +17,7 @@ public abstract class UserEditDialogBase : PhantomComponent { [Parameter] public EventCallback<UserInfo> UserModified { get; set; } - protected readonly FormButtonSubmit.SubmitModel SubmitModel = new(); + protected readonly FormButtonSubmit.SubmitModel SubmitModel = new (); private UserInfo? EditedUser { get; set; } = null; protected string EditedUserName { get; private set; } = string.Empty; @@ -41,19 +42,26 @@ public abstract class UserEditDialogBase : PhantomComponent { protected async Task Submit() { await SubmitModel.StartSubmitting(); - var loggedInUserGuid = await GetUserGuid(); - if (loggedInUserGuid == null || !await CheckPermission(Permission.EditUsers)) { - SubmitModel.StopSubmitting("You do not have permission to edit users."); - } - else if (EditedUser == null) { + if (EditedUser == null) { SubmitModel.StopSubmitting("Invalid user."); + return; } - else { - await DoEdit(loggedInUserGuid.Value, EditedUser); + + switch (await DoEdit(await GetAuthenticatedUser(), EditedUser)) { + case null: + break; + + case UserActionFailure.NotAuthorized: + SubmitModel.StopSubmitting("You do not have permission to edit users."); + break; + + default: + SubmitModel.StopSubmitting("Unknown error."); + break; } } - - protected abstract Task DoEdit(Guid loggedInUserGuid, UserInfo user); + + protected abstract Task<UserActionFailure?> DoEdit(AuthenticatedUser? authenticatedUser, UserInfo editedUser); protected async Task OnEditSuccess() { await UserModified.InvokeAsync(EditedUser); @@ -61,7 +69,7 @@ public abstract class UserEditDialogBase : PhantomComponent { SubmitModel.StopSubmitting(); OnClosed(); } - + protected void OnEditFailure(string message) { SubmitModel.StopSubmitting(message); } diff --git a/Web/Phantom.Web/Shared/UserRolesDialog.razor b/Web/Phantom.Web/Shared/UserRolesDialog.razor index 046df3a..a89cab3 100644 --- a/Web/Phantom.Web/Shared/UserRolesDialog.razor +++ b/Web/Phantom.Web/Shared/UserRolesDialog.razor @@ -1,5 +1,6 @@ @using System.Collections.Immutable @using Phantom.Common.Data.Web.Users +@using Phantom.Web.Services.Authentication @using Phantom.Web.Services.Users @inherits UserEditDialogBase @inject RoleManager RoleManager @@ -36,8 +37,8 @@ this.items = allRoles.Select(role => new RoleItem(role, currentRoleGuids.Contains(role.Guid))).ToList(); } - protected override async Task DoEdit(Guid loggedInUserGuid, UserInfo user) { - var currentRoleGuids = await UserRoleManager.GetUserRoles(user.Guid, CancellationToken); + protected override async Task<UserActionFailure?> DoEdit(AuthenticatedUser? authenticatedUser, UserInfo editedUser) { + var currentRoleGuids = await UserRoleManager.GetUserRoles(editedUser.Guid, CancellationToken); var addToRoleGuids = ImmutableHashSet.CreateBuilder<Guid>(); var removeFromRoleGuids = ImmutableHashSet.CreateBuilder<Guid>(); @@ -56,18 +57,21 @@ } } - await DoChangeUserRoles(user, loggedInUserGuid, addToRoleGuids.ToImmutable(), removeFromRoleGuids.ToImmutable()); + return await DoChangeUserRoles(authenticatedUser, editedUser, addToRoleGuids.ToImmutable(), removeFromRoleGuids.ToImmutable()); } - private async Task DoChangeUserRoles(UserInfo user, Guid loggedInUserGuid, ImmutableHashSet<Guid> addToRoleGuids, ImmutableHashSet<Guid> removeFromRoleGuids) { - var result = await UserRoleManager.ChangeUserRoles(loggedInUserGuid, user.Guid, addToRoleGuids, removeFromRoleGuids, CancellationToken); + private async Task<UserActionFailure?> DoChangeUserRoles(AuthenticatedUser? authenticatedUser, UserInfo editedUser, ImmutableHashSet<Guid> addToRoleGuids, ImmutableHashSet<Guid> removeFromRoleGuids) { + var result = await UserRoleManager.ChangeUserRoles(authenticatedUser, editedUser.Guid, addToRoleGuids, removeFromRoleGuids, CancellationToken); + if (!result) { + return result.Error; + } - var failedToAdd = addToRoleGuids.Except(result.AddedToRoleGuids); - var failedToRemove = removeFromRoleGuids.Except(result.RemovedFromRoleGuids); + var failedToAdd = addToRoleGuids.Except(result.Value.AddedToRoleGuids); + var failedToRemove = removeFromRoleGuids.Except(result.Value.RemovedFromRoleGuids); if (failedToAdd.IsEmpty && failedToRemove.IsEmpty) { await OnEditSuccess(); - return; + return null; } var errors = new List<string>(); @@ -81,6 +85,7 @@ } OnEditFailure(string.Join("\n", errors)); + return null; } private string GetRoleName(Guid roleGuid) {