mirror of
https://github.com/chylex/Minecraft-Phantom-Panel.git
synced 2024-11-25 07:42:58 +01:00
Compare commits
9 Commits
599177409e
...
1855ff8062
Author | SHA1 | Date | |
---|---|---|---|
1855ff8062 | |||
21c90cb7c2 | |||
7b68b5d50d | |||
2d70d2b7d1 | |||
8c623171f3 | |||
62f8c685f0 | |||
4a110db078 | |||
f683a1f700 | |||
3ffb37529c |
@ -3,7 +3,7 @@
|
|||||||
"isRoot": true,
|
"isRoot": true,
|
||||||
"tools": {
|
"tools": {
|
||||||
"dotnet-ef": {
|
"dotnet-ef": {
|
||||||
"version": "7.0.0-rc.1.22426.7",
|
"version": "8.0.3",
|
||||||
"commands": [
|
"commands": [
|
||||||
"dotnet-ef"
|
"dotnet-ef"
|
||||||
]
|
]
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using MemoryPack;
|
using System.Collections.Immutable;
|
||||||
|
using MemoryPack;
|
||||||
|
|
||||||
namespace Phantom.Common.Data.Web.Users;
|
namespace Phantom.Common.Data.Web.Users;
|
||||||
|
|
||||||
@ -6,5 +7,18 @@ namespace Phantom.Common.Data.Web.Users;
|
|||||||
public sealed partial record AuthenticatedUserInfo(
|
public sealed partial record AuthenticatedUserInfo(
|
||||||
[property: MemoryPackOrder(0)] Guid Guid,
|
[property: MemoryPackOrder(0)] Guid Guid,
|
||||||
[property: MemoryPackOrder(1)] string Name,
|
[property: MemoryPackOrder(1)] string Name,
|
||||||
[property: MemoryPackOrder(2)] PermissionSet Permissions
|
[property: MemoryPackOrder(2)] PermissionSet Permissions,
|
||||||
);
|
[property: MemoryPackOrder(3)] ImmutableHashSet<Guid> ManagedAgentGuids
|
||||||
|
) {
|
||||||
|
public bool CheckPermission(Permission permission) {
|
||||||
|
return Permissions.Check(permission);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasAccessToAgent(Guid agentGuid) {
|
||||||
|
return ManagedAgentGuids.Contains(agentGuid) || Permissions.Check(Permission.ManageAllAgents);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImmutableHashSet<Guid> FilterAccessibleAgentGuids(ImmutableHashSet<Guid> agentGuids) {
|
||||||
|
return Permissions.Check(Permission.ManageAllAgents) ? agentGuids : agentGuids.Intersect(ManagedAgentGuids);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -6,5 +6,5 @@ namespace Phantom.Common.Data.Web.Users;
|
|||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||||
public sealed partial record LogInSuccess(
|
public sealed partial record LogInSuccess(
|
||||||
[property: MemoryPackOrder(0)] AuthenticatedUserInfo UserInfo,
|
[property: MemoryPackOrder(0)] AuthenticatedUserInfo UserInfo,
|
||||||
[property: MemoryPackOrder(1)] ImmutableArray<byte> Token
|
[property: MemoryPackOrder(1)] ImmutableArray<byte> AuthToken
|
||||||
);
|
);
|
||||||
|
@ -14,6 +14,9 @@ public sealed record Permission(string Id, Permission? Parent) {
|
|||||||
return Register(id, this);
|
return Register(id, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public const string ManageAllAgentsPolicy = "Agents.ManageAll";
|
||||||
|
public static readonly Permission ManageAllAgents = Register(ManageAllAgentsPolicy);
|
||||||
|
|
||||||
public const string ViewInstancesPolicy = "Instances.View";
|
public const string ViewInstancesPolicy = "Instances.View";
|
||||||
public static readonly Permission ViewInstances = Register(ViewInstancesPolicy);
|
public static readonly Permission ViewInstances = Register(ViewInstancesPolicy);
|
||||||
|
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
namespace Phantom.Common.Data.Web.Users;
|
||||||
|
|
||||||
|
public enum UserActionFailure {
|
||||||
|
NotAuthorized
|
||||||
|
}
|
@ -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;
|
@ -1,5 +1,6 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using MemoryPack;
|
using MemoryPack;
|
||||||
|
using Phantom.Utils.Result;
|
||||||
|
|
||||||
namespace Phantom.Common.Data;
|
namespace Phantom.Common.Data;
|
||||||
|
|
||||||
@ -33,10 +34,18 @@ public sealed partial class Result<TValue, TError> {
|
|||||||
return hasValue && EqualityComparer<TValue>.Default.Equals(value, expectedValue);
|
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!);
|
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) {
|
public static implicit operator Result<TValue, TError>(TValue value) {
|
||||||
return new Result<TValue, TError>(hasValue: true, value, default);
|
return new Result<TValue, TError>(hasValue: true, value, default);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using MemoryPack;
|
using MemoryPack;
|
||||||
|
using Phantom.Common.Data;
|
||||||
using Phantom.Common.Data.Web.Users;
|
using Phantom.Common.Data.Web.Users;
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
|
|
||||||
@ -7,8 +8,8 @@ namespace Phantom.Common.Messages.Web.ToController;
|
|||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||||
public sealed partial record ChangeUserRolesMessage(
|
public sealed partial record ChangeUserRolesMessage(
|
||||||
[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
|
[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
|
||||||
[property: MemoryPackOrder(1)] Guid SubjectUserGuid,
|
[property: MemoryPackOrder(1)] Guid SubjectUserGuid,
|
||||||
[property: MemoryPackOrder(2)] ImmutableHashSet<Guid> AddToRoleGuids,
|
[property: MemoryPackOrder(2)] ImmutableHashSet<Guid> AddToRoleGuids,
|
||||||
[property: MemoryPackOrder(3)] ImmutableHashSet<Guid> RemoveFromRoleGuids
|
[property: MemoryPackOrder(3)] ImmutableHashSet<Guid> RemoveFromRoleGuids
|
||||||
) : IMessageToController, ICanReply<ChangeUserRolesResult>;
|
) : IMessageToController, ICanReply<Result<ChangeUserRolesResult, UserActionFailure>>;
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
using MemoryPack;
|
using System.Collections.Immutable;
|
||||||
|
using MemoryPack;
|
||||||
using Phantom.Common.Data;
|
using Phantom.Common.Data;
|
||||||
using Phantom.Common.Data.Instance;
|
using Phantom.Common.Data.Instance;
|
||||||
using Phantom.Common.Data.Replies;
|
|
||||||
using Phantom.Common.Data.Web.Instance;
|
using Phantom.Common.Data.Web.Instance;
|
||||||
|
using Phantom.Common.Data.Web.Users;
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
|
|
||||||
namespace Phantom.Common.Messages.Web.ToController;
|
namespace Phantom.Common.Messages.Web.ToController;
|
||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||||
public sealed partial record CreateOrUpdateInstanceMessage(
|
public sealed partial record CreateOrUpdateInstanceMessage(
|
||||||
[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
|
[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
|
||||||
[property: MemoryPackOrder(1)] Guid InstanceGuid,
|
[property: MemoryPackOrder(1)] Guid InstanceGuid,
|
||||||
[property: MemoryPackOrder(2)] InstanceConfiguration Configuration
|
[property: MemoryPackOrder(2)] InstanceConfiguration Configuration
|
||||||
) : IMessageToController, ICanReply<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>;
|
) : IMessageToController, ICanReply<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>;
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
using MemoryPack;
|
using System.Collections.Immutable;
|
||||||
|
using MemoryPack;
|
||||||
|
using Phantom.Common.Data;
|
||||||
using Phantom.Common.Data.Web.Users;
|
using Phantom.Common.Data.Web.Users;
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
|
|
||||||
@ -6,7 +8,7 @@ namespace Phantom.Common.Messages.Web.ToController;
|
|||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||||
public sealed partial record CreateUserMessage(
|
public sealed partial record CreateUserMessage(
|
||||||
[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
|
[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
|
||||||
[property: MemoryPackOrder(1)] string Username,
|
[property: MemoryPackOrder(1)] string Username,
|
||||||
[property: MemoryPackOrder(2)] string Password
|
[property: MemoryPackOrder(2)] string Password
|
||||||
) : IMessageToController, ICanReply<CreateUserResult>;
|
) : IMessageToController, ICanReply<Result<CreateUserResult, UserActionFailure>>;
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
using MemoryPack;
|
using System.Collections.Immutable;
|
||||||
|
using MemoryPack;
|
||||||
|
using Phantom.Common.Data;
|
||||||
using Phantom.Common.Data.Web.Users;
|
using Phantom.Common.Data.Web.Users;
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
|
|
||||||
@ -6,6 +8,6 @@ namespace Phantom.Common.Messages.Web.ToController;
|
|||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||||
public sealed partial record DeleteUserMessage(
|
public sealed partial record DeleteUserMessage(
|
||||||
[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
|
[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
|
||||||
[property: MemoryPackOrder(1)] Guid SubjectUserGuid
|
[property: MemoryPackOrder(1)] Guid SubjectUserGuid
|
||||||
) : IMessageToController, ICanReply<DeleteUserResult>;
|
) : IMessageToController, ICanReply<Result<DeleteUserResult, UserActionFailure>>;
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using MemoryPack;
|
using MemoryPack;
|
||||||
|
using Phantom.Common.Data;
|
||||||
using Phantom.Common.Data.Web.AuditLog;
|
using Phantom.Common.Data.Web.AuditLog;
|
||||||
|
using Phantom.Common.Data.Web.Users;
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
|
|
||||||
namespace Phantom.Common.Messages.Web.ToController;
|
namespace Phantom.Common.Messages.Web.ToController;
|
||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||||
public sealed partial record GetAuditLogMessage(
|
public sealed partial record GetAuditLogMessage(
|
||||||
[property: MemoryPackOrder(0)] int Count
|
[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
|
||||||
) : IMessageToController, ICanReply<ImmutableArray<AuditLogItem>>;
|
[property: MemoryPackOrder(1)] int Count
|
||||||
|
) : IMessageToController, ICanReply<Result<ImmutableArray<AuditLogItem>, UserActionFailure>>;
|
||||||
|
@ -9,5 +9,5 @@ namespace Phantom.Common.Messages.Web.ToController;
|
|||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||||
public sealed partial record GetAuthenticatedUser(
|
public sealed partial record GetAuthenticatedUser(
|
||||||
[property: MemoryPackOrder(0)] Guid UserGuid,
|
[property: MemoryPackOrder(0)] Guid UserGuid,
|
||||||
[property: MemoryPackOrder(1)] ImmutableArray<byte> SessionToken
|
[property: MemoryPackOrder(1)] ImmutableArray<byte> AuthToken
|
||||||
) : IMessageToController, ICanReply<Optional<AuthenticatedUserInfo>>;
|
) : IMessageToController, ICanReply<Optional<AuthenticatedUserInfo>>;
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using MemoryPack;
|
using MemoryPack;
|
||||||
|
using Phantom.Common.Data;
|
||||||
using Phantom.Common.Data.Web.EventLog;
|
using Phantom.Common.Data.Web.EventLog;
|
||||||
|
using Phantom.Common.Data.Web.Users;
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
|
|
||||||
namespace Phantom.Common.Messages.Web.ToController;
|
namespace Phantom.Common.Messages.Web.ToController;
|
||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||||
public sealed partial record GetEventLogMessage(
|
public sealed partial record GetEventLogMessage(
|
||||||
[property: MemoryPackOrder(0)] int Count
|
[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
|
||||||
) : IMessageToController, ICanReply<ImmutableArray<EventLogItem>>;
|
[property: MemoryPackOrder(1)] int Count
|
||||||
|
) : IMessageToController, ICanReply<Result<ImmutableArray<EventLogItem>, UserActionFailure>>;
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
using MemoryPack;
|
using System.Collections.Immutable;
|
||||||
|
using MemoryPack;
|
||||||
using Phantom.Common.Data;
|
using Phantom.Common.Data;
|
||||||
using Phantom.Common.Data.Replies;
|
using Phantom.Common.Data.Replies;
|
||||||
|
using Phantom.Common.Data.Web.Users;
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
|
|
||||||
namespace Phantom.Common.Messages.Web.ToController;
|
namespace Phantom.Common.Messages.Web.ToController;
|
||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||||
public sealed partial record LaunchInstanceMessage(
|
public sealed partial record LaunchInstanceMessage(
|
||||||
[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
|
[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
|
||||||
[property: MemoryPackOrder(1)] Guid AgentGuid,
|
[property: MemoryPackOrder(1)] Guid AgentGuid,
|
||||||
[property: MemoryPackOrder(2)] Guid InstanceGuid
|
[property: MemoryPackOrder(2)] Guid InstanceGuid
|
||||||
) : IMessageToController, ICanReply<Result<LaunchInstanceResult, InstanceActionFailure>>;
|
) : IMessageToController, ICanReply<Result<LaunchInstanceResult, UserInstanceActionFailure>>;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using MemoryPack;
|
using MemoryPack;
|
||||||
|
using Phantom.Common.Data;
|
||||||
using Phantom.Common.Data.Web.Users;
|
using Phantom.Common.Data.Web.Users;
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
|
|
||||||
@ -8,4 +9,4 @@ namespace Phantom.Common.Messages.Web.ToController;
|
|||||||
public sealed partial record LogInMessage(
|
public sealed partial record LogInMessage(
|
||||||
[property: MemoryPackOrder(0)] string Username,
|
[property: MemoryPackOrder(0)] string Username,
|
||||||
[property: MemoryPackOrder(1)] string Password
|
[property: MemoryPackOrder(1)] string Password
|
||||||
) : IMessageToController, ICanReply<LogInSuccess?>;
|
) : IMessageToController, ICanReply<Optional<LogInSuccess>>;
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
using MemoryPack;
|
using System.Collections.Immutable;
|
||||||
|
using MemoryPack;
|
||||||
using Phantom.Common.Data;
|
using Phantom.Common.Data;
|
||||||
using Phantom.Common.Data.Replies;
|
using Phantom.Common.Data.Replies;
|
||||||
|
using Phantom.Common.Data.Web.Users;
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
|
|
||||||
namespace Phantom.Common.Messages.Web.ToController;
|
namespace Phantom.Common.Messages.Web.ToController;
|
||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||||
public sealed partial record SendCommandToInstanceMessage(
|
public sealed partial record SendCommandToInstanceMessage(
|
||||||
[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
|
[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
|
||||||
[property: MemoryPackOrder(1)] Guid AgentGuid,
|
[property: MemoryPackOrder(1)] Guid AgentGuid,
|
||||||
[property: MemoryPackOrder(2)] Guid InstanceGuid,
|
[property: MemoryPackOrder(2)] Guid InstanceGuid,
|
||||||
[property: MemoryPackOrder(3)] string Command
|
[property: MemoryPackOrder(3)] string Command
|
||||||
) : IMessageToController, ICanReply<Result<SendCommandToInstanceResult, InstanceActionFailure>>;
|
) : IMessageToController, ICanReply<Result<SendCommandToInstanceResult, UserInstanceActionFailure>>;
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
using MemoryPack;
|
using System.Collections.Immutable;
|
||||||
|
using MemoryPack;
|
||||||
using Phantom.Common.Data;
|
using Phantom.Common.Data;
|
||||||
using Phantom.Common.Data.Minecraft;
|
using Phantom.Common.Data.Minecraft;
|
||||||
using Phantom.Common.Data.Replies;
|
using Phantom.Common.Data.Replies;
|
||||||
|
using Phantom.Common.Data.Web.Users;
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
|
|
||||||
namespace Phantom.Common.Messages.Web.ToController;
|
namespace Phantom.Common.Messages.Web.ToController;
|
||||||
|
|
||||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||||
public sealed partial record StopInstanceMessage(
|
public sealed partial record StopInstanceMessage(
|
||||||
[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
|
[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
|
||||||
[property: MemoryPackOrder(1)] Guid AgentGuid,
|
[property: MemoryPackOrder(1)] Guid AgentGuid,
|
||||||
[property: MemoryPackOrder(2)] Guid InstanceGuid,
|
[property: MemoryPackOrder(2)] Guid InstanceGuid,
|
||||||
[property: MemoryPackOrder(3)] MinecraftStopStrategy StopStrategy
|
[property: MemoryPackOrder(3)] MinecraftStopStrategy StopStrategy
|
||||||
) : IMessageToController, ICanReply<Result<StopInstanceResult, InstanceActionFailure>>;
|
) : IMessageToController, ICanReply<Result<StopInstanceResult, UserInstanceActionFailure>>;
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
using MemoryPack;
|
||||||
|
|
||||||
|
namespace Phantom.Common.Messages.Web.ToWeb;
|
||||||
|
|
||||||
|
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||||
|
public sealed partial record RefreshUserSessionMessage(
|
||||||
|
[property: MemoryPackOrder(0)] Guid UserGuid
|
||||||
|
) : IMessageToWeb;
|
@ -24,30 +24,31 @@ public static class WebMessageRegistries {
|
|||||||
static WebMessageRegistries() {
|
static WebMessageRegistries() {
|
||||||
ToController.Add<RegisterWebMessage>(0);
|
ToController.Add<RegisterWebMessage>(0);
|
||||||
ToController.Add<UnregisterWebMessage>(1);
|
ToController.Add<UnregisterWebMessage>(1);
|
||||||
ToController.Add<LogInMessage, LogInSuccess?>(2);
|
ToController.Add<LogInMessage, Optional<LogInSuccess>>(2);
|
||||||
ToController.Add<LogOutMessage>(3);
|
ToController.Add<LogOutMessage>(3);
|
||||||
ToController.Add<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(4);
|
ToController.Add<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(4);
|
||||||
ToController.Add<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(5);
|
ToController.Add<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(5);
|
||||||
ToController.Add<CreateUserMessage, CreateUserResult>(6);
|
ToController.Add<CreateUserMessage, Result<CreateUserResult, UserActionFailure>>(6);
|
||||||
ToController.Add<DeleteUserMessage, DeleteUserResult>(7);
|
ToController.Add<DeleteUserMessage, Result<DeleteUserResult, UserActionFailure>>(7);
|
||||||
ToController.Add<GetUsersMessage, ImmutableArray<UserInfo>>(8);
|
ToController.Add<GetUsersMessage, ImmutableArray<UserInfo>>(8);
|
||||||
ToController.Add<GetRolesMessage, ImmutableArray<RoleInfo>>(9);
|
ToController.Add<GetRolesMessage, ImmutableArray<RoleInfo>>(9);
|
||||||
ToController.Add<GetUserRolesMessage, ImmutableDictionary<Guid, ImmutableArray<Guid>>>(10);
|
ToController.Add<GetUserRolesMessage, ImmutableDictionary<Guid, ImmutableArray<Guid>>>(10);
|
||||||
ToController.Add<ChangeUserRolesMessage, ChangeUserRolesResult>(11);
|
ToController.Add<ChangeUserRolesMessage, Result<ChangeUserRolesResult, UserActionFailure>>(11);
|
||||||
ToController.Add<CreateOrUpdateInstanceMessage, Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(12);
|
ToController.Add<CreateOrUpdateInstanceMessage, Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>(12);
|
||||||
ToController.Add<LaunchInstanceMessage, Result<LaunchInstanceResult, InstanceActionFailure>>(13);
|
ToController.Add<LaunchInstanceMessage, Result<LaunchInstanceResult, UserInstanceActionFailure>>(13);
|
||||||
ToController.Add<StopInstanceMessage, Result<StopInstanceResult, InstanceActionFailure>>(14);
|
ToController.Add<StopInstanceMessage, Result<StopInstanceResult, UserInstanceActionFailure>>(14);
|
||||||
ToController.Add<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, InstanceActionFailure>>(15);
|
ToController.Add<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, UserInstanceActionFailure>>(15);
|
||||||
ToController.Add<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(16);
|
ToController.Add<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(16);
|
||||||
ToController.Add<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(17);
|
ToController.Add<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(17);
|
||||||
ToController.Add<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(18);
|
ToController.Add<GetAuditLogMessage, Result<ImmutableArray<AuditLogItem>, UserActionFailure>>(18);
|
||||||
ToController.Add<GetEventLogMessage, ImmutableArray<EventLogItem>>(19);
|
ToController.Add<GetEventLogMessage, Result<ImmutableArray<EventLogItem>, UserActionFailure>>(19);
|
||||||
ToController.Add<ReplyMessage>(127);
|
ToController.Add<ReplyMessage>(127);
|
||||||
|
|
||||||
ToWeb.Add<RegisterWebResultMessage>(0);
|
ToWeb.Add<RegisterWebResultMessage>(0);
|
||||||
ToWeb.Add<RefreshAgentsMessage>(1);
|
ToWeb.Add<RefreshAgentsMessage>(1);
|
||||||
ToWeb.Add<RefreshInstancesMessage>(2);
|
ToWeb.Add<RefreshInstancesMessage>(2);
|
||||||
ToWeb.Add<InstanceOutputMessage>(3);
|
ToWeb.Add<InstanceOutputMessage>(3);
|
||||||
|
ToWeb.Add<RefreshUserSessionMessage>(4);
|
||||||
ToWeb.Add<ReplyMessage>(127);
|
ToWeb.Add<ReplyMessage>(127);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
353
Controller/Phantom.Controller.Database.Postgres/Migrations/20240407211636_UserAgentAccess.Designer.cs
generated
Normal file
353
Controller/Phantom.Controller.Database.Postgres/Migrations/20240407211636_UserAgentAccess.Designer.cs
generated
Normal file
@ -0,0 +1,353 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
using Phantom.Controller.Database;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Phantom.Controller.Database.Postgres.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20240407211636_UserAgentAccess")]
|
||||||
|
partial class UserAgentAccess
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "8.0.0")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Phantom.Controller.Database.Entities.AgentEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("AgentGuid")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("BuildVersion")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("MaxInstances")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<ushort>("MaxMemory")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("ProtocolVersion")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("AgentGuid");
|
||||||
|
|
||||||
|
b.ToTable("Agents", "agents");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Phantom.Controller.Database.Entities.AuditLogEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<JsonDocument>("Data")
|
||||||
|
.HasColumnType("jsonb");
|
||||||
|
|
||||||
|
b.Property<string>("EventType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("SubjectId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("SubjectType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<Guid?>("UserGuid")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UtcTime")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserGuid");
|
||||||
|
|
||||||
|
b.ToTable("AuditLog", "system");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Phantom.Controller.Database.Entities.EventLogEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("EventGuid")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid?>("AgentGuid")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<JsonDocument>("Data")
|
||||||
|
.HasColumnType("jsonb");
|
||||||
|
|
||||||
|
b.Property<string>("EventType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("SubjectId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("SubjectType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UtcTime")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("EventGuid");
|
||||||
|
|
||||||
|
b.ToTable("EventLog", "system");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Phantom.Controller.Database.Entities.InstanceEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("InstanceGuid")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("AgentGuid")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("InstanceName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<Guid>("JavaRuntimeGuid")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("JvmArguments")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<bool>("LaunchAutomatically")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<ushort>("MemoryAllocation")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("MinecraftServerKind")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("MinecraftVersion")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("RconPort")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("ServerPort")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("InstanceGuid");
|
||||||
|
|
||||||
|
b.ToTable("Instances", "agents");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Phantom.Controller.Database.Entities.PermissionEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Permissions", "identity");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Phantom.Controller.Database.Entities.RoleEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("RoleGuid")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("RoleGuid");
|
||||||
|
|
||||||
|
b.ToTable("Roles", "identity");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Phantom.Controller.Database.Entities.RolePermissionEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("RoleGuid")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("PermissionId")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("RoleGuid", "PermissionId");
|
||||||
|
|
||||||
|
b.HasIndex("PermissionId");
|
||||||
|
|
||||||
|
b.ToTable("RolePermissions", "identity");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserAgentAccessEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("UserGuid")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("AgentGuid")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("UserGuid", "AgentGuid");
|
||||||
|
|
||||||
|
b.HasIndex("AgentGuid");
|
||||||
|
|
||||||
|
b.ToTable("UserAgentAccess", "identity");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("UserGuid")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("UserGuid");
|
||||||
|
|
||||||
|
b.HasIndex("Name")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Users", "identity");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserPermissionEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("UserGuid")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("PermissionId")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("UserGuid", "PermissionId");
|
||||||
|
|
||||||
|
b.HasIndex("PermissionId");
|
||||||
|
|
||||||
|
b.ToTable("UserPermissions", "identity");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserRoleEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("UserGuid")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("RoleGuid")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("UserGuid", "RoleGuid");
|
||||||
|
|
||||||
|
b.HasIndex("RoleGuid");
|
||||||
|
|
||||||
|
b.ToTable("UserRoles", "identity");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Phantom.Controller.Database.Entities.AuditLogEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Phantom.Controller.Database.Entities.UserEntity", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserGuid")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Phantom.Controller.Database.Entities.RolePermissionEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Phantom.Controller.Database.Entities.PermissionEntity", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("PermissionId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Phantom.Controller.Database.Entities.RoleEntity", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleGuid")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserAgentAccessEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Phantom.Controller.Database.Entities.AgentEntity", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AgentGuid")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Phantom.Controller.Database.Entities.UserEntity", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserGuid")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserPermissionEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Phantom.Controller.Database.Entities.PermissionEntity", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("PermissionId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Phantom.Controller.Database.Entities.UserEntity", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserGuid")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserRoleEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Phantom.Controller.Database.Entities.RoleEntity", "Role")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleGuid")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Phantom.Controller.Database.Entities.UserEntity", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserGuid")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Role");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Phantom.Controller.Database.Postgres.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class UserAgentAccess : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "UserAgentAccess",
|
||||||
|
schema: "identity",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
UserGuid = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
AgentGuid = table.Column<Guid>(type: "uuid", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_UserAgentAccess", x => new { x.UserGuid, x.AgentGuid });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_UserAgentAccess_Agents_AgentGuid",
|
||||||
|
column: x => x.AgentGuid,
|
||||||
|
principalSchema: "agents",
|
||||||
|
principalTable: "Agents",
|
||||||
|
principalColumn: "AgentGuid",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_UserAgentAccess_Users_UserGuid",
|
||||||
|
column: x => x.UserGuid,
|
||||||
|
principalSchema: "identity",
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "UserGuid",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_UserAgentAccess_AgentGuid",
|
||||||
|
schema: "identity",
|
||||||
|
table: "UserAgentAccess",
|
||||||
|
column: "AgentGuid");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "UserAgentAccess",
|
||||||
|
schema: "identity");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -18,7 +18,7 @@ namespace Phantom.Controller.Database.Postgres.Migrations
|
|||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasAnnotation("ProductVersion", "7.0.11")
|
.HasAnnotation("ProductVersion", "8.0.0")
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
@ -204,6 +204,21 @@ namespace Phantom.Controller.Database.Postgres.Migrations
|
|||||||
b.ToTable("RolePermissions", "identity");
|
b.ToTable("RolePermissions", "identity");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserAgentAccessEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("UserGuid")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("AgentGuid")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("UserGuid", "AgentGuid");
|
||||||
|
|
||||||
|
b.HasIndex("AgentGuid");
|
||||||
|
|
||||||
|
b.ToTable("UserAgentAccess", "identity");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserEntity", b =>
|
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserEntity", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("UserGuid")
|
b.Property<Guid>("UserGuid")
|
||||||
@ -281,6 +296,21 @@ namespace Phantom.Controller.Database.Postgres.Migrations
|
|||||||
.IsRequired();
|
.IsRequired();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserAgentAccessEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Phantom.Controller.Database.Entities.AgentEntity", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AgentGuid")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Phantom.Controller.Database.Entities.UserEntity", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserGuid")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserPermissionEntity", b =>
|
modelBuilder.Entity("Phantom.Controller.Database.Entities.UserPermissionEntity", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Phantom.Controller.Database.Entities.PermissionEntity", null)
|
b.HasOne("Phantom.Controller.Database.Entities.PermissionEntity", null)
|
||||||
|
@ -20,6 +20,7 @@ public class ApplicationDbContext : DbContext {
|
|||||||
public DbSet<UserRoleEntity> UserRoles { get; init; } = null!;
|
public DbSet<UserRoleEntity> UserRoles { get; init; } = null!;
|
||||||
public DbSet<UserPermissionEntity> UserPermissions { get; init; } = null!;
|
public DbSet<UserPermissionEntity> UserPermissions { get; init; } = null!;
|
||||||
public DbSet<RolePermissionEntity> RolePermissions { get; init; } = null!;
|
public DbSet<RolePermissionEntity> RolePermissions { get; init; } = null!;
|
||||||
|
public DbSet<UserAgentAccessEntity> UserAgentAccess { get; init; } = null!;
|
||||||
|
|
||||||
public DbSet<AgentEntity> Agents { get; init; } = null!;
|
public DbSet<AgentEntity> Agents { get; init; } = null!;
|
||||||
public DbSet<InstanceEntity> Instances { get; init; } = null!;
|
public DbSet<InstanceEntity> Instances { get; init; } = null!;
|
||||||
@ -62,6 +63,12 @@ public class ApplicationDbContext : DbContext {
|
|||||||
b.HasOne<RoleEntity>().WithMany().HasForeignKey(static e => e.RoleGuid).IsRequired().OnDelete(DeleteBehavior.Cascade);
|
b.HasOne<RoleEntity>().WithMany().HasForeignKey(static e => e.RoleGuid).IsRequired().OnDelete(DeleteBehavior.Cascade);
|
||||||
b.HasOne<PermissionEntity>().WithMany().HasForeignKey(static e => e.PermissionId).IsRequired().OnDelete(DeleteBehavior.Cascade);
|
b.HasOne<PermissionEntity>().WithMany().HasForeignKey(static e => e.PermissionId).IsRequired().OnDelete(DeleteBehavior.Cascade);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
builder.Entity<UserAgentAccessEntity>(static b => {
|
||||||
|
b.HasKey(static e => new { UserId = e.UserGuid, AgentId = e.AgentGuid });
|
||||||
|
b.HasOne<UserEntity>().WithMany().HasForeignKey(static e => e.UserGuid).IsRequired().OnDelete(DeleteBehavior.Cascade);
|
||||||
|
b.HasOne<AgentEntity>().WithMany().HasForeignKey(static e => e.AgentGuid).IsRequired().OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void ConfigureConventions(ModelConfigurationBuilder builder) {
|
protected override void ConfigureConventions(ModelConfigurationBuilder builder) {
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace Phantom.Controller.Database.Entities;
|
||||||
|
|
||||||
|
[Table("UserAgentAccess", Schema = "identity")]
|
||||||
|
public sealed class UserAgentAccessEntity {
|
||||||
|
public Guid UserGuid { get; init; }
|
||||||
|
public Guid AgentGuid { get; init; }
|
||||||
|
|
||||||
|
public UserAgentAccessEntity(Guid userGuid, Guid agentGuid) {
|
||||||
|
UserGuid = userGuid;
|
||||||
|
AgentGuid = agentGuid;
|
||||||
|
}
|
||||||
|
}
|
@ -17,11 +17,12 @@ public sealed class EventLogRepository {
|
|||||||
db.Ctx.EventLog.Add(new EventLogEntity(eventGuid, utcTime, agentGuid, eventType, subjectId, extra));
|
db.Ctx.EventLog.Add(new EventLogEntity(eventGuid, utcTime, agentGuid, eventType, subjectId, extra));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<ImmutableArray<EventLogItem>> GetMostRecentItems(int count, CancellationToken cancellationToken) {
|
public Task<ImmutableArray<EventLogItem>> GetMostRecentItems(ImmutableHashSet<Guid> agentGuids, int count, CancellationToken cancellationToken) {
|
||||||
return db.Ctx
|
return db.Ctx
|
||||||
.EventLog
|
.EventLog
|
||||||
.AsQueryable()
|
.AsQueryable()
|
||||||
.OrderByDescending(static entity => entity.UtcTime)
|
.OrderByDescending(static entity => entity.UtcTime)
|
||||||
|
.Where(entity => entity.AgentGuid == null || agentGuids.Contains(entity.AgentGuid.Value))
|
||||||
.Take(count)
|
.Take(count)
|
||||||
.AsAsyncEnumerable()
|
.AsAsyncEnumerable()
|
||||||
.Select(static entity => new EventLogItem(entity.UtcTime, entity.AgentGuid, entity.EventType, entity.SubjectType, entity.SubjectId, entity.Data?.RootElement.ToString()))
|
.Select(static entity => new EventLogItem(entity.UtcTime, entity.AgentGuid, entity.EventType, entity.SubjectType, entity.SubjectId, entity.Data?.RootElement.ToString()))
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
using System.Collections.Immutable;
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<ImmutableHashSet<Guid>> GetManagedAgentGuids(UserEntity user) {
|
||||||
|
return db.Ctx.UserAgentAccess
|
||||||
|
.Where(ua => ua.UserGuid == user.UserGuid)
|
||||||
|
.Select(static ua => ua.AgentGuid)
|
||||||
|
.AsAsyncEnumerable()
|
||||||
|
.ToImmutableSetAsync();
|
||||||
|
}
|
||||||
|
}
|
@ -146,13 +146,13 @@ 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, InstanceActionFailure>> RequestInstance<TCommand, TReply>(Guid instanceGuid, TCommand command) where TCommand : InstanceActor.ICommand, ICanReply<Result<TReply, InstanceActionFailure>> {
|
||||||
if (instanceActorByGuid.TryGetValue(instanceGuid, out var instance)) {
|
if (instanceActorByGuid.TryGetValue(instanceGuid, out var instance)) {
|
||||||
return instance.Request(command, cancellationToken);
|
return await instance.Request(command, cancellationToken);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Logger.Warning("Could not deliver command {CommandType} to instance {InstanceGuid}, instance not found.", command.GetType().Name, instanceGuid);
|
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 InstanceActionFailure.InstanceDoesNotExist;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,15 +183,15 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
|
|||||||
|
|
||||||
public sealed record UpdateJavaRuntimesCommand(ImmutableArray<TaggedJavaRuntime> JavaRuntimes) : 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(Guid LoggedInUserGuid, Guid InstanceGuid, InstanceConfiguration Configuration) : ICommand, ICanReply<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>;
|
||||||
|
|
||||||
public sealed record UpdateInstanceStatusCommand(Guid InstanceGuid, IInstanceStatus Status) : ICommand;
|
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(Guid LoggedInUserGuid, Guid InstanceGuid) : ICommand, ICanReply<Result<LaunchInstanceResult, InstanceActionFailure>>;
|
||||||
|
|
||||||
public sealed record StopInstanceCommand(Guid InstanceGuid, Guid AuditLogUserGuid, MinecraftStopStrategy StopStrategy) : ICommand, ICanReply<Result<StopInstanceResult, InstanceActionFailure>>;
|
public sealed record StopInstanceCommand(Guid LoggedInUserGuid, Guid InstanceGuid, MinecraftStopStrategy StopStrategy) : ICommand, ICanReply<Result<StopInstanceResult, InstanceActionFailure>>;
|
||||||
|
|
||||||
public sealed record SendCommandToInstanceCommand(Guid InstanceGuid, Guid AuditLogUserGuid, string Command) : ICommand, ICanReply<Result<SendCommandToInstanceResult, InstanceActionFailure>>;
|
public sealed record SendCommandToInstanceCommand(Guid LoggedInUserGuid, Guid InstanceGuid, string Command) : ICommand, ICanReply<Result<SendCommandToInstanceResult, InstanceActionFailure>>;
|
||||||
|
|
||||||
public sealed record ReceiveInstanceDataCommand(Instance Instance) : ICommand, IJumpAhead;
|
public sealed record ReceiveInstanceDataCommand(Instance Instance) : ICommand, IJumpAhead;
|
||||||
|
|
||||||
@ -308,7 +308,7 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
|
|||||||
instanceActorRef = CreateNewInstance(Instance.Offline(command.InstanceGuid, instanceConfiguration));
|
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(command.LoggedInUserGuid, command.InstanceGuid, instanceConfiguration, new InstanceLaunchProperties(serverExecutableInfo), isCreatingInstance);
|
||||||
|
|
||||||
return instanceActorRef.Request(configureInstanceCommand, cancellationToken)
|
return instanceActorRef.Request(configureInstanceCommand, cancellationToken)
|
||||||
.ContinueOnActor(CreateOrUpdateInstance2, configureInstanceCommand);
|
.ContinueOnActor(CreateOrUpdateInstance2, configureInstanceCommand);
|
||||||
@ -330,7 +330,7 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
|
|||||||
else {
|
else {
|
||||||
string action = isCreating ? "adding" : "editing";
|
string action = isCreating ? "adding" : "editing";
|
||||||
string relation = isCreating ? "to agent" : "in agent";
|
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);
|
Logger.Information("Failed " + action + " instance \"{InstanceName}\" (GUID {InstanceGuid}) " + relation + " \"{AgentName}\". {ErrorMessage}", instanceName, instanceGuid, configuration.AgentName, reason);
|
||||||
|
|
||||||
@ -343,15 +343,15 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Task<Result<LaunchInstanceResult, InstanceActionFailure>> LaunchInstance(LaunchInstanceCommand command) {
|
private Task<Result<LaunchInstanceResult, InstanceActionFailure>> LaunchInstance(LaunchInstanceCommand command) {
|
||||||
return RequestInstance<InstanceActor.LaunchInstanceCommand, LaunchInstanceResult>(command.InstanceGuid, new InstanceActor.LaunchInstanceCommand(command.AuditLogUserGuid));
|
return RequestInstance<InstanceActor.LaunchInstanceCommand, LaunchInstanceResult>(command.InstanceGuid, new InstanceActor.LaunchInstanceCommand(command.LoggedInUserGuid));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<Result<StopInstanceResult, InstanceActionFailure>> StopInstance(StopInstanceCommand command) {
|
private Task<Result<StopInstanceResult, InstanceActionFailure>> StopInstance(StopInstanceCommand command) {
|
||||||
return RequestInstance<InstanceActor.StopInstanceCommand, StopInstanceResult>(command.InstanceGuid, new InstanceActor.StopInstanceCommand(command.AuditLogUserGuid, command.StopStrategy));
|
return RequestInstance<InstanceActor.StopInstanceCommand, StopInstanceResult>(command.InstanceGuid, new InstanceActor.StopInstanceCommand(command.LoggedInUserGuid, command.StopStrategy));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<Result<SendCommandToInstanceResult, InstanceActionFailure>> SendMinecraftCommand(SendCommandToInstanceCommand command) {
|
private Task<Result<SendCommandToInstanceResult, InstanceActionFailure>> SendMinecraftCommand(SendCommandToInstanceCommand command) {
|
||||||
return RequestInstance<InstanceActor.SendCommandToInstanceCommand, SendCommandToInstanceResult>(command.InstanceGuid, new InstanceActor.SendCommandToInstanceCommand(command.AuditLogUserGuid, command.Command));
|
return RequestInstance<InstanceActor.SendCommandToInstanceCommand, SendCommandToInstanceResult>(command.InstanceGuid, new InstanceActor.SendCommandToInstanceCommand(command.LoggedInUserGuid, command.Command));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ReceiveInstanceData(ReceiveInstanceDataCommand command) {
|
private void ReceiveInstanceData(ReceiveInstanceDataCommand command) {
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using Akka.Actor;
|
using Akka.Actor;
|
||||||
using Phantom.Common.Data;
|
using Phantom.Common.Data;
|
||||||
using Phantom.Common.Data.Agent;
|
using Phantom.Common.Data.Agent;
|
||||||
using Phantom.Common.Data.Replies;
|
using Phantom.Common.Data.Replies;
|
||||||
using Phantom.Common.Data.Web.Agent;
|
using Phantom.Common.Data.Web.Agent;
|
||||||
|
using Phantom.Common.Data.Web.Users;
|
||||||
using Phantom.Common.Messages.Agent;
|
using Phantom.Common.Messages.Agent;
|
||||||
using Phantom.Common.Messages.Agent.ToAgent;
|
using Phantom.Common.Messages.Agent.ToAgent;
|
||||||
using Phantom.Controller.Database;
|
using Phantom.Controller.Database;
|
||||||
using Phantom.Controller.Minecraft;
|
using Phantom.Controller.Minecraft;
|
||||||
|
using Phantom.Controller.Services.Users.Sessions;
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
using Phantom.Utils.Logging;
|
using Phantom.Utils.Logging;
|
||||||
using Phantom.Utils.Rpc.Runtime;
|
using Phantom.Utils.Rpc.Runtime;
|
||||||
@ -22,17 +25,19 @@ sealed class AgentManager {
|
|||||||
private readonly AuthToken authToken;
|
private readonly AuthToken authToken;
|
||||||
private readonly ControllerState controllerState;
|
private readonly ControllerState controllerState;
|
||||||
private readonly MinecraftVersions minecraftVersions;
|
private readonly MinecraftVersions minecraftVersions;
|
||||||
|
private readonly UserLoginManager userLoginManager;
|
||||||
private readonly IDbContextProvider dbProvider;
|
private readonly IDbContextProvider dbProvider;
|
||||||
private readonly CancellationToken cancellationToken;
|
private readonly CancellationToken cancellationToken;
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<Guid, ActorRef<AgentActor.ICommand>> agentsByGuid = new ();
|
private readonly ConcurrentDictionary<Guid, ActorRef<AgentActor.ICommand>> agentsByGuid = new ();
|
||||||
private readonly Func<Guid, AgentConfiguration, ActorRef<AgentActor.ICommand>> addAgentActorFactory;
|
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.actorSystem = actorSystem;
|
||||||
this.authToken = authToken;
|
this.authToken = authToken;
|
||||||
this.controllerState = controllerState;
|
this.controllerState = controllerState;
|
||||||
this.minecraftVersions = minecraftVersions;
|
this.minecraftVersions = minecraftVersions;
|
||||||
|
this.userLoginManager = userLoginManager;
|
||||||
this.dbProvider = dbProvider;
|
this.dbProvider = dbProvider;
|
||||||
this.cancellationToken = cancellationToken;
|
this.cancellationToken = cancellationToken;
|
||||||
|
|
||||||
@ -83,7 +88,18 @@ 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>> {
|
public async Task<Result<TReply, UserInstanceActionFailure>> DoInstanceAction<TCommand, TReply>(Permission requiredPermission, ImmutableArray<byte> authToken, Guid agentGuid, Func<Guid, TCommand> commandFactoryFromLoggedInUserGuid) where TCommand : class, AgentActor.ICommand, ICanReply<Result<TReply, InstanceActionFailure>> {
|
||||||
return agentsByGuid.TryGetValue(agentGuid, out var agent) ? await agent.Request(command, cancellationToken) : InstanceActionFailure.AgentDoesNotExist;
|
var loggedInUser = userLoginManager.GetLoggedInUser(authToken);
|
||||||
|
if (!loggedInUser.HasAccessToAgent(agentGuid) || !loggedInUser.CheckPermission(requiredPermission)) {
|
||||||
|
return (UserInstanceActionFailure) UserActionFailure.NotAuthorized;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!agentsByGuid.TryGetValue(agentGuid, out var agent)) {
|
||||||
|
return (UserInstanceActionFailure) InstanceActionFailure.AgentDoesNotExist;
|
||||||
|
}
|
||||||
|
|
||||||
|
var command = commandFactoryFromLoggedInUserGuid(loggedInUser.Guid!.Value);
|
||||||
|
var result = await agent.Request(command, cancellationToken);
|
||||||
|
return result.MapError(static error => (UserInstanceActionFailure) error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ using Phantom.Controller.Services.Events;
|
|||||||
using Phantom.Controller.Services.Instances;
|
using Phantom.Controller.Services.Instances;
|
||||||
using Phantom.Controller.Services.Rpc;
|
using Phantom.Controller.Services.Rpc;
|
||||||
using Phantom.Controller.Services.Users;
|
using Phantom.Controller.Services.Users;
|
||||||
|
using Phantom.Controller.Services.Users.Sessions;
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
using Phantom.Utils.Rpc.Runtime;
|
using Phantom.Utils.Rpc.Runtime;
|
||||||
using IMessageFromAgentToController = Phantom.Common.Messages.Agent.IMessageToController;
|
using IMessageFromAgentToController = Phantom.Common.Messages.Agent.IMessageToController;
|
||||||
@ -24,17 +25,18 @@ public sealed class ControllerServices : IDisposable {
|
|||||||
private ControllerState ControllerState { get; }
|
private ControllerState ControllerState { get; }
|
||||||
private MinecraftVersions MinecraftVersions { get; }
|
private MinecraftVersions MinecraftVersions { get; }
|
||||||
|
|
||||||
private AgentManager AgentManager { get; }
|
private AuthenticatedUserCache AuthenticatedUserCache { get; }
|
||||||
private InstanceLogManager InstanceLogManager { get; }
|
|
||||||
private EventLogManager EventLogManager { get; }
|
|
||||||
|
|
||||||
private UserManager UserManager { get; }
|
private UserManager UserManager { get; }
|
||||||
private RoleManager RoleManager { get; }
|
private RoleManager RoleManager { get; }
|
||||||
private PermissionManager PermissionManager { get; }
|
|
||||||
|
|
||||||
private UserRoleManager UserRoleManager { get; }
|
private UserRoleManager UserRoleManager { get; }
|
||||||
private UserLoginManager UserLoginManager { get; }
|
private UserLoginManager UserLoginManager { get; }
|
||||||
|
private PermissionManager PermissionManager { get; }
|
||||||
|
|
||||||
|
private AgentManager AgentManager { get; }
|
||||||
|
private InstanceLogManager InstanceLogManager { get; }
|
||||||
|
|
||||||
private AuditLogManager AuditLogManager { get; }
|
private AuditLogManager AuditLogManager { get; }
|
||||||
|
private EventLogManager EventLogManager { get; }
|
||||||
|
|
||||||
public IRegistrationHandler<IMessageToAgent, IMessageFromAgentToController, RegisterAgentMessage> AgentRegistrationHandler { get; }
|
public IRegistrationHandler<IMessageToAgent, IMessageFromAgentToController, RegisterAgentMessage> AgentRegistrationHandler { get; }
|
||||||
public IRegistrationHandler<IMessageToWeb, IMessageFromWebToController, RegisterWebMessage> WebRegistrationHandler { get; }
|
public IRegistrationHandler<IMessageToWeb, IMessageFromWebToController, RegisterWebMessage> WebRegistrationHandler { get; }
|
||||||
@ -51,17 +53,18 @@ public sealed class ControllerServices : IDisposable {
|
|||||||
this.ControllerState = new ControllerState();
|
this.ControllerState = new ControllerState();
|
||||||
this.MinecraftVersions = new MinecraftVersions();
|
this.MinecraftVersions = new MinecraftVersions();
|
||||||
|
|
||||||
this.AgentManager = new AgentManager(ActorSystem, agentAuthToken, ControllerState, MinecraftVersions, dbProvider, cancellationToken);
|
this.AuthenticatedUserCache = new AuthenticatedUserCache();
|
||||||
this.InstanceLogManager = new InstanceLogManager();
|
this.UserManager = new UserManager(AuthenticatedUserCache, ControllerState, dbProvider);
|
||||||
|
|
||||||
this.UserManager = new UserManager(dbProvider);
|
|
||||||
this.RoleManager = new RoleManager(dbProvider);
|
this.RoleManager = new RoleManager(dbProvider);
|
||||||
|
this.UserRoleManager = new UserRoleManager(AuthenticatedUserCache, ControllerState, dbProvider);
|
||||||
|
this.UserLoginManager = new UserLoginManager(AuthenticatedUserCache, UserManager, dbProvider);
|
||||||
this.PermissionManager = new PermissionManager(dbProvider);
|
this.PermissionManager = new PermissionManager(dbProvider);
|
||||||
|
|
||||||
this.UserRoleManager = new UserRoleManager(dbProvider);
|
this.AgentManager = new AgentManager(ActorSystem, agentAuthToken, ControllerState, MinecraftVersions, UserLoginManager, dbProvider, cancellationToken);
|
||||||
this.UserLoginManager = new UserLoginManager(UserManager, PermissionManager, dbProvider);
|
this.InstanceLogManager = new InstanceLogManager();
|
||||||
|
|
||||||
this.AuditLogManager = new AuditLogManager(dbProvider);
|
this.AuditLogManager = new AuditLogManager(dbProvider);
|
||||||
this.EventLogManager = new EventLogManager(ActorSystem, dbProvider, shutdownCancellationToken);
|
this.EventLogManager = new EventLogManager(ControllerState, ActorSystem, dbProvider, shutdownCancellationToken);
|
||||||
|
|
||||||
this.AgentRegistrationHandler = new AgentRegistrationHandler(AgentManager, InstanceLogManager, EventLogManager);
|
this.AgentRegistrationHandler = new AgentRegistrationHandler(AgentManager, InstanceLogManager, EventLogManager);
|
||||||
this.WebRegistrationHandler = new WebRegistrationHandler(webAuthToken, ControllerState, InstanceLogManager, UserManager, RoleManager, UserRoleManager, UserLoginManager, AuditLogManager, AgentManager, MinecraftVersions, EventLogManager);
|
this.WebRegistrationHandler = new WebRegistrationHandler(webAuthToken, ControllerState, InstanceLogManager, UserManager, RoleManager, UserRoleManager, UserLoginManager, AuditLogManager, AgentManager, MinecraftVersions, EventLogManager);
|
||||||
|
@ -19,6 +19,8 @@ sealed class ControllerState {
|
|||||||
public ObservableState<ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>.Receiver AgentJavaRuntimesByGuidReceiver => agentJavaRuntimesByGuid.ReceiverSide;
|
public ObservableState<ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>.Receiver AgentJavaRuntimesByGuidReceiver => agentJavaRuntimesByGuid.ReceiverSide;
|
||||||
public ObservableState<ImmutableDictionary<Guid, Instance>>.Receiver InstancesByGuidReceiver => instancesByGuid.ReceiverSide;
|
public ObservableState<ImmutableDictionary<Guid, Instance>>.Receiver InstancesByGuidReceiver => instancesByGuid.ReceiverSide;
|
||||||
|
|
||||||
|
public event EventHandler<Guid>? UserUpdatedOrDeleted;
|
||||||
|
|
||||||
public void UpdateAgent(Agent agent) {
|
public void UpdateAgent(Agent agent) {
|
||||||
agentsByGuid.PublisherSide.Publish(static (agentsByGuid, agent) => agentsByGuid.SetItem(agent.AgentGuid, agent), agent);
|
agentsByGuid.PublisherSide.Publish(static (agentsByGuid, agent) => agentsByGuid.SetItem(agent.AgentGuid, agent), agent);
|
||||||
}
|
}
|
||||||
@ -30,4 +32,8 @@ sealed class ControllerState {
|
|||||||
public void UpdateInstance(Instance instance) {
|
public void UpdateInstance(Instance instance) {
|
||||||
instancesByGuid.PublisherSide.Publish(static (instancesByGuid, instance) => instancesByGuid.SetItem(instance.InstanceGuid, instance), instance);
|
instancesByGuid.PublisherSide.Publish(static (instancesByGuid, instance) => instancesByGuid.SetItem(instance.InstanceGuid, instance), instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void UpdateOrDeleteUser(Guid userGuid) {
|
||||||
|
UserUpdatedOrDeleted?.Invoke(null, userGuid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,23 @@
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using Akka.Actor;
|
using Akka.Actor;
|
||||||
|
using Phantom.Common.Data;
|
||||||
using Phantom.Common.Data.Web.EventLog;
|
using Phantom.Common.Data.Web.EventLog;
|
||||||
|
using Phantom.Common.Data.Web.Users;
|
||||||
using Phantom.Controller.Database;
|
using Phantom.Controller.Database;
|
||||||
using Phantom.Controller.Database.Repositories;
|
using Phantom.Controller.Database.Repositories;
|
||||||
|
using Phantom.Controller.Services.Users.Sessions;
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
|
|
||||||
namespace Phantom.Controller.Services.Events;
|
namespace Phantom.Controller.Services.Events;
|
||||||
|
|
||||||
sealed partial class EventLogManager {
|
sealed partial class EventLogManager {
|
||||||
|
private readonly ControllerState controllerState;
|
||||||
private readonly ActorRef<EventLogDatabaseStorageActor.ICommand> databaseStorageActor;
|
private readonly ActorRef<EventLogDatabaseStorageActor.ICommand> databaseStorageActor;
|
||||||
private readonly IDbContextProvider dbProvider;
|
private readonly IDbContextProvider dbProvider;
|
||||||
private readonly CancellationToken cancellationToken;
|
private readonly CancellationToken cancellationToken;
|
||||||
|
|
||||||
public EventLogManager(IActorRefFactory actorSystem, IDbContextProvider dbProvider, CancellationToken cancellationToken) {
|
public EventLogManager(ControllerState controllerState, IActorRefFactory actorSystem, IDbContextProvider dbProvider, CancellationToken cancellationToken) {
|
||||||
|
this.controllerState = controllerState;
|
||||||
this.databaseStorageActor = actorSystem.ActorOf(EventLogDatabaseStorageActor.Factory(new EventLogDatabaseStorageActor.Init(dbProvider, cancellationToken)), "EventLogDatabaseStorage");
|
this.databaseStorageActor = actorSystem.ActorOf(EventLogDatabaseStorageActor.Factory(new EventLogDatabaseStorageActor.Init(dbProvider, cancellationToken)), "EventLogDatabaseStorage");
|
||||||
this.dbProvider = dbProvider;
|
this.dbProvider = dbProvider;
|
||||||
this.cancellationToken = cancellationToken;
|
this.cancellationToken = cancellationToken;
|
||||||
@ -22,8 +27,14 @@ sealed partial class EventLogManager {
|
|||||||
databaseStorageActor.Tell(new EventLogDatabaseStorageActor.StoreEventCommand(eventGuid, utcTime, agentGuid, eventType, subjectId, extra));
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
var accessibleAgentGuids = loggedInUser.FilterAccessibleAgentGuids(controllerState.AgentsByGuid.Keys.ToImmutableHashSet());
|
||||||
|
|
||||||
await using var db = dbProvider.Lazy();
|
await using var db = dbProvider.Lazy();
|
||||||
return await new EventLogRepository(db).GetMostRecentItems(count, cancellationToken);
|
return await new EventLogRepository(db).GetMostRecentItems(accessibleAgentGuids, count, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,22 +30,31 @@ sealed class WebMessageDataUpdateSenderActor : ReceiveActor<WebMessageDataUpdate
|
|||||||
ReceiveAsync<RefreshAgentsCommand>(RefreshAgents);
|
ReceiveAsync<RefreshAgentsCommand>(RefreshAgents);
|
||||||
ReceiveAsync<RefreshInstancesCommand>(RefreshInstances);
|
ReceiveAsync<RefreshInstancesCommand>(RefreshInstances);
|
||||||
ReceiveAsync<ReceiveInstanceLogsCommand>(ReceiveInstanceLogs);
|
ReceiveAsync<ReceiveInstanceLogsCommand>(ReceiveInstanceLogs);
|
||||||
|
ReceiveAsync<RefreshUserSessionCommand>(RefreshUserSession);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void PreStart() {
|
protected override void PreStart() {
|
||||||
controllerState.AgentsByGuidReceiver.Register(SelfTyped, static state => new RefreshAgentsCommand(state));
|
controllerState.AgentsByGuidReceiver.Register(SelfTyped, static state => new RefreshAgentsCommand(state));
|
||||||
controllerState.InstancesByGuidReceiver.Register(SelfTyped, static state => new RefreshInstancesCommand(state));
|
controllerState.InstancesByGuidReceiver.Register(SelfTyped, static state => new RefreshInstancesCommand(state));
|
||||||
|
|
||||||
|
controllerState.UserUpdatedOrDeleted += OnUserUpdatedOrDeleted;
|
||||||
|
|
||||||
instanceLogManager.LogsReceived += OnInstanceLogsReceived;
|
instanceLogManager.LogsReceived += OnInstanceLogsReceived;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void PostStop() {
|
protected override void PostStop() {
|
||||||
instanceLogManager.LogsReceived -= OnInstanceLogsReceived;
|
instanceLogManager.LogsReceived -= OnInstanceLogsReceived;
|
||||||
|
|
||||||
|
controllerState.UserUpdatedOrDeleted -= OnUserUpdatedOrDeleted;
|
||||||
|
|
||||||
controllerState.AgentsByGuidReceiver.Unregister(SelfTyped);
|
controllerState.AgentsByGuidReceiver.Unregister(SelfTyped);
|
||||||
controllerState.InstancesByGuidReceiver.Unregister(SelfTyped);
|
controllerState.InstancesByGuidReceiver.Unregister(SelfTyped);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnUserUpdatedOrDeleted(object? sender, Guid userGuid) {
|
||||||
|
selfCached.Tell(new RefreshUserSessionCommand(userGuid));
|
||||||
|
}
|
||||||
|
|
||||||
private void OnInstanceLogsReceived(object? sender, InstanceLogManager.Event e) {
|
private void OnInstanceLogsReceived(object? sender, InstanceLogManager.Event e) {
|
||||||
selfCached.Tell(new ReceiveInstanceLogsCommand(e.InstanceGuid, e.Lines));
|
selfCached.Tell(new ReceiveInstanceLogsCommand(e.InstanceGuid, e.Lines));
|
||||||
}
|
}
|
||||||
@ -58,6 +67,8 @@ sealed class WebMessageDataUpdateSenderActor : ReceiveActor<WebMessageDataUpdate
|
|||||||
|
|
||||||
private sealed record ReceiveInstanceLogsCommand(Guid InstanceGuid, ImmutableArray<string> Lines) : ICommand;
|
private sealed record ReceiveInstanceLogsCommand(Guid InstanceGuid, ImmutableArray<string> Lines) : ICommand;
|
||||||
|
|
||||||
|
private sealed record RefreshUserSessionCommand(Guid UserGuid) : ICommand;
|
||||||
|
|
||||||
private Task RefreshAgents(RefreshAgentsCommand command) {
|
private Task RefreshAgents(RefreshAgentsCommand command) {
|
||||||
return connection.Send(new RefreshAgentsMessage(command.Agents.Values.ToImmutableArray()));
|
return connection.Send(new RefreshAgentsMessage(command.Agents.Values.ToImmutableArray()));
|
||||||
}
|
}
|
||||||
@ -69,4 +80,8 @@ sealed class WebMessageDataUpdateSenderActor : ReceiveActor<WebMessageDataUpdate
|
|||||||
private Task ReceiveInstanceLogs(ReceiveInstanceLogsCommand command) {
|
private Task ReceiveInstanceLogs(ReceiveInstanceLogsCommand command) {
|
||||||
return connection.Send(new InstanceOutputMessage(command.InstanceGuid, command.Lines));
|
return connection.Send(new InstanceOutputMessage(command.InstanceGuid, command.Lines));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Task RefreshUserSession(RefreshUserSessionCommand command) {
|
||||||
|
return connection.Send(new RefreshUserSessionMessage(command.UserGuid));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ using Phantom.Controller.Services.Agents;
|
|||||||
using Phantom.Controller.Services.Events;
|
using Phantom.Controller.Services.Events;
|
||||||
using Phantom.Controller.Services.Instances;
|
using Phantom.Controller.Services.Instances;
|
||||||
using Phantom.Controller.Services.Users;
|
using Phantom.Controller.Services.Users;
|
||||||
|
using Phantom.Controller.Services.Users.Sessions;
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
using Phantom.Utils.Rpc.Runtime;
|
using Phantom.Utils.Rpc.Runtime;
|
||||||
|
|
||||||
@ -70,24 +71,24 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
|
|||||||
|
|
||||||
ReceiveAsync<RegisterWebMessage>(HandleRegisterWeb);
|
ReceiveAsync<RegisterWebMessage>(HandleRegisterWeb);
|
||||||
Receive<UnregisterWebMessage>(HandleUnregisterWeb);
|
Receive<UnregisterWebMessage>(HandleUnregisterWeb);
|
||||||
ReceiveAndReplyLater<LogInMessage, LogInSuccess?>(HandleLogIn);
|
ReceiveAndReplyLater<LogInMessage, Optional<LogInSuccess>>(HandleLogIn);
|
||||||
Receive<LogOutMessage>(HandleLogOut);
|
Receive<LogOutMessage>(HandleLogOut);
|
||||||
ReceiveAndReply<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(GetAuthenticatedUser);
|
ReceiveAndReply<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(GetAuthenticatedUser);
|
||||||
ReceiveAndReplyLater<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(HandleCreateOrUpdateAdministratorUser);
|
ReceiveAndReplyLater<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(HandleCreateOrUpdateAdministratorUser);
|
||||||
ReceiveAndReplyLater<CreateUserMessage, CreateUserResult>(HandleCreateUser);
|
ReceiveAndReplyLater<CreateUserMessage, Result<CreateUserResult, UserActionFailure>>(HandleCreateUser);
|
||||||
ReceiveAndReplyLater<GetUsersMessage, ImmutableArray<UserInfo>>(HandleGetUsers);
|
ReceiveAndReplyLater<GetUsersMessage, ImmutableArray<UserInfo>>(HandleGetUsers);
|
||||||
ReceiveAndReplyLater<GetRolesMessage, ImmutableArray<RoleInfo>>(HandleGetRoles);
|
ReceiveAndReplyLater<GetRolesMessage, ImmutableArray<RoleInfo>>(HandleGetRoles);
|
||||||
ReceiveAndReplyLater<GetUserRolesMessage, ImmutableDictionary<Guid, ImmutableArray<Guid>>>(HandleGetUserRoles);
|
ReceiveAndReplyLater<GetUserRolesMessage, ImmutableDictionary<Guid, ImmutableArray<Guid>>>(HandleGetUserRoles);
|
||||||
ReceiveAndReplyLater<ChangeUserRolesMessage, ChangeUserRolesResult>(HandleChangeUserRoles);
|
ReceiveAndReplyLater<ChangeUserRolesMessage, Result<ChangeUserRolesResult, UserActionFailure>>(HandleChangeUserRoles);
|
||||||
ReceiveAndReplyLater<DeleteUserMessage, DeleteUserResult>(HandleDeleteUser);
|
ReceiveAndReplyLater<DeleteUserMessage, Result<DeleteUserResult, UserActionFailure>>(HandleDeleteUser);
|
||||||
ReceiveAndReplyLater<CreateOrUpdateInstanceMessage, Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(HandleCreateOrUpdateInstance);
|
ReceiveAndReplyLater<CreateOrUpdateInstanceMessage, Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>(HandleCreateOrUpdateInstance);
|
||||||
ReceiveAndReplyLater<LaunchInstanceMessage, Result<LaunchInstanceResult, InstanceActionFailure>>(HandleLaunchInstance);
|
ReceiveAndReplyLater<LaunchInstanceMessage, Result<LaunchInstanceResult, UserInstanceActionFailure>>(HandleLaunchInstance);
|
||||||
ReceiveAndReplyLater<StopInstanceMessage, Result<StopInstanceResult, InstanceActionFailure>>(HandleStopInstance);
|
ReceiveAndReplyLater<StopInstanceMessage, Result<StopInstanceResult, UserInstanceActionFailure>>(HandleStopInstance);
|
||||||
ReceiveAndReplyLater<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, InstanceActionFailure>>(HandleSendCommandToInstance);
|
ReceiveAndReplyLater<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, UserInstanceActionFailure>>(HandleSendCommandToInstance);
|
||||||
ReceiveAndReplyLater<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(HandleGetMinecraftVersions);
|
ReceiveAndReplyLater<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(HandleGetMinecraftVersions);
|
||||||
ReceiveAndReply<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(HandleGetAgentJavaRuntimes);
|
ReceiveAndReply<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(HandleGetAgentJavaRuntimes);
|
||||||
ReceiveAndReplyLater<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(HandleGetAuditLog);
|
ReceiveAndReplyLater<GetAuditLogMessage, Result<ImmutableArray<AuditLogItem>, UserActionFailure>>(HandleGetAuditLog);
|
||||||
ReceiveAndReplyLater<GetEventLogMessage, ImmutableArray<EventLogItem>>(HandleGetEventLog);
|
ReceiveAndReplyLater<GetEventLogMessage, Result<ImmutableArray<EventLogItem>, UserActionFailure>>(HandleGetEventLog);
|
||||||
Receive<ReplyMessage>(HandleReply);
|
Receive<ReplyMessage>(HandleReply);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,7 +100,7 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
|
|||||||
connection.Close();
|
connection.Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<LogInSuccess?> HandleLogIn(LogInMessage message) {
|
private Task<Optional<LogInSuccess>> HandleLogIn(LogInMessage message) {
|
||||||
return userLoginManager.LogIn(message.Username, message.Password);
|
return userLoginManager.LogIn(message.Username, message.Password);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,15 +109,15 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Optional<AuthenticatedUserInfo> GetAuthenticatedUser(GetAuthenticatedUser message) {
|
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) {
|
private Task<CreateOrUpdateAdministratorUserResult> HandleCreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUserMessage message) {
|
||||||
return userManager.CreateOrUpdateAdministrator(message.Username, message.Password);
|
return userManager.CreateOrUpdateAdministrator(message.Username, message.Password);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<CreateUserResult> HandleCreateUser(CreateUserMessage message) {
|
private Task<Result<CreateUserResult, UserActionFailure>> HandleCreateUser(CreateUserMessage message) {
|
||||||
return userManager.Create(message.LoggedInUserGuid, message.Username, message.Password);
|
return userManager.Create(userLoginManager.GetLoggedInUser(message.AuthToken), message.Username, message.Password);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<ImmutableArray<UserInfo>> HandleGetUsers(GetUsersMessage message) {
|
private Task<ImmutableArray<UserInfo>> HandleGetUsers(GetUsersMessage message) {
|
||||||
@ -131,28 +132,48 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
|
|||||||
return userRoleManager.GetUserRoles(message.UserGuids);
|
return userRoleManager.GetUserRoles(message.UserGuids);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<ChangeUserRolesResult> HandleChangeUserRoles(ChangeUserRolesMessage message) {
|
private Task<Result<ChangeUserRolesResult, UserActionFailure>> HandleChangeUserRoles(ChangeUserRolesMessage message) {
|
||||||
return userRoleManager.ChangeUserRoles(message.LoggedInUserGuid, message.SubjectUserGuid, message.AddToRoleGuids, message.RemoveFromRoleGuids);
|
return userRoleManager.ChangeUserRoles(userLoginManager.GetLoggedInUser(message.AuthToken), message.SubjectUserGuid, message.AddToRoleGuids, message.RemoveFromRoleGuids);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<DeleteUserResult> HandleDeleteUser(DeleteUserMessage message) {
|
private Task<Result<DeleteUserResult, UserActionFailure>> HandleDeleteUser(DeleteUserMessage message) {
|
||||||
return userManager.DeleteByGuid(message.LoggedInUserGuid, message.SubjectUserGuid);
|
return userManager.DeleteByGuid(userLoginManager.GetLoggedInUser(message.AuthToken), message.SubjectUserGuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>> HandleCreateOrUpdateInstance(CreateOrUpdateInstanceMessage message) {
|
private Task<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>> HandleCreateOrUpdateInstance(CreateOrUpdateInstanceMessage message) {
|
||||||
return agentManager.DoInstanceAction<AgentActor.CreateOrUpdateInstanceCommand, CreateOrUpdateInstanceResult>(message.Configuration.AgentGuid, new AgentActor.CreateOrUpdateInstanceCommand(message.LoggedInUserGuid, message.InstanceGuid, message.Configuration));
|
return agentManager.DoInstanceAction<AgentActor.CreateOrUpdateInstanceCommand, CreateOrUpdateInstanceResult>(
|
||||||
|
Permission.CreateInstances,
|
||||||
|
message.AuthToken,
|
||||||
|
message.Configuration.AgentGuid,
|
||||||
|
loggedInUserGuid => new AgentActor.CreateOrUpdateInstanceCommand(loggedInUserGuid, message.InstanceGuid, message.Configuration)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<Result<LaunchInstanceResult, InstanceActionFailure>> HandleLaunchInstance(LaunchInstanceMessage message) {
|
private Task<Result<LaunchInstanceResult, UserInstanceActionFailure>> HandleLaunchInstance(LaunchInstanceMessage message) {
|
||||||
return agentManager.DoInstanceAction<AgentActor.LaunchInstanceCommand, LaunchInstanceResult>(message.AgentGuid, new AgentActor.LaunchInstanceCommand(message.InstanceGuid, message.LoggedInUserGuid));
|
return agentManager.DoInstanceAction<AgentActor.LaunchInstanceCommand, LaunchInstanceResult>(
|
||||||
|
Permission.ControlInstances,
|
||||||
|
message.AuthToken,
|
||||||
|
message.AgentGuid,
|
||||||
|
loggedInUserGuid => new AgentActor.LaunchInstanceCommand(loggedInUserGuid, message.InstanceGuid)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<Result<StopInstanceResult, InstanceActionFailure>> HandleStopInstance(StopInstanceMessage message) {
|
private Task<Result<StopInstanceResult, UserInstanceActionFailure>> HandleStopInstance(StopInstanceMessage message) {
|
||||||
return agentManager.DoInstanceAction<AgentActor.StopInstanceCommand, StopInstanceResult>(message.AgentGuid, new AgentActor.StopInstanceCommand(message.InstanceGuid, message.LoggedInUserGuid, message.StopStrategy));
|
return agentManager.DoInstanceAction<AgentActor.StopInstanceCommand, StopInstanceResult>(
|
||||||
|
Permission.ControlInstances,
|
||||||
|
message.AuthToken,
|
||||||
|
message.AgentGuid,
|
||||||
|
loggedInUserGuid => new AgentActor.StopInstanceCommand(loggedInUserGuid, message.InstanceGuid, message.StopStrategy)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<Result<SendCommandToInstanceResult, InstanceActionFailure>> HandleSendCommandToInstance(SendCommandToInstanceMessage message) {
|
private Task<Result<SendCommandToInstanceResult, UserInstanceActionFailure>> HandleSendCommandToInstance(SendCommandToInstanceMessage message) {
|
||||||
return agentManager.DoInstanceAction<AgentActor.SendCommandToInstanceCommand, SendCommandToInstanceResult>(message.AgentGuid, new AgentActor.SendCommandToInstanceCommand(message.InstanceGuid, message.LoggedInUserGuid, message.Command));
|
return agentManager.DoInstanceAction<AgentActor.SendCommandToInstanceCommand, SendCommandToInstanceResult>(
|
||||||
|
Permission.ControlInstances,
|
||||||
|
message.AuthToken,
|
||||||
|
message.AgentGuid,
|
||||||
|
loggedInUserGuid => new AgentActor.SendCommandToInstanceCommand(loggedInUserGuid, message.InstanceGuid, message.Command)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<ImmutableArray<MinecraftVersion>> HandleGetMinecraftVersions(GetMinecraftVersionsMessage message) {
|
private Task<ImmutableArray<MinecraftVersion>> HandleGetMinecraftVersions(GetMinecraftVersionsMessage message) {
|
||||||
@ -163,12 +184,12 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
|
|||||||
return controllerState.AgentJavaRuntimesByGuid;
|
return controllerState.AgentJavaRuntimesByGuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<ImmutableArray<AuditLogItem>> HandleGetAuditLog(GetAuditLogMessage message) {
|
private Task<Result<ImmutableArray<AuditLogItem>, UserActionFailure>> HandleGetAuditLog(GetAuditLogMessage message) {
|
||||||
return auditLogManager.GetMostRecentItems(message.Count);
|
return auditLogManager.GetMostRecentItems(userLoginManager.GetLoggedInUser(message.AuthToken), message.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<ImmutableArray<EventLogItem>> HandleGetEventLog(GetEventLogMessage message) {
|
private Task<Result<ImmutableArray<EventLogItem>, UserActionFailure>> HandleGetEventLog(GetEventLogMessage message) {
|
||||||
return eventLogManager.GetMostRecentItems(message.Count);
|
return eventLogManager.GetMostRecentItems(userLoginManager.GetLoggedInUser(message.AuthToken), message.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleReply(ReplyMessage message) {
|
private void HandleReply(ReplyMessage message) {
|
||||||
|
@ -7,6 +7,7 @@ using Phantom.Controller.Services.Agents;
|
|||||||
using Phantom.Controller.Services.Events;
|
using Phantom.Controller.Services.Events;
|
||||||
using Phantom.Controller.Services.Instances;
|
using Phantom.Controller.Services.Instances;
|
||||||
using Phantom.Controller.Services.Users;
|
using Phantom.Controller.Services.Users;
|
||||||
|
using Phantom.Controller.Services.Users.Sessions;
|
||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
using Phantom.Utils.Logging;
|
using Phantom.Utils.Logging;
|
||||||
using Phantom.Utils.Rpc.Runtime;
|
using Phantom.Utils.Rpc.Runtime;
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
|
using Phantom.Common.Data;
|
||||||
using Phantom.Common.Data.Web.AuditLog;
|
using Phantom.Common.Data.Web.AuditLog;
|
||||||
|
using Phantom.Common.Data.Web.Users;
|
||||||
using Phantom.Controller.Database;
|
using Phantom.Controller.Database;
|
||||||
using Phantom.Controller.Database.Repositories;
|
using Phantom.Controller.Database.Repositories;
|
||||||
|
using Phantom.Controller.Services.Users.Sessions;
|
||||||
|
|
||||||
namespace Phantom.Controller.Services.Users;
|
namespace Phantom.Controller.Services.Users;
|
||||||
|
|
||||||
@ -12,7 +15,11 @@ sealed class AuditLogManager {
|
|||||||
this.dbProvider = dbProvider;
|
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();
|
await using var db = dbProvider.Lazy();
|
||||||
return await new AuditLogRepository(db).GetMostRecentItems(count, CancellationToken.None);
|
return await new AuditLogRepository(db).GetMostRecentItems(count, CancellationToken.None);
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
public static ImmutableArray<string> GetMissingPermissionsOrdered(IEnumerable<Permission> allPermissions, ImmutableHashSet<string> existingPermissionIds) {
|
||||||
return allPermissions.Select(static permission => permission.Id).Except(existingPermissionIds).Order().ToImmutableArray();
|
return allPermissions.Select(static permission => permission.Id).Except(existingPermissionIds).Order().ToImmutableArray();
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
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 permissionRepository = new PermissionRepository(db);
|
||||||
|
var userPermissions = await permissionRepository.GetAllUserPermissions(user);
|
||||||
|
var userManagedAgentGuids = await permissionRepository.GetManagedAgentGuids(user);
|
||||||
|
|
||||||
|
var userGuid = user.UserGuid;
|
||||||
|
var userInfo = new AuthenticatedUserInfo(userGuid, user.Name, userPermissions, userManagedAgentGuids);
|
||||||
|
return authenticatedUsersByGuid[userGuid] = userInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Remove(Guid userGuid) {
|
||||||
|
authenticatedUsersByGuid.Remove(userGuid, out _);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
using System.Collections.Immutable;
|
||||||
|
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 is {} info && info.CheckPermission(permission);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasAccessToAgent(Guid agentGuid) {
|
||||||
|
return AuthenticatedUserInfo is {} info && info.HasAccessToAgent(agentGuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImmutableHashSet<Guid> FilterAccessibleAgentGuids(ImmutableHashSet<Guid> agentGuids) {
|
||||||
|
return AuthenticatedUserInfo is {} info ? info.FilterAccessibleAgentGuids(agentGuids) : ImmutableHashSet<Guid>.Empty;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,140 @@
|
|||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using Phantom.Common.Data;
|
||||||
|
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<Optional<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 default;
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticatedUserInfo = await authenticatedUserCache.Update(user, db);
|
||||||
|
if (authenticatedUserInfo == null) {
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +1,10 @@
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
|
using Phantom.Common.Data;
|
||||||
using Phantom.Common.Data.Web.Users;
|
using Phantom.Common.Data.Web.Users;
|
||||||
using Phantom.Controller.Database;
|
using Phantom.Controller.Database;
|
||||||
using Phantom.Controller.Database.Entities;
|
using Phantom.Controller.Database.Entities;
|
||||||
using Phantom.Controller.Database.Repositories;
|
using Phantom.Controller.Database.Repositories;
|
||||||
|
using Phantom.Controller.Services.Users.Sessions;
|
||||||
using Phantom.Utils.Logging;
|
using Phantom.Utils.Logging;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
@ -11,9 +13,13 @@ namespace Phantom.Controller.Services.Users;
|
|||||||
sealed class UserManager {
|
sealed class UserManager {
|
||||||
private static readonly ILogger Logger = PhantomLogger.Create<UserManager>();
|
private static readonly ILogger Logger = PhantomLogger.Create<UserManager>();
|
||||||
|
|
||||||
|
private readonly AuthenticatedUserCache authenticatedUserCache;
|
||||||
|
private readonly ControllerState controllerState;
|
||||||
private readonly IDbContextProvider dbProvider;
|
private readonly IDbContextProvider dbProvider;
|
||||||
|
|
||||||
public UserManager(IDbContextProvider dbProvider) {
|
public UserManager(AuthenticatedUserCache authenticatedUserCache, ControllerState controllerState, IDbContextProvider dbProvider) {
|
||||||
|
this.authenticatedUserCache = authenticatedUserCache;
|
||||||
|
this.controllerState = controllerState;
|
||||||
this.dbProvider = dbProvider;
|
this.dbProvider = dbProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,10 +91,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();
|
await using var db = dbProvider.Lazy();
|
||||||
var userRepository = new UserRepository(db);
|
var userRepository = new UserRepository(db);
|
||||||
var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUserGuid);
|
var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUser.Guid);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var result = await userRepository.CreateUser(username, password);
|
var result = await userRepository.CreateUser(username, password);
|
||||||
@ -109,7 +119,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();
|
await using var db = dbProvider.Lazy();
|
||||||
var userRepository = new UserRepository(db);
|
var userRepository = new UserRepository(db);
|
||||||
|
|
||||||
@ -118,12 +132,18 @@ sealed class UserManager {
|
|||||||
return DeleteUserResult.NotFound;
|
return DeleteUserResult.NotFound;
|
||||||
}
|
}
|
||||||
|
|
||||||
var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUserGuid);
|
authenticatedUserCache.Remove(userGuid);
|
||||||
|
|
||||||
|
var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUser.Guid);
|
||||||
try {
|
try {
|
||||||
userRepository.DeleteUser(user);
|
userRepository.DeleteUser(user);
|
||||||
auditLogWriter.UserDeleted(user);
|
auditLogWriter.UserDeleted(user);
|
||||||
await db.Ctx.SaveChangesAsync();
|
await db.Ctx.SaveChangesAsync();
|
||||||
|
|
||||||
|
// In case the user logged in during deletion.
|
||||||
|
authenticatedUserCache.Remove(userGuid);
|
||||||
|
controllerState.UpdateOrDeleteUser(userGuid);
|
||||||
|
|
||||||
Logger.Information("Deleted user \"{Username}\" (GUID {Guid}).", user.Name, user.UserGuid);
|
Logger.Information("Deleted user \"{Username}\" (GUID {Guid}).", user.Name, user.UserGuid);
|
||||||
return DeleteUserResult.Deleted;
|
return DeleteUserResult.Deleted;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
|
using Phantom.Common.Data;
|
||||||
using Phantom.Common.Data.Web.Users;
|
using Phantom.Common.Data.Web.Users;
|
||||||
using Phantom.Controller.Database;
|
using Phantom.Controller.Database;
|
||||||
using Phantom.Controller.Database.Repositories;
|
using Phantom.Controller.Database.Repositories;
|
||||||
|
using Phantom.Controller.Services.Users.Sessions;
|
||||||
using Phantom.Utils.Logging;
|
using Phantom.Utils.Logging;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
@ -10,9 +12,13 @@ namespace Phantom.Controller.Services.Users;
|
|||||||
sealed class UserRoleManager {
|
sealed class UserRoleManager {
|
||||||
private static readonly ILogger Logger = PhantomLogger.Create<UserRoleManager>();
|
private static readonly ILogger Logger = PhantomLogger.Create<UserRoleManager>();
|
||||||
|
|
||||||
|
private readonly AuthenticatedUserCache authenticatedUserCache;
|
||||||
|
private readonly ControllerState controllerState;
|
||||||
private readonly IDbContextProvider dbProvider;
|
private readonly IDbContextProvider dbProvider;
|
||||||
|
|
||||||
public UserRoleManager(IDbContextProvider dbProvider) {
|
public UserRoleManager(AuthenticatedUserCache authenticatedUserCache, ControllerState controllerState, IDbContextProvider dbProvider) {
|
||||||
|
this.authenticatedUserCache = authenticatedUserCache;
|
||||||
|
this.controllerState = controllerState;
|
||||||
this.dbProvider = dbProvider;
|
this.dbProvider = dbProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -21,7 +27,11 @@ sealed class UserRoleManager {
|
|||||||
return await new UserRoleRepository(db).GetRoleGuidsByUserGuid(userGuids);
|
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();
|
await using var db = dbProvider.Lazy();
|
||||||
var userRepository = new UserRepository(db);
|
var userRepository = new UserRepository(db);
|
||||||
|
|
||||||
@ -32,7 +42,7 @@ sealed class UserRoleManager {
|
|||||||
|
|
||||||
var roleRepository = new RoleRepository(db);
|
var roleRepository = new RoleRepository(db);
|
||||||
var userRoleRepository = new UserRoleRepository(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));
|
var rolesByGuid = await roleRepository.GetByGuids(addToRoleGuids.Union(removeFromRoleGuids));
|
||||||
|
|
||||||
@ -62,6 +72,9 @@ sealed class UserRoleManager {
|
|||||||
auditLogWriter.UserRolesChanged(user, addedToRoleNames, removedFromRoleNames);
|
auditLogWriter.UserRolesChanged(user, addedToRoleNames, removedFromRoleNames);
|
||||||
await db.Ctx.SaveChangesAsync();
|
await db.Ctx.SaveChangesAsync();
|
||||||
|
|
||||||
|
await authenticatedUserCache.Update(user, db);
|
||||||
|
controllerState.UpdateOrDeleteUser(user.UserGuid);
|
||||||
|
|
||||||
Logger.Information("Changed roles for user \"{Username}\" (GUID {Guid}).", user.Name, user.UserGuid);
|
Logger.Information("Changed roles for user \"{Username}\" (GUID {Guid}).", user.Name, user.UserGuid);
|
||||||
return new ChangeUserRolesResult(addedToRoleGuids.ToImmutable(), removedFromRoleGuids.ToImmutable());
|
return new ChangeUserRolesResult(addedToRoleGuids.ToImmutable(), removedFromRoleGuids.ToImmutable());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
@ -17,6 +17,10 @@ public static class TaskExtensions {
|
|||||||
return task.ContinueOnActor(result => mapper(result, arg));
|
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) {
|
private static Task<TResult> MapResult<TSource, TResult>(Task<TSource> task, Func<TSource, TResult> mapper, TaskCompletionSource<TResult> completionSource) {
|
||||||
if (task.IsFaulted) {
|
if (task.IsFaulted) {
|
||||||
completionSource.SetException(task.Exception.InnerExceptions);
|
completionSource.SetException(task.Exception.InnerExceptions);
|
||||||
|
3
Utils/Phantom.Utils/Result/Err.cs
Normal file
3
Utils/Phantom.Utils/Result/Err.cs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
namespace Phantom.Utils.Result;
|
||||||
|
|
||||||
|
public sealed record Err<T>(T Error) : Result;
|
3
Utils/Phantom.Utils/Result/Ok.cs
Normal file
3
Utils/Phantom.Utils/Result/Ok.cs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
namespace Phantom.Utils.Result;
|
||||||
|
|
||||||
|
public sealed record Ok<T>(T Value) : Result;
|
5
Utils/Phantom.Utils/Result/Result.cs
Normal file
5
Utils/Phantom.Utils/Result/Result.cs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
namespace Phantom.Utils.Result;
|
||||||
|
|
||||||
|
public abstract record Result {
|
||||||
|
private protected Result() {}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
@using Phantom.Web.Components.Utils
|
@using Phantom.Web.Components.Utils
|
||||||
@if (messageLines.Length > 0) {
|
@if (messageLines.Length > 0) {
|
||||||
<div class="form-submit-errors text-danger">
|
<div class="form-submit-errors text-danger" role="alert">
|
||||||
@for (int i = 0; i < messageLines.Length; i++) {
|
@for (int i = 0; i < messageLines.Length; i++) {
|
||||||
@messageLines[i]
|
@messageLines[i]
|
||||||
if (i < messageLines.Length - 1) {
|
if (i < messageLines.Length - 1) {
|
||||||
|
@ -17,9 +17,9 @@ public abstract class PhantomComponent : ComponentBase, IDisposable {
|
|||||||
|
|
||||||
protected CancellationToken CancellationToken => cancellationTokenSource.Token;
|
protected CancellationToken CancellationToken => cancellationTokenSource.Token;
|
||||||
|
|
||||||
protected async Task<Guid?> GetUserGuid() {
|
protected async Task<AuthenticatedUser?> GetAuthenticatedUser() {
|
||||||
var authenticationState = await AuthenticationStateTask;
|
var authenticationState = await AuthenticationStateTask;
|
||||||
return authenticationState.TryGetGuid();
|
return authenticationState.GetAuthenticatedUser();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task<bool> CheckPermission(Permission permission) {
|
protected async Task<bool> CheckPermission(Permission permission) {
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using Phantom.Common.Data.Web.Agent;
|
using Phantom.Common.Data.Web.Agent;
|
||||||
using Phantom.Utils.Events;
|
using Phantom.Utils.Events;
|
||||||
using Phantom.Utils.Logging;
|
using Phantom.Utils.Logging;
|
||||||
|
using Phantom.Web.Services.Authentication;
|
||||||
|
|
||||||
namespace Phantom.Web.Services.Agents;
|
namespace Phantom.Web.Services.Agents;
|
||||||
|
|
||||||
@ -18,7 +19,13 @@ public sealed class AgentManager {
|
|||||||
return agents.Value;
|
return agents.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ImmutableDictionary<Guid, Agent> ToDictionaryByGuid() {
|
public ImmutableDictionary<Guid, Agent> ToDictionaryByGuid(AuthenticatedUser? authenticatedUser) {
|
||||||
return agents.Value.ToImmutableDictionary(static agent => agent.AgentGuid);
|
if (authenticatedUser == null) {
|
||||||
|
return ImmutableDictionary<Guid, Agent>.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return agents.Value
|
||||||
|
.Where(agent => authenticatedUser.Info.HasAccessToAgent(agent.AgentGuid))
|
||||||
|
.ToImmutableDictionary(static agent => agent.AgentGuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
using System.Collections.Immutable;
|
||||||
|
using Phantom.Common.Data.Web.Users;
|
||||||
|
|
||||||
|
namespace Phantom.Web.Services.Authentication;
|
||||||
|
|
||||||
|
public sealed record AuthenticatedUser(AuthenticatedUserInfo Info, ImmutableArray<byte> Token);
|
@ -5,23 +5,27 @@ using Phantom.Common.Data.Web.Users;
|
|||||||
namespace Phantom.Web.Services.Authentication;
|
namespace Phantom.Web.Services.Authentication;
|
||||||
|
|
||||||
public static class AuthenticationStateExtensions {
|
public static class AuthenticationStateExtensions {
|
||||||
public static Guid? TryGetGuid(this AuthenticationState authenticationState) {
|
public static AuthenticatedUser? GetAuthenticatedUser(this AuthenticationState authenticationState) {
|
||||||
return authenticationState.User is CustomClaimsPrincipal customUser ? customUser.UserInfo.Guid : null;
|
return authenticationState.User.GetAuthenticatedUser();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PermissionSet GetPermissions(this ClaimsPrincipal user) {
|
public static AuthenticatedUser? GetAuthenticatedUser(this ClaimsPrincipal claimsPrincipal) {
|
||||||
return user is CustomClaimsPrincipal customUser ? customUser.UserInfo.Permissions : PermissionSet.None;
|
return claimsPrincipal is CustomClaimsPrincipal principal ? principal.User : null;
|
||||||
}
|
|
||||||
|
|
||||||
public static bool CheckPermission(this ClaimsPrincipal user, Permission permission) {
|
|
||||||
return user.GetPermissions().Check(permission);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PermissionSet GetPermissions(this AuthenticationState authenticationState) {
|
public static PermissionSet GetPermissions(this AuthenticationState authenticationState) {
|
||||||
return authenticationState.User.GetPermissions();
|
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) {
|
public static bool CheckPermission(this AuthenticationState authenticationState, Permission permission) {
|
||||||
return authenticationState.User.CheckPermission(permission);
|
return authenticationState.User.CheckPermission(permission);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool CheckPermission(this ClaimsPrincipal claimsPrincipal, Permission permission) {
|
||||||
|
return claimsPrincipal.GetPermissions().Check(permission);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,41 +4,118 @@ using Microsoft.AspNetCore.Components.Server;
|
|||||||
using Phantom.Common.Data;
|
using Phantom.Common.Data;
|
||||||
using Phantom.Common.Data.Web.Users;
|
using Phantom.Common.Data.Web.Users;
|
||||||
using Phantom.Common.Messages.Web.ToController;
|
using Phantom.Common.Messages.Web.ToController;
|
||||||
|
using Phantom.Utils.Logging;
|
||||||
using Phantom.Web.Services.Rpc;
|
using Phantom.Web.Services.Rpc;
|
||||||
|
using ILogger = Serilog.ILogger;
|
||||||
|
|
||||||
namespace Phantom.Web.Services.Authentication;
|
namespace Phantom.Web.Services.Authentication;
|
||||||
|
|
||||||
public sealed class CustomAuthenticationStateProvider : ServerAuthenticationStateProvider {
|
public sealed class CustomAuthenticationStateProvider : ServerAuthenticationStateProvider, IAsyncDisposable {
|
||||||
|
private static readonly ILogger Logger = PhantomLogger.Create<CustomAuthenticationStateProvider>();
|
||||||
|
|
||||||
|
private readonly UserSessionRefreshManager sessionRefreshManager;
|
||||||
private readonly UserSessionBrowserStorage sessionBrowserStorage;
|
private readonly UserSessionBrowserStorage sessionBrowserStorage;
|
||||||
private readonly ControllerConnection controllerConnection;
|
private readonly ControllerConnection controllerConnection;
|
||||||
private bool isLoaded;
|
|
||||||
|
|
||||||
public CustomAuthenticationStateProvider(UserSessionBrowserStorage sessionBrowserStorage, ControllerConnection controllerConnection) {
|
private readonly SemaphoreSlim loadSemaphore = new (1);
|
||||||
|
private bool isLoaded = false;
|
||||||
|
private CancellationTokenSource? loadCancellationTokenSource;
|
||||||
|
private UserSessionRefreshManager.EventHolder? userRefreshEventHolder;
|
||||||
|
|
||||||
|
public CustomAuthenticationStateProvider(UserSessionRefreshManager sessionRefreshManager, UserSessionBrowserStorage sessionBrowserStorage, ControllerConnection controllerConnection) {
|
||||||
|
this.sessionRefreshManager = sessionRefreshManager;
|
||||||
this.sessionBrowserStorage = sessionBrowserStorage;
|
this.sessionBrowserStorage = sessionBrowserStorage;
|
||||||
this.controllerConnection = controllerConnection;
|
this.controllerConnection = controllerConnection;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<AuthenticationState> GetAuthenticationStateAsync() {
|
public override async Task<AuthenticationState> GetAuthenticationStateAsync() {
|
||||||
if (!isLoaded) {
|
if (!isLoaded) {
|
||||||
var stored = await sessionBrowserStorage.Get();
|
await LoadSession();
|
||||||
if (stored != null) {
|
|
||||||
var session = await controllerConnection.Send<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(new GetAuthenticatedUser(stored.UserGuid, stored.Token), TimeSpan.FromSeconds(30));
|
|
||||||
if (session.Value is {} userInfo) {
|
|
||||||
SetLoadedSession(userInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return await base.GetAuthenticationStateAsync();
|
return await base.GetAuthenticationStateAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void SetLoadedSession(AuthenticatedUserInfo user) {
|
private async Task LoadSession() {
|
||||||
|
await CancelCurrentLoad();
|
||||||
|
await loadSemaphore.WaitAsync(CancellationToken.None);
|
||||||
|
|
||||||
|
loadCancellationTokenSource = new CancellationTokenSource();
|
||||||
|
CancellationToken cancellationToken = loadCancellationTokenSource.Token;
|
||||||
|
|
||||||
|
try {
|
||||||
|
var authenticatedUser = await TryGetSession(cancellationToken);
|
||||||
|
if (authenticatedUser != null) {
|
||||||
|
SetLoadedSession(authenticatedUser);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SetUnloadedSession();
|
||||||
|
}
|
||||||
|
} catch (OperationCanceledException) {
|
||||||
|
SetUnloadedSession();
|
||||||
|
} catch (Exception e) {
|
||||||
|
SetUnloadedSession();
|
||||||
|
Logger.Error(e, "Could not load user session.");
|
||||||
|
} finally {
|
||||||
|
loadCancellationTokenSource.Dispose();
|
||||||
|
loadCancellationTokenSource = null;
|
||||||
|
loadSemaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CancelCurrentLoad() {
|
||||||
|
var cancellationTokenSource = loadCancellationTokenSource;
|
||||||
|
if (cancellationTokenSource != null) {
|
||||||
|
await cancellationTokenSource.CancelAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<AuthenticatedUser?> TryGetSession(CancellationToken cancellationToken) {
|
||||||
|
var stored = await sessionBrowserStorage.Get();
|
||||||
|
if (stored == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var userGuid = stored.UserGuid;
|
||||||
|
var authToken = stored.Token;
|
||||||
|
|
||||||
|
if (userRefreshEventHolder == null) {
|
||||||
|
userRefreshEventHolder = sessionRefreshManager.GetEventHolder(userGuid);
|
||||||
|
userRefreshEventHolder.UserNeedsRefresh += OnUserNeedsRefresh;
|
||||||
|
}
|
||||||
|
|
||||||
|
var session = await controllerConnection.Send<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(new GetAuthenticatedUser(userGuid, authToken), TimeSpan.FromSeconds(30), cancellationToken);
|
||||||
|
if (session.Value is {} userInfo) {
|
||||||
|
return new AuthenticatedUser(userInfo, authToken);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetLoadedSession(AuthenticatedUser authenticatedUser) {
|
||||||
|
SetAuthenticationState(Task.FromResult(new AuthenticationState(new CustomClaimsPrincipal(authenticatedUser))));
|
||||||
isLoaded = true;
|
isLoaded = true;
|
||||||
SetAuthenticationState(Task.FromResult(new AuthenticationState(new CustomClaimsPrincipal(user))));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void SetUnloadedSession() {
|
internal void SetUnloadedSession() {
|
||||||
isLoaded = false;
|
|
||||||
SetAuthenticationState(Task.FromResult(new AuthenticationState(new ClaimsPrincipal())));
|
SetAuthenticationState(Task.FromResult(new AuthenticationState(new ClaimsPrincipal())));
|
||||||
|
isLoaded = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnUserNeedsRefresh(object? sender, EventArgs args) {
|
||||||
|
_ = LoadSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync() {
|
||||||
|
if (userRefreshEventHolder != null) {
|
||||||
|
userRefreshEventHolder.UserNeedsRefresh -= OnUserNeedsRefresh;
|
||||||
|
userRefreshEventHolder = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await CancelCurrentLoad();
|
||||||
|
loadSemaphore.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,10 @@ using Phantom.Common.Data.Web.Users;
|
|||||||
namespace Phantom.Web.Services.Authentication;
|
namespace Phantom.Web.Services.Authentication;
|
||||||
|
|
||||||
sealed class CustomClaimsPrincipal : ClaimsPrincipal {
|
sealed class CustomClaimsPrincipal : ClaimsPrincipal {
|
||||||
internal AuthenticatedUserInfo UserInfo { get; }
|
internal AuthenticatedUser User { get; }
|
||||||
|
|
||||||
internal CustomClaimsPrincipal(AuthenticatedUserInfo userInfo) : base(GetIdentity(userInfo)) {
|
internal CustomClaimsPrincipal(AuthenticatedUser user) : base(GetIdentity(user.Info)) {
|
||||||
UserInfo = userInfo;
|
User = user;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ClaimsIdentity GetIdentity(AuthenticatedUserInfo userInfo) {
|
private static ClaimsIdentity GetIdentity(AuthenticatedUserInfo userInfo) {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Phantom.Common.Data.Web.Users;
|
using Phantom.Common.Data;
|
||||||
|
using Phantom.Common.Data.Web.Users;
|
||||||
using Phantom.Common.Messages.Web.ToController;
|
using Phantom.Common.Messages.Web.ToController;
|
||||||
using Phantom.Utils.Logging;
|
using Phantom.Utils.Logging;
|
||||||
using Phantom.Web.Services.Rpc;
|
using Phantom.Web.Services.Rpc;
|
||||||
@ -22,24 +23,23 @@ public sealed class UserLoginManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> LogIn(string username, string password, string? returnUrl = null) {
|
public async Task<bool> LogIn(string username, string password, string? returnUrl = null) {
|
||||||
LogInSuccess? success;
|
Optional<LogInSuccess> result;
|
||||||
try {
|
try {
|
||||||
success = await controllerConnection.Send<LogInMessage, LogInSuccess?>(new LogInMessage(username, password), TimeSpan.FromSeconds(30));
|
result = await controllerConnection.Send<LogInMessage, Optional<LogInSuccess>>(new LogInMessage(username, password), TimeSpan.FromSeconds(30));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Logger.Error(e, "Could not log in {Username}.", username);
|
Logger.Error(e, "Could not log in {Username}.", username);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (success == null) {
|
if (result.Value is not var (userInfo, authToken)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.Information("Successfully logged in {Username}.", username);
|
Logger.Information("Successfully logged in {Username}.", username);
|
||||||
|
|
||||||
var userInfo = success.UserInfo;
|
authenticationStateProvider.SetUnloadedSession();
|
||||||
|
await sessionBrowserStorage.Store(userInfo.Guid, authToken);
|
||||||
await sessionBrowserStorage.Store(userInfo.Guid, success.Token);
|
await authenticationStateProvider.GetAuthenticationStateAsync();
|
||||||
authenticationStateProvider.SetLoadedSession(userInfo);
|
|
||||||
await navigation.NavigateTo(returnUrl ?? string.Empty);
|
await navigation.NavigateTo(returnUrl ?? string.Empty);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace Phantom.Web.Services.Authentication;
|
||||||
|
|
||||||
|
public sealed class UserSessionRefreshManager {
|
||||||
|
private readonly ConcurrentDictionary<Guid, EventHolder> userUpdateEventHoldersByUserGuid = new ();
|
||||||
|
|
||||||
|
internal EventHolder GetEventHolder(Guid userGuid) {
|
||||||
|
return userUpdateEventHoldersByUserGuid.GetOrAdd(userGuid, static _ => new EventHolder());
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void RefreshUser(Guid userGuid) {
|
||||||
|
if (userUpdateEventHoldersByUserGuid.TryGetValue(userGuid, out var eventHolder)) {
|
||||||
|
eventHolder.Notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class EventHolder {
|
||||||
|
public event EventHandler? UserNeedsRefresh;
|
||||||
|
|
||||||
|
internal void Notify() {
|
||||||
|
UserNeedsRefresh?.Invoke(null, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,9 @@
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
|
using Phantom.Common.Data;
|
||||||
using Phantom.Common.Data.Web.EventLog;
|
using Phantom.Common.Data.Web.EventLog;
|
||||||
|
using Phantom.Common.Data.Web.Users;
|
||||||
using Phantom.Common.Messages.Web.ToController;
|
using Phantom.Common.Messages.Web.ToController;
|
||||||
|
using Phantom.Web.Services.Authentication;
|
||||||
using Phantom.Web.Services.Rpc;
|
using Phantom.Web.Services.Rpc;
|
||||||
|
|
||||||
namespace Phantom.Web.Services.Events;
|
namespace Phantom.Web.Services.Events;
|
||||||
@ -12,8 +15,13 @@ public sealed class EventLogManager {
|
|||||||
this.controllerConnection = controllerConnection;
|
this.controllerConnection = controllerConnection;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<ImmutableArray<EventLogItem>> GetMostRecentItems(int count, CancellationToken cancellationToken) {
|
public async Task<Result<ImmutableArray<EventLogItem>, UserActionFailure>> GetMostRecentItems(AuthenticatedUser? authenticatedUser, int count, CancellationToken cancellationToken) {
|
||||||
var message = new GetEventLogMessage(count);
|
if (authenticatedUser != null && authenticatedUser.Info.CheckPermission(Permission.ViewEvents)) {
|
||||||
return controllerConnection.Send<GetEventLogMessage, ImmutableArray<EventLogItem>>(message, cancellationToken);
|
var message = new GetEventLogMessage(authenticatedUser.Token, count);
|
||||||
|
return await controllerConnection.Send<GetEventLogMessage, Result<ImmutableArray<EventLogItem>, UserActionFailure>>(message, cancellationToken);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return UserActionFailure.NotAuthorized;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,9 +4,11 @@ using Phantom.Common.Data.Instance;
|
|||||||
using Phantom.Common.Data.Minecraft;
|
using Phantom.Common.Data.Minecraft;
|
||||||
using Phantom.Common.Data.Replies;
|
using Phantom.Common.Data.Replies;
|
||||||
using Phantom.Common.Data.Web.Instance;
|
using Phantom.Common.Data.Web.Instance;
|
||||||
|
using Phantom.Common.Data.Web.Users;
|
||||||
using Phantom.Common.Messages.Web.ToController;
|
using Phantom.Common.Messages.Web.ToController;
|
||||||
using Phantom.Utils.Events;
|
using Phantom.Utils.Events;
|
||||||
using Phantom.Utils.Logging;
|
using Phantom.Utils.Logging;
|
||||||
|
using Phantom.Web.Services.Authentication;
|
||||||
using Phantom.Web.Services.Rpc;
|
using Phantom.Web.Services.Rpc;
|
||||||
|
|
||||||
namespace Phantom.Web.Services.Instances;
|
namespace Phantom.Web.Services.Instances;
|
||||||
@ -31,27 +33,52 @@ public sealed class InstanceManager {
|
|||||||
return instances.Value;
|
return instances.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Instance? GetByGuid(Guid instanceGuid) {
|
public Instance? GetByGuid(AuthenticatedUser? authenticatedUser, Guid instanceGuid) {
|
||||||
return instances.Value.GetValueOrDefault(instanceGuid);
|
if (authenticatedUser == null) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>> CreateOrUpdateInstance(Guid loggedInUserGuid, Guid instanceGuid, InstanceConfiguration configuration, CancellationToken cancellationToken) {
|
var instance = instances.Value.GetValueOrDefault(instanceGuid);
|
||||||
var message = new CreateOrUpdateInstanceMessage(loggedInUserGuid, instanceGuid, configuration);
|
return instance != null && authenticatedUser.Info.HasAccessToAgent(instance.Configuration.AgentGuid) ? instance : null;
|
||||||
return controllerConnection.Send<CreateOrUpdateInstanceMessage, Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(message, cancellationToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<Result<LaunchInstanceResult, InstanceActionFailure>> LaunchInstance(Guid loggedInUserGuid, Guid agentGuid, Guid instanceGuid, CancellationToken cancellationToken) {
|
public async Task<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>> CreateOrUpdateInstance(AuthenticatedUser? authenticatedUser, Guid instanceGuid, InstanceConfiguration configuration, CancellationToken cancellationToken) {
|
||||||
var message = new LaunchInstanceMessage(loggedInUserGuid, agentGuid, instanceGuid);
|
if (authenticatedUser != null && authenticatedUser.Info.CheckPermission(Permission.CreateInstances)) {
|
||||||
return controllerConnection.Send<LaunchInstanceMessage, Result<LaunchInstanceResult, InstanceActionFailure>>(message, cancellationToken);
|
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<StopInstanceResult, InstanceActionFailure>> StopInstance(Guid loggedInUserGuid, Guid agentGuid, Guid instanceGuid, MinecraftStopStrategy stopStrategy, CancellationToken cancellationToken) {
|
public async Task<Result<LaunchInstanceResult, UserInstanceActionFailure>> LaunchInstance(AuthenticatedUser? authenticatedUser, Guid agentGuid, Guid instanceGuid, CancellationToken cancellationToken) {
|
||||||
var message = new StopInstanceMessage(loggedInUserGuid, agentGuid, instanceGuid, stopStrategy);
|
if (authenticatedUser != null && authenticatedUser.Info.CheckPermission(Permission.ControlInstances)) {
|
||||||
return controllerConnection.Send<StopInstanceMessage, Result<StopInstanceResult, InstanceActionFailure>>(message, cancellationToken);
|
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<SendCommandToInstanceResult, InstanceActionFailure>> SendCommandToInstance(Guid loggedInUserGuid, Guid agentGuid, Guid instanceGuid, string command, CancellationToken cancellationToken) {
|
public async Task<Result<StopInstanceResult, UserInstanceActionFailure>> StopInstance(AuthenticatedUser? authenticatedUser, Guid agentGuid, Guid instanceGuid, MinecraftStopStrategy stopStrategy, CancellationToken cancellationToken) {
|
||||||
var message = new SendCommandToInstanceMessage(loggedInUserGuid, agentGuid, instanceGuid, command);
|
if (authenticatedUser != null && authenticatedUser.Info.CheckPermission(Permission.ControlInstances)) {
|
||||||
return controllerConnection.Send<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, InstanceActionFailure>>(message, cancellationToken);
|
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 async Task<Result<SendCommandToInstanceResult, UserInstanceActionFailure>> SendCommandToInstance(AuthenticatedUser? authenticatedUser, Guid agentGuid, Guid instanceGuid, string command, CancellationToken cancellationToken) {
|
||||||
|
if (authenticatedUser != null && authenticatedUser.Info.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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ public static class PhantomWebServices {
|
|||||||
|
|
||||||
services.AddSingleton<UserManager>();
|
services.AddSingleton<UserManager>();
|
||||||
services.AddSingleton<AuditLogManager>();
|
services.AddSingleton<AuditLogManager>();
|
||||||
|
services.AddSingleton<UserSessionRefreshManager>();
|
||||||
services.AddScoped<UserLoginManager>();
|
services.AddScoped<UserLoginManager>();
|
||||||
services.AddScoped<UserSessionBrowserStorage>();
|
services.AddScoped<UserSessionBrowserStorage>();
|
||||||
|
|
||||||
|
@ -4,12 +4,20 @@ using Phantom.Common.Messages.Web.ToWeb;
|
|||||||
using Phantom.Utils.Actor;
|
using Phantom.Utils.Actor;
|
||||||
using Phantom.Utils.Rpc.Runtime;
|
using Phantom.Utils.Rpc.Runtime;
|
||||||
using Phantom.Web.Services.Agents;
|
using Phantom.Web.Services.Agents;
|
||||||
|
using Phantom.Web.Services.Authentication;
|
||||||
using Phantom.Web.Services.Instances;
|
using Phantom.Web.Services.Instances;
|
||||||
|
|
||||||
namespace Phantom.Web.Services.Rpc;
|
namespace Phantom.Web.Services.Rpc;
|
||||||
|
|
||||||
sealed class ControllerMessageHandlerActor : ReceiveActor<IMessageToWeb> {
|
sealed class ControllerMessageHandlerActor : ReceiveActor<IMessageToWeb> {
|
||||||
public readonly record struct Init(RpcConnectionToServer<IMessageToController> Connection, AgentManager AgentManager, InstanceManager InstanceManager, InstanceLogManager InstanceLogManager, TaskCompletionSource<bool> RegisterSuccessWaiter);
|
public readonly record struct Init(
|
||||||
|
RpcConnectionToServer<IMessageToController> Connection,
|
||||||
|
AgentManager AgentManager,
|
||||||
|
InstanceManager InstanceManager,
|
||||||
|
InstanceLogManager InstanceLogManager,
|
||||||
|
UserSessionRefreshManager UserSessionRefreshManager,
|
||||||
|
TaskCompletionSource<bool> RegisterSuccessWaiter
|
||||||
|
);
|
||||||
|
|
||||||
public static Props<IMessageToWeb> Factory(Init init) {
|
public static Props<IMessageToWeb> Factory(Init init) {
|
||||||
return Props<IMessageToWeb>.Create(() => new ControllerMessageHandlerActor(init), new ActorConfiguration { SupervisorStrategy = SupervisorStrategies.Resume });
|
return Props<IMessageToWeb>.Create(() => new ControllerMessageHandlerActor(init), new ActorConfiguration { SupervisorStrategy = SupervisorStrategies.Resume });
|
||||||
@ -19,6 +27,7 @@ sealed class ControllerMessageHandlerActor : ReceiveActor<IMessageToWeb> {
|
|||||||
private readonly AgentManager agentManager;
|
private readonly AgentManager agentManager;
|
||||||
private readonly InstanceManager instanceManager;
|
private readonly InstanceManager instanceManager;
|
||||||
private readonly InstanceLogManager instanceLogManager;
|
private readonly InstanceLogManager instanceLogManager;
|
||||||
|
private readonly UserSessionRefreshManager userSessionRefreshManager;
|
||||||
private readonly TaskCompletionSource<bool> registerSuccessWaiter;
|
private readonly TaskCompletionSource<bool> registerSuccessWaiter;
|
||||||
|
|
||||||
private ControllerMessageHandlerActor(Init init) {
|
private ControllerMessageHandlerActor(Init init) {
|
||||||
@ -26,12 +35,14 @@ sealed class ControllerMessageHandlerActor : ReceiveActor<IMessageToWeb> {
|
|||||||
this.agentManager = init.AgentManager;
|
this.agentManager = init.AgentManager;
|
||||||
this.instanceManager = init.InstanceManager;
|
this.instanceManager = init.InstanceManager;
|
||||||
this.instanceLogManager = init.InstanceLogManager;
|
this.instanceLogManager = init.InstanceLogManager;
|
||||||
|
this.userSessionRefreshManager = init.UserSessionRefreshManager;
|
||||||
this.registerSuccessWaiter = init.RegisterSuccessWaiter;
|
this.registerSuccessWaiter = init.RegisterSuccessWaiter;
|
||||||
|
|
||||||
Receive<RegisterWebResultMessage>(HandleRegisterWebResult);
|
Receive<RegisterWebResultMessage>(HandleRegisterWebResult);
|
||||||
Receive<RefreshAgentsMessage>(HandleRefreshAgents);
|
Receive<RefreshAgentsMessage>(HandleRefreshAgents);
|
||||||
Receive<RefreshInstancesMessage>(HandleRefreshInstances);
|
Receive<RefreshInstancesMessage>(HandleRefreshInstances);
|
||||||
Receive<InstanceOutputMessage>(HandleInstanceOutput);
|
Receive<InstanceOutputMessage>(HandleInstanceOutput);
|
||||||
|
Receive<RefreshUserSessionMessage>(HandleRefreshUserSession);
|
||||||
Receive<ReplyMessage>(HandleReply);
|
Receive<ReplyMessage>(HandleReply);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,6 +62,10 @@ sealed class ControllerMessageHandlerActor : ReceiveActor<IMessageToWeb> {
|
|||||||
instanceLogManager.AddLines(message.InstanceGuid, message.Lines);
|
instanceLogManager.AddLines(message.InstanceGuid, message.Lines);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void HandleRefreshUserSession(RefreshUserSessionMessage message) {
|
||||||
|
userSessionRefreshManager.RefreshUser(message.UserGuid);
|
||||||
|
}
|
||||||
|
|
||||||
private void HandleReply(ReplyMessage message) {
|
private void HandleReply(ReplyMessage message) {
|
||||||
connection.Receive(message);
|
connection.Receive(message);
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ using Phantom.Utils.Actor;
|
|||||||
using Phantom.Utils.Rpc.Runtime;
|
using Phantom.Utils.Rpc.Runtime;
|
||||||
using Phantom.Utils.Tasks;
|
using Phantom.Utils.Tasks;
|
||||||
using Phantom.Web.Services.Agents;
|
using Phantom.Web.Services.Agents;
|
||||||
|
using Phantom.Web.Services.Authentication;
|
||||||
using Phantom.Web.Services.Instances;
|
using Phantom.Web.Services.Instances;
|
||||||
|
|
||||||
namespace Phantom.Web.Services.Rpc;
|
namespace Phantom.Web.Services.Rpc;
|
||||||
@ -13,6 +14,7 @@ public sealed class ControllerMessageHandlerFactory {
|
|||||||
private readonly AgentManager agentManager;
|
private readonly AgentManager agentManager;
|
||||||
private readonly InstanceManager instanceManager;
|
private readonly InstanceManager instanceManager;
|
||||||
private readonly InstanceLogManager instanceLogManager;
|
private readonly InstanceLogManager instanceLogManager;
|
||||||
|
private readonly UserSessionRefreshManager userSessionRefreshManager;
|
||||||
|
|
||||||
private readonly TaskCompletionSource<bool> registerSuccessWaiter = AsyncTasks.CreateCompletionSource<bool>();
|
private readonly TaskCompletionSource<bool> registerSuccessWaiter = AsyncTasks.CreateCompletionSource<bool>();
|
||||||
|
|
||||||
@ -20,15 +22,17 @@ public sealed class ControllerMessageHandlerFactory {
|
|||||||
|
|
||||||
private int messageHandlerId = 0;
|
private int messageHandlerId = 0;
|
||||||
|
|
||||||
public ControllerMessageHandlerFactory(RpcConnectionToServer<IMessageToController> connection, AgentManager agentManager, InstanceManager instanceManager, InstanceLogManager instanceLogManager) {
|
public ControllerMessageHandlerFactory(RpcConnectionToServer<IMessageToController> connection, AgentManager agentManager, InstanceManager instanceManager, InstanceLogManager instanceLogManager, UserSessionRefreshManager userSessionRefreshManager) {
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
this.agentManager = agentManager;
|
this.agentManager = agentManager;
|
||||||
this.instanceManager = instanceManager;
|
this.instanceManager = instanceManager;
|
||||||
this.instanceLogManager = instanceLogManager;
|
this.instanceLogManager = instanceLogManager;
|
||||||
|
this.userSessionRefreshManager = userSessionRefreshManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ActorRef<IMessageToWeb> Create(IActorRefFactory actorSystem) {
|
public ActorRef<IMessageToWeb> Create(IActorRefFactory actorSystem) {
|
||||||
int id = Interlocked.Increment(ref messageHandlerId);
|
var init = new ControllerMessageHandlerActor.Init(connection, agentManager, instanceManager, instanceLogManager, userSessionRefreshManager, registerSuccessWaiter);
|
||||||
return actorSystem.ActorOf(ControllerMessageHandlerActor.Factory(new ControllerMessageHandlerActor.Init(connection, agentManager, instanceManager, instanceLogManager, registerSuccessWaiter)), "ControllerMessageHandler-" + id);
|
var name = "ControllerMessageHandler-" + Interlocked.Increment(ref messageHandlerId);
|
||||||
|
return actorSystem.ActorOf(ControllerMessageHandlerActor.Factory(init), name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
|
using Phantom.Common.Data;
|
||||||
using Phantom.Common.Data.Web.AuditLog;
|
using Phantom.Common.Data.Web.AuditLog;
|
||||||
|
using Phantom.Common.Data.Web.Users;
|
||||||
using Phantom.Common.Messages.Web.ToController;
|
using Phantom.Common.Messages.Web.ToController;
|
||||||
|
using Phantom.Web.Services.Authentication;
|
||||||
using Phantom.Web.Services.Rpc;
|
using Phantom.Web.Services.Rpc;
|
||||||
|
|
||||||
namespace Phantom.Web.Services.Users;
|
namespace Phantom.Web.Services.Users;
|
||||||
@ -12,8 +15,13 @@ public sealed class AuditLogManager {
|
|||||||
this.controllerConnection = controllerConnection;
|
this.controllerConnection = controllerConnection;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<ImmutableArray<AuditLogItem>> GetMostRecentItems(int count, CancellationToken cancellationToken) {
|
public async Task<Result<ImmutableArray<AuditLogItem>, UserActionFailure>> GetMostRecentItems(AuthenticatedUser? authenticatedUser, int count, CancellationToken cancellationToken) {
|
||||||
var message = new GetAuditLogMessage(count);
|
if (authenticatedUser != null && authenticatedUser.Info.CheckPermission(Permission.ViewAudit)) {
|
||||||
return controllerConnection.Send<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(message, cancellationToken);
|
var message = new GetAuditLogMessage(authenticatedUser.Token, count);
|
||||||
|
return await controllerConnection.Send<GetAuditLogMessage, Result<ImmutableArray<AuditLogItem>, UserActionFailure>>(message, cancellationToken);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return UserActionFailure.NotAuthorized;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
|
using Phantom.Common.Data;
|
||||||
using Phantom.Common.Data.Web.Users;
|
using Phantom.Common.Data.Web.Users;
|
||||||
using Phantom.Common.Messages.Web.ToController;
|
using Phantom.Common.Messages.Web.ToController;
|
||||||
|
using Phantom.Web.Services.Authentication;
|
||||||
using Phantom.Web.Services.Rpc;
|
using Phantom.Web.Services.Rpc;
|
||||||
|
|
||||||
namespace Phantom.Web.Services.Users;
|
namespace Phantom.Web.Services.Users;
|
||||||
@ -16,11 +18,21 @@ public sealed class UserManager {
|
|||||||
return controllerConnection.Send<GetUsersMessage, ImmutableArray<UserInfo>>(new GetUsersMessage(), cancellationToken);
|
return controllerConnection.Send<GetUsersMessage, ImmutableArray<UserInfo>>(new GetUsersMessage(), cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<CreateUserResult> Create(Guid loggedInUserGuid, string username, string password, CancellationToken cancellationToken) {
|
public async Task<Result<CreateUserResult, UserActionFailure>> Create(AuthenticatedUser? authenticatedUser, string username, string password, CancellationToken cancellationToken) {
|
||||||
return controllerConnection.Send<CreateUserMessage, CreateUserResult>(new CreateUserMessage(loggedInUserGuid, username, password), cancellationToken);
|
if (authenticatedUser != null && authenticatedUser.Info.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) {
|
public async Task<Result<DeleteUserResult, UserActionFailure>> DeleteByGuid(AuthenticatedUser? authenticatedUser, Guid userGuid, CancellationToken cancellationToken) {
|
||||||
return controllerConnection.Send<DeleteUserMessage, DeleteUserResult>(new DeleteUserMessage(loggedInUserGuid, userGuid), cancellationToken);
|
if (authenticatedUser != null && authenticatedUser.Info.CheckPermission(Permission.EditUsers)) {
|
||||||
|
return await controllerConnection.Send<DeleteUserMessage, Result<DeleteUserResult, UserActionFailure>>(new DeleteUserMessage(authenticatedUser.Token, userGuid), cancellationToken);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return UserActionFailure.NotAuthorized;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
|
using Phantom.Common.Data;
|
||||||
using Phantom.Common.Data.Web.Users;
|
using Phantom.Common.Data.Web.Users;
|
||||||
using Phantom.Common.Messages.Web.ToController;
|
using Phantom.Common.Messages.Web.ToController;
|
||||||
|
using Phantom.Web.Services.Authentication;
|
||||||
using Phantom.Web.Services.Rpc;
|
using Phantom.Web.Services.Rpc;
|
||||||
|
|
||||||
namespace Phantom.Web.Services.Users;
|
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);
|
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) {
|
public async Task<Result<ChangeUserRolesResult, UserActionFailure>> ChangeUserRoles(AuthenticatedUser? authenticatedUser, Guid subjectUserGuid, ImmutableHashSet<Guid> addToRoleGuids, ImmutableHashSet<Guid> removeFromRoleGuids, CancellationToken cancellationToken) {
|
||||||
return controllerConnection.Send<ChangeUserRolesMessage, ChangeUserRolesResult>(new ChangeUserRolesMessage(loggedInUserGuid, subjectUserGuid, addToRoleGuids, removeFromRoleGuids), cancellationToken);
|
if (authenticatedUser != null && authenticatedUser.Info.CheckPermission(Permission.EditUsers)) {
|
||||||
|
return await controllerConnection.Send<ChangeUserRolesMessage, Result<ChangeUserRolesResult, UserActionFailure>>(new ChangeUserRolesMessage(authenticatedUser.Token, subjectUserGuid, addToRoleGuids, removeFromRoleGuids), cancellationToken);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return UserActionFailure.NotAuthorized;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
<p role="alert">You do not have permission to visit this page.</p>
|
<p role="alert">You do not have permission to visit this page.</p>
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Navigation.NavigateTo("login" + QueryString.Create("return", Navigation.CreateReturnUrl()), forceLoad: true);
|
_ = Navigation.NavigateTo("login" + QueryString.Create("return", Navigation.CreateReturnUrl()), forceLoad: true);
|
||||||
}
|
}
|
||||||
</NotAuthorized>
|
</NotAuthorized>
|
||||||
</AuthorizeRouteView>
|
</AuthorizeRouteView>
|
||||||
|
@ -17,7 +17,6 @@
|
|||||||
|
|
||||||
<div class="navbar-menu @NavMenuCssClass" @onclick="ToggleNavMenu">
|
<div class="navbar-menu @NavMenuCssClass" @onclick="ToggleNavMenu">
|
||||||
<nav>
|
<nav>
|
||||||
<NavMenuItem Label="Home" Icon="home" Match="NavLinkMatch.All" />
|
|
||||||
<AuthorizeView>
|
<AuthorizeView>
|
||||||
<NotAuthorized>
|
<NotAuthorized>
|
||||||
<NavMenuItem Label="Login" Icon="account-login" Href="login" />
|
<NavMenuItem Label="Login" Icon="account-login" Href="login" />
|
||||||
@ -25,6 +24,8 @@
|
|||||||
<Authorized>
|
<Authorized>
|
||||||
@{ var permissions = context.GetPermissions(); }
|
@{ var permissions = context.GetPermissions(); }
|
||||||
|
|
||||||
|
<NavMenuItem Label="Home" Icon="home" Match="NavLinkMatch.All" />
|
||||||
|
|
||||||
@if (permissions.Check(Permission.ViewInstances)) {
|
@if (permissions.Check(Permission.ViewInstances)) {
|
||||||
<NavMenuItem Label="Instances" Icon="folder" Href="instances" />
|
<NavMenuItem Label="Instances" Icon="folder" Href="instances" />
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
@page "/agents"
|
@page "/agents"
|
||||||
|
@using System.Collections.Immutable
|
||||||
@using Phantom.Common.Data.Web.Agent
|
@using Phantom.Common.Data.Web.Agent
|
||||||
@using Phantom.Utils.Collections
|
@using Phantom.Utils.Collections
|
||||||
@using Phantom.Web.Services.Agents
|
@using Phantom.Web.Services.Agents
|
||||||
@ -46,33 +47,45 @@
|
|||||||
<Cell class="fw-semibold text-center text-success">Online</Cell>
|
<Cell class="fw-semibold text-center text-success">Online</Cell>
|
||||||
<Cell class="text-end">-</Cell>
|
<Cell class="text-end">-</Cell>
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case AgentIsOffline:
|
case AgentIsOffline:
|
||||||
<Cell class="fw-semibold text-center">Offline</Cell>
|
<Cell class="fw-semibold text-center">Offline</Cell>
|
||||||
<Cell class="text-end">N/A</Cell>
|
<Cell class="text-end">N/A</Cell>
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case AgentIsDisconnected status:
|
case AgentIsDisconnected status:
|
||||||
<Cell class="fw-semibold text-center">Offline</Cell>
|
<Cell class="fw-semibold text-center">Offline</Cell>
|
||||||
<Cell class="text-end">
|
<Cell class="text-end">
|
||||||
<TimeWithOffset Time="status.LastPingTime" />
|
<TimeWithOffset Time="status.LastPingTime" />
|
||||||
</Cell>
|
</Cell>
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
<Cell class="fw-semibold text-center">N/A</Cell>
|
<Cell class="fw-semibold text-center">N/A</Cell>
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
</ItemRow>
|
</ItemRow>
|
||||||
<NoItemsRow>
|
<NoItemsRow>
|
||||||
No agents registered.
|
No agents found.
|
||||||
</NoItemsRow>
|
</NoItemsRow>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
|
||||||
private readonly TableData<Agent, Guid> agentTable = new();
|
private TableData<Agent, Guid>? agentTable;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync() {
|
||||||
|
var authenticatedUser = await GetAuthenticatedUser();
|
||||||
|
if (authenticatedUser == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnInitialized() {
|
|
||||||
AgentManager.AgentsChanged.Subscribe(this, agents => {
|
AgentManager.AgentsChanged.Subscribe(this, agents => {
|
||||||
var sortedAgents = agents.Sort(static (a1, a2) => a1.Configuration.AgentName.CompareTo(a2.Configuration.AgentName));
|
var sortedAgents = agents.Where(agent => authenticatedUser.Info.HasAccessToAgent(agent.AgentGuid))
|
||||||
|
.OrderBy(static agent => agent.Configuration.AgentName)
|
||||||
|
.ToImmutableArray();
|
||||||
|
|
||||||
|
agentTable ??= new TableData<Agent, Guid>();
|
||||||
agentTable.UpdateFrom(sortedAgents, static agent => agent.AgentGuid, static agent => agent, static (agent, _) => agent);
|
agentTable.UpdateFrom(sortedAgents, static agent => agent.AgentGuid, static agent => agent, static (agent, _) => agent);
|
||||||
InvokeAsync(StateHasChanged);
|
InvokeAsync(StateHasChanged);
|
||||||
});
|
});
|
||||||
|
@ -5,13 +5,18 @@
|
|||||||
@using Phantom.Common.Data.Web.Users
|
@using Phantom.Common.Data.Web.Users
|
||||||
@using Phantom.Web.Services.Users
|
@using Phantom.Web.Services.Users
|
||||||
@using Phantom.Web.Services.Instances
|
@using Phantom.Web.Services.Instances
|
||||||
@inherits Phantom.Web.Components.PhantomComponent
|
@inherits PhantomComponent
|
||||||
@inject AuditLogManager AuditLogManager
|
@inject AuditLogManager AuditLogManager
|
||||||
@inject InstanceManager InstanceManager
|
@inject InstanceManager InstanceManager
|
||||||
@inject UserManager UserManager
|
@inject UserManager UserManager
|
||||||
|
|
||||||
<h1>Audit Log</h1>
|
<h1>Audit Log</h1>
|
||||||
|
|
||||||
|
@if (loadError is {} error) {
|
||||||
|
<p role="alert">@error</p>
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
<Table TItem="AuditLogItem" Items="logItems">
|
<Table TItem="AuditLogItem" Items="logItems">
|
||||||
<HeaderRow>
|
<HeaderRow>
|
||||||
<Column Class="text-end" MinWidth="200px">Time</Column>
|
<Column Class="text-end" MinWidth="200px">Time</Column>
|
||||||
@ -46,21 +51,25 @@
|
|||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
|
||||||
private CancellationTokenSource? initializationCancellationTokenSource;
|
|
||||||
private ImmutableArray<AuditLogItem>? logItems;
|
private ImmutableArray<AuditLogItem>? logItems;
|
||||||
|
private string? loadError;
|
||||||
|
|
||||||
private ImmutableDictionary<Guid, string>? userNamesByGuid;
|
private ImmutableDictionary<Guid, string>? userNamesByGuid;
|
||||||
private ImmutableDictionary<Guid, string> instanceNamesByGuid = ImmutableDictionary<Guid, string>.Empty;
|
private ImmutableDictionary<Guid, string> instanceNamesByGuid = ImmutableDictionary<Guid, string>.Empty;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync() {
|
protected override async Task OnInitializedAsync() {
|
||||||
initializationCancellationTokenSource = new CancellationTokenSource();
|
var result = await AuditLogManager.GetMostRecentItems(await GetAuthenticatedUser(), 50, CancellationToken);
|
||||||
var cancellationToken = initializationCancellationTokenSource.Token;
|
if (result) {
|
||||||
|
logItems = result.Value;
|
||||||
try {
|
userNamesByGuid = (await UserManager.GetAll(CancellationToken)).ToImmutableDictionary(static user => user.Guid, static user => user.Name);
|
||||||
logItems = await AuditLogManager.GetMostRecentItems(50, cancellationToken);
|
|
||||||
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);
|
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) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,11 @@
|
|||||||
|
|
||||||
<h1>Event Log</h1>
|
<h1>Event Log</h1>
|
||||||
|
|
||||||
|
@if (loadError is {} error) {
|
||||||
|
<p role="alert">@error</p>
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
<Table TItem="EventLogItem" Items="logItems">
|
<Table TItem="EventLogItem" Items="logItems">
|
||||||
<HeaderRow>
|
<HeaderRow>
|
||||||
<Column Class="text-end" MinWidth="200px">Time</Column>
|
<Column Class="text-end" MinWidth="200px">Time</Column>
|
||||||
@ -50,21 +55,25 @@
|
|||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
|
||||||
private CancellationTokenSource? initializationCancellationTokenSource;
|
|
||||||
private ImmutableArray<EventLogItem>? logItems;
|
private ImmutableArray<EventLogItem>? logItems;
|
||||||
|
private string? loadError;
|
||||||
|
|
||||||
private ImmutableDictionary<Guid, string> agentNamesByGuid = ImmutableDictionary<Guid, string>.Empty;
|
private ImmutableDictionary<Guid, string> agentNamesByGuid = ImmutableDictionary<Guid, string>.Empty;
|
||||||
private ImmutableDictionary<Guid, string> instanceNamesByGuid = ImmutableDictionary<Guid, string>.Empty;
|
private ImmutableDictionary<Guid, string> instanceNamesByGuid = ImmutableDictionary<Guid, string>.Empty;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync() {
|
protected override async Task OnInitializedAsync() {
|
||||||
initializationCancellationTokenSource = new CancellationTokenSource();
|
var result = await EventLogManager.GetMostRecentItems(await GetAuthenticatedUser(), 50, CancellationToken);
|
||||||
var cancellationToken = initializationCancellationTokenSource.Token;
|
if (result) {
|
||||||
|
logItems = result.Value;
|
||||||
try {
|
|
||||||
logItems = await EventLogManager.GetMostRecentItems(50, cancellationToken);
|
|
||||||
agentNamesByGuid = AgentManager.GetAll().ToImmutableDictionary(static kvp => kvp.AgentGuid, static kvp => kvp.Configuration.AgentName);
|
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);
|
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) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,18 @@
|
|||||||
@page "/"
|
@page "/"
|
||||||
@attribute [AllowAnonymous]
|
@inherits Phantom.Web.Components.PhantomComponent
|
||||||
|
|
||||||
<h1>Hello, world!</h1>
|
<h1>Home</h1>
|
||||||
|
|
||||||
Welcome to your new app.
|
@if (username != null) {
|
||||||
|
<p>Welcome back, @username!</p>
|
||||||
|
}
|
||||||
|
|
||||||
<AuthorizeView>
|
@code {
|
||||||
<Authorized>
|
|
||||||
You are logged in as @context.User.Identity!.Name.
|
private string? username = null;
|
||||||
</Authorized>
|
|
||||||
</AuthorizeView>
|
protected override async Task OnInitializedAsync() {
|
||||||
|
username = (await GetAuthenticatedUser())?.Info.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -1,27 +1,35 @@
|
|||||||
@page "/instances/{InstanceGuid:guid}"
|
@page "/instances/{InstanceGuid:guid}"
|
||||||
@attribute [Authorize(Permission.ViewInstancesPolicy)]
|
@attribute [Authorize(Permission.ViewInstancesPolicy)]
|
||||||
@using Phantom.Common.Data.Instance
|
|
||||||
@using Phantom.Common.Data.Replies
|
@using Phantom.Common.Data.Replies
|
||||||
@using Phantom.Common.Data.Web.Instance
|
@using Phantom.Common.Data.Web.Instance
|
||||||
@using Phantom.Common.Data.Web.Users
|
@using Phantom.Common.Data.Web.Users
|
||||||
|
@using Phantom.Utils.Result
|
||||||
|
@using Phantom.Common.Data.Instance
|
||||||
@using Phantom.Web.Services.Instances
|
@using Phantom.Web.Services.Instances
|
||||||
@using Phantom.Web.Services.Authorization
|
@using Phantom.Web.Services.Authorization
|
||||||
@inherits Phantom.Web.Components.PhantomComponent
|
@inherits PhantomComponent
|
||||||
@inject InstanceManager InstanceManager
|
@inject InstanceManager InstanceManager
|
||||||
|
|
||||||
|
@if (isLoading) {
|
||||||
|
<h1>Instance</h1>
|
||||||
|
<p>Loading...</p>
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
@if (Instance == null) {
|
@if (Instance == null) {
|
||||||
<h1>Instance Not Found</h1>
|
<h1>Instance Not Found</h1>
|
||||||
<p>Return to <a href="instances">all instances</a>.</p>
|
<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">
|
<div class="d-flex flex-row align-items-center gap-3 mb-3">
|
||||||
<h1 class="mb-0">Instance: @Instance.Configuration.InstanceName</h1>
|
<h1 class="mb-0">Instance: @Instance.Configuration.InstanceName</h1>
|
||||||
<span class="fs-4 text-muted">//</span>
|
<span class="fs-4 text-muted">//</span>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<InstanceStatusText Status="Instance.Status" />
|
<InstanceStatusText Status="Instance.Status" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex flex-row align-items-center gap-2">
|
<div class="d-flex flex-row align-items-center gap-2">
|
||||||
<PermissionView Permission="Permission.ControlInstances">
|
<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-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>
|
<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#stop-instance" disabled="@(!Instance.Status.CanStop())">Stop...</button>
|
||||||
@ -30,35 +38,39 @@ else {
|
|||||||
<PermissionView Permission="Permission.CreateInstances">
|
<PermissionView Permission="Permission.CreateInstances">
|
||||||
<a href="instances/@InstanceGuid/edit" class="btn btn-warning ms-auto">Edit Configuration</a>
|
<a href="instances/@InstanceGuid/edit" class="btn btn-warning ms-auto">Edit Configuration</a>
|
||||||
</PermissionView>
|
</PermissionView>
|
||||||
</div>
|
</div>
|
||||||
@if (lastError != null) {
|
@if (lastError != null) {
|
||||||
<p class="text-danger mt-2">@lastError</p>
|
<p class="text-danger mt-2" role="alert">@lastError</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
<PermissionView Permission="Permission.ViewInstanceLogs">
|
<PermissionView Permission="Permission.ViewInstanceLogs">
|
||||||
<InstanceLog InstanceGuid="InstanceGuid" />
|
<InstanceLog InstanceGuid="InstanceGuid" />
|
||||||
</PermissionView>
|
</PermissionView>
|
||||||
|
|
||||||
<PermissionView Permission="Permission.ControlInstances">
|
<PermissionView Permission="Permission.ControlInstances">
|
||||||
<div class="mb-3">
|
<div class="my-3">
|
||||||
<InstanceCommandInput AgentGuid="Instance.Configuration.AgentGuid" InstanceGuid="InstanceGuid" Disabled="@(!Instance.Status.CanSendCommand())" />
|
<InstanceCommandInput AgentGuid="Instance.Configuration.AgentGuid" InstanceGuid="InstanceGuid" Disabled="@(!Instance.Status.CanSendCommand())" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<InstanceStopDialog AgentGuid="Instance.Configuration.AgentGuid" InstanceGuid="InstanceGuid" ModalId="stop-instance" Disabled="@(!Instance.Status.CanStop())" />
|
<InstanceStopDialog AgentGuid="Instance.Configuration.AgentGuid" InstanceGuid="InstanceGuid" ModalId="stop-instance" Disabled="@(!Instance.Status.CanStop())" />
|
||||||
</PermissionView>
|
</PermissionView>
|
||||||
}
|
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public Guid InstanceGuid { get; init; }
|
public Guid InstanceGuid { get; init; }
|
||||||
|
|
||||||
|
private Instance? Instance { get; set; }
|
||||||
|
private bool isLoading = true;
|
||||||
|
|
||||||
private string? lastError = null;
|
private string? lastError = null;
|
||||||
private bool isLaunchingInstance = false;
|
private bool isLaunchingInstance = false;
|
||||||
|
|
||||||
private Instance? Instance { get; set; }
|
protected override async Task OnInitializedAsync() {
|
||||||
|
Instance = InstanceManager.GetByGuid(await GetAuthenticatedUser(), InstanceGuid);
|
||||||
|
isLoading = false;
|
||||||
|
|
||||||
protected override void OnInitialized() {
|
if (Instance != null) {
|
||||||
InstanceManager.InstancesChanged.Subscribe(this, instances => {
|
InstanceManager.InstancesChanged.Subscribe(this, instances => {
|
||||||
var newInstance = instances.TryGetValue(InstanceGuid, out var instance) ? instance : null;
|
var newInstance = instances.TryGetValue(InstanceGuid, out var instance) ? instance : null;
|
||||||
if (newInstance != Instance) {
|
if (newInstance != Instance) {
|
||||||
@ -67,26 +79,39 @@ else {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task LaunchInstance() {
|
private async Task LaunchInstance() {
|
||||||
isLaunchingInstance = true;
|
isLaunchingInstance = true;
|
||||||
lastError = null;
|
lastError = null;
|
||||||
|
|
||||||
try {
|
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) {
|
if (Instance == null) {
|
||||||
lastError = "Instance not found.";
|
lastError = "Instance not found.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await InstanceManager.LaunchInstance(loggedInUserGuid.Value, Instance.Configuration.AgentGuid, InstanceGuid, CancellationToken);
|
var result = await InstanceManager.LaunchInstance(await GetAuthenticatedUser(), Instance.Configuration.AgentGuid, InstanceGuid, CancellationToken);
|
||||||
if (!result.Is(LaunchInstanceResult.LaunchInitiated)) {
|
|
||||||
lastError = result.Map(Messages.ToSentence, InstanceActionFailureExtensions.ToSentence);
|
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 {
|
} finally {
|
||||||
isLaunchingInstance = false;
|
isLaunchingInstance = false;
|
||||||
|
@ -6,14 +6,20 @@
|
|||||||
@inherits PhantomComponent
|
@inherits PhantomComponent
|
||||||
@inject InstanceManager InstanceManager
|
@inject InstanceManager InstanceManager
|
||||||
|
|
||||||
|
@if (isLoading) {
|
||||||
|
<h1>Edit Instance</h1>
|
||||||
|
<p>Loading...</p>
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
@if (Instance == null) {
|
@if (Instance == null) {
|
||||||
<h1>Instance Not Found</h1>
|
<h1>Instance Not Found</h1>
|
||||||
<p>Return to <a href="instances">all instances</a>.</p>
|
<p>Return to <a href="instances">all instances</a>.</p>
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
<h1>Edit Instance: @Instance.Configuration.InstanceName</h1>
|
<h1>Edit Instance: @Instance.Configuration.InstanceName</h1>
|
||||||
<InstanceAddOrEditForm EditedInstance="Instance" />
|
<InstanceAddOrEditForm EditedInstance="Instance" />
|
||||||
}
|
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
|
||||||
@ -21,9 +27,11 @@ else {
|
|||||||
public Guid InstanceGuid { get; init; }
|
public Guid InstanceGuid { get; init; }
|
||||||
|
|
||||||
private Instance? Instance { get; set; }
|
private Instance? Instance { get; set; }
|
||||||
|
private bool isLoading = true;
|
||||||
|
|
||||||
protected override void OnInitialized() {
|
protected override async Task OnInitializedAsync() {
|
||||||
Instance = InstanceManager.GetByGuid(InstanceGuid);
|
Instance = InstanceManager.GetByGuid(await GetAuthenticatedUser(), InstanceGuid);
|
||||||
|
isLoading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -69,9 +69,18 @@
|
|||||||
this.agentNamesByGuid = agents.ToImmutableDictionary(static agent => agent.AgentGuid, static agent => agent.Configuration.AgentName);
|
this.agentNamesByGuid = agents.ToImmutableDictionary(static agent => agent.AgentGuid, static agent => agent.Configuration.AgentName);
|
||||||
InvokeAsync(StateHasChanged);
|
InvokeAsync(StateHasChanged);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync() {
|
||||||
|
var authenticatedUser = await GetAuthenticatedUser();
|
||||||
|
if (authenticatedUser == null) {
|
||||||
|
instances = ImmutableArray<Instance>.Empty;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
InstanceManager.InstancesChanged.Subscribe(this, instances => {
|
InstanceManager.InstancesChanged.Subscribe(this, instances => {
|
||||||
this.instances = instances.Values
|
this.instances = instances.Values
|
||||||
|
.Where(instance => authenticatedUser.Info.HasAccessToAgent(instance.Configuration.AgentGuid))
|
||||||
.OrderBy(instance => agentNamesByGuid.TryGetValue(instance.Configuration.AgentGuid, out var agentName) ? agentName : string.Empty)
|
.OrderBy(instance => agentNamesByGuid.TryGetValue(instance.Configuration.AgentGuid, out var agentName) ? agentName : string.Empty)
|
||||||
.ThenBy(static instance => instance.Configuration.InstanceName)
|
.ThenBy(static instance => instance.Configuration.InstanceName)
|
||||||
.ToImmutableArray();
|
.ToImmutableArray();
|
||||||
|
@ -60,7 +60,7 @@
|
|||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
|
||||||
private Guid? me = Guid.Empty;
|
private Guid? me = null;
|
||||||
private ImmutableArray<UserInfo>? allUsers;
|
private ImmutableArray<UserInfo>? allUsers;
|
||||||
private ImmutableDictionary<Guid, RoleInfo> allRolesByGuid = ImmutableDictionary<Guid, RoleInfo>.Empty;
|
private ImmutableDictionary<Guid, RoleInfo> allRolesByGuid = ImmutableDictionary<Guid, RoleInfo>.Empty;
|
||||||
private readonly Dictionary<Guid, string> userGuidToRoleDescription = new ();
|
private readonly Dictionary<Guid, string> userGuidToRoleDescription = new ();
|
||||||
@ -71,7 +71,7 @@
|
|||||||
private UserDeleteDialog userDeleteDialog = null!;
|
private UserDeleteDialog userDeleteDialog = null!;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync() {
|
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));
|
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);
|
allRolesByGuid = (await RoleManager.GetAll(CancellationToken)).ToImmutableDictionary(static role => role.Guid, static role => role);
|
||||||
|
@ -2,13 +2,14 @@
|
|||||||
@using System.Collections.Immutable
|
@using System.Collections.Immutable
|
||||||
@using System.ComponentModel.DataAnnotations
|
@using System.ComponentModel.DataAnnotations
|
||||||
@using System.Diagnostics.CodeAnalysis
|
@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.Instance
|
||||||
@using Phantom.Common.Data.Web.Minecraft
|
@using Phantom.Common.Data.Web.Minecraft
|
||||||
@using Phantom.Common.Data.Web.Users
|
@using Phantom.Common.Data.Web.Users
|
||||||
@using Phantom.Common.Messages.Web.ToController
|
@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.Java
|
||||||
@using Phantom.Common.Data
|
@using Phantom.Common.Data
|
||||||
@using Phantom.Common.Data.Instance
|
@using Phantom.Common.Data.Instance
|
||||||
@ -29,7 +30,8 @@
|
|||||||
@{
|
@{
|
||||||
static RenderFragment GetAgentOption(Agent agent) {
|
static RenderFragment GetAgentOption(Agent agent) {
|
||||||
var configuration = agent.Configuration;
|
var configuration = agent.Configuration;
|
||||||
return @<option value="@agent.AgentGuid">
|
return
|
||||||
|
@<option value="@agent.AgentGuid">
|
||||||
@configuration.AgentName
|
@configuration.AgentName
|
||||||
•
|
•
|
||||||
@(agent.Stats?.RunningInstanceCount.ToString() ?? "?")/@(configuration.MaxInstances) @(configuration.MaxInstances == 1 ? "Instance" : "Instances")
|
@(agent.Stats?.RunningInstanceCount.ToString() ?? "?")/@(configuration.MaxInstances) @(configuration.MaxInstances == 1 ? "Instance" : "Instances")
|
||||||
@ -276,10 +278,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync() {
|
protected override async Task OnInitializedAsync() {
|
||||||
|
var authenticatedUser = await GetAuthenticatedUser();
|
||||||
var agentJavaRuntimesTask = ControllerConnection.Send<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(new GetAgentJavaRuntimesMessage(), TimeSpan.FromSeconds(30));
|
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));
|
var minecraftVersionsTask = ControllerConnection.Send<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(new GetMinecraftVersionsMessage(), TimeSpan.FromSeconds(30));
|
||||||
|
|
||||||
allAgentsByGuid = AgentManager.ToDictionaryByGuid();
|
allAgentsByGuid = AgentManager.ToDictionaryByGuid(authenticatedUser);
|
||||||
allAgentJavaRuntimes = await agentJavaRuntimesTask;
|
allAgentJavaRuntimes = await agentJavaRuntimesTask;
|
||||||
allMinecraftVersions = await minecraftVersionsTask;
|
allMinecraftVersions = await minecraftVersionsTask;
|
||||||
|
|
||||||
@ -324,12 +327,6 @@
|
|||||||
|
|
||||||
await form.SubmitModel.StartSubmitting();
|
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 instanceGuid = EditedInstance?.InstanceGuid ?? Guid.NewGuid();
|
||||||
var instanceConfiguration = new InstanceConfiguration(
|
var instanceConfiguration = new InstanceConfiguration(
|
||||||
EditedInstance?.Configuration.AgentGuid ?? selectedAgent.AgentGuid,
|
EditedInstance?.Configuration.AgentGuid ?? selectedAgent.AgentGuid,
|
||||||
@ -343,12 +340,28 @@
|
|||||||
JvmArgumentsHelper.Split(form.JvmArguments)
|
JvmArgumentsHelper.Split(form.JvmArguments)
|
||||||
);
|
);
|
||||||
|
|
||||||
var result = await InstanceManager.CreateOrUpdateInstance(loggedInUserGuid.Value, instanceGuid, instanceConfiguration, CancellationToken);
|
var result = await InstanceManager.CreateOrUpdateInstance(await GetAuthenticatedUser(), instanceGuid, instanceConfiguration, CancellationToken);
|
||||||
if (result.Is(CreateOrUpdateInstanceResult.Success)) {
|
|
||||||
|
switch (result.Variant()) {
|
||||||
|
case Ok<CreateOrUpdateInstanceResult>(CreateOrUpdateInstanceResult.Success):
|
||||||
await Navigation.NavigateTo("instances/" + instanceGuid);
|
await Navigation.NavigateTo("instances/" + instanceGuid);
|
||||||
}
|
break;
|
||||||
else {
|
|
||||||
form.SubmitModel.StopSubmitting(result.Map(CreateOrUpdateInstanceResultExtensions.ToSentence, InstanceActionFailureExtensions.ToSentence));
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
@using Phantom.Web.Services.Instances
|
@using Phantom.Common.Data.Replies
|
||||||
@using Phantom.Common.Data.Web.Users
|
@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
|
@inherits Phantom.Web.Components.PhantomComponent
|
||||||
@inject InstanceManager InstanceManager
|
@inject InstanceManager InstanceManager
|
||||||
|
|
||||||
<Form Model="form" OnSubmit="ExecuteCommand">
|
<Form Model="form" OnSubmit="ExecuteCommand">
|
||||||
<label for="command-input" class="form-label">Instance Name</label>
|
<label for="command-input" class="form-label">Execute Command</label>
|
||||||
<div class="input-group flex-nowrap">
|
<div class="input-group flex-nowrap">
|
||||||
<span class="input-group-text" style="padding-top: 0.3rem;">/</span>
|
<span class="input-group-text" style="padding-top: 0.3rem;">/</span>
|
||||||
<input id="command-input" class="form-control" type="text" placeholder="command" @bind="form.Command" @bind:event="oninput" disabled="@(Disabled || form.SubmitModel.IsSubmitting)" @ref="commandInputElement" />
|
<input id="command-input" class="form-control" type="text" placeholder="command" @bind="form.Command" @bind:event="oninput" disabled="@(Disabled || form.SubmitModel.IsSubmitting)" @ref="commandInputElement" />
|
||||||
@ -36,19 +37,29 @@
|
|||||||
private async Task ExecuteCommand(EditContext context) {
|
private async Task ExecuteCommand(EditContext context) {
|
||||||
await form.SubmitModel.StartSubmitting();
|
await form.SubmitModel.StartSubmitting();
|
||||||
|
|
||||||
var loggedInUserGuid = await GetUserGuid();
|
var result = await InstanceManager.SendCommandToInstance(await GetAuthenticatedUser(), AgentGuid, InstanceGuid, form.Command, CancellationToken);
|
||||||
if (loggedInUserGuid == null || !await CheckPermission(Permission.ControlInstances)) {
|
|
||||||
form.SubmitModel.StopSubmitting("You do not have permission to execute commands.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await InstanceManager.SendCommandToInstance(loggedInUserGuid.Value, AgentGuid, InstanceGuid, form.Command, CancellationToken);
|
switch (result.Variant()) {
|
||||||
if (result.Is(SendCommandToInstanceResult.Success)) {
|
case Ok<SendCommandToInstanceResult>(SendCommandToInstanceResult.Success):
|
||||||
form.Command = string.Empty;
|
form.Command = string.Empty;
|
||||||
form.SubmitModel.StopSubmitting();
|
form.SubmitModel.StopSubmitting();
|
||||||
}
|
break;
|
||||||
else {
|
|
||||||
form.SubmitModel.StopSubmitting(result.Map(Messages.ToSentence, InstanceActionFailureExtensions.ToSentence));
|
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();
|
StateHasChanged();
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
@using Phantom.Web.Services.Instances
|
@using Phantom.Common.Data.Replies
|
||||||
@using System.ComponentModel.DataAnnotations
|
|
||||||
@using Phantom.Common.Data.Web.Users
|
@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.Minecraft
|
||||||
@using Phantom.Common.Data.Replies
|
|
||||||
@inherits Phantom.Web.Components.PhantomComponent
|
@inherits Phantom.Web.Components.PhantomComponent
|
||||||
@inject IJSRuntime Js;
|
@inject IJSRuntime Js;
|
||||||
@inject InstanceManager InstanceManager;
|
@inject InstanceManager InstanceManager;
|
||||||
@ -53,19 +54,29 @@
|
|||||||
private async Task StopInstance(EditContext context) {
|
private async Task StopInstance(EditContext context) {
|
||||||
await form.SubmitModel.StartSubmitting();
|
await form.SubmitModel.StartSubmitting();
|
||||||
|
|
||||||
var loggedInUserGuid = await GetUserGuid();
|
var result = await InstanceManager.StopInstance(await GetAuthenticatedUser(), AgentGuid, InstanceGuid, new MinecraftStopStrategy(form.StopInSeconds), CancellationToken);
|
||||||
if (loggedInUserGuid == null || !await CheckPermission(Permission.ControlInstances)) {
|
|
||||||
form.SubmitModel.StopSubmitting("You do not have permission to stop instances.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await InstanceManager.StopInstance(loggedInUserGuid.Value, AgentGuid, InstanceGuid, new MinecraftStopStrategy(form.StopInSeconds), CancellationToken);
|
switch (result.Variant()) {
|
||||||
if (result.Is(StopInstanceResult.StopInitiated)) {
|
case Ok<StopInstanceResult>(StopInstanceResult.StopInitiated):
|
||||||
await Js.InvokeVoidAsync("closeModal", ModalId);
|
await Js.InvokeVoidAsync("closeModal", ModalId);
|
||||||
form.SubmitModel.StopSubmitting();
|
form.SubmitModel.StopSubmitting();
|
||||||
}
|
break;
|
||||||
else {
|
|
||||||
form.SubmitModel.StopSubmitting(result.Map(Messages.ToSentence, InstanceActionFailureExtensions.ToSentence));
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
@using Phantom.Common.Data.Web.Users
|
@using Phantom.Common.Data.Web.Users
|
||||||
@using Phantom.Common.Data.Web.Users.CreateUserResults
|
@using Phantom.Common.Data.Web.Users.CreateUserResults
|
||||||
|
@using Phantom.Utils.Result
|
||||||
@using Phantom.Web.Services.Users
|
@using Phantom.Web.Services.Users
|
||||||
@using System.ComponentModel.DataAnnotations
|
@using System.ComponentModel.DataAnnotations
|
||||||
@inherits Phantom.Web.Components.PhantomComponent
|
@inherits Phantom.Web.Components.PhantomComponent
|
||||||
@ -39,7 +40,7 @@
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback<UserInfo> UserAdded { get; set; }
|
public EventCallback<UserInfo> UserAdded { get; set; }
|
||||||
|
|
||||||
private readonly AddUserFormModel form = new();
|
private readonly AddUserFormModel form = new ();
|
||||||
|
|
||||||
private sealed class AddUserFormModel : FormModel {
|
private sealed class AddUserFormModel : FormModel {
|
||||||
[Required]
|
[Required]
|
||||||
@ -52,23 +53,23 @@
|
|||||||
private async Task AddUser(EditContext context) {
|
private async Task AddUser(EditContext context) {
|
||||||
await form.SubmitModel.StartSubmitting();
|
await form.SubmitModel.StartSubmitting();
|
||||||
|
|
||||||
var loggedInUserGuid = await GetUserGuid();
|
var result = await UserManager.Create(await GetAuthenticatedUser(), form.Username, form.Password, CancellationToken);
|
||||||
if (loggedInUserGuid == null || !await CheckPermission(Permission.EditUsers)) {
|
|
||||||
form.SubmitModel.StopSubmitting("You do not have permission to add users.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (await UserManager.Create(loggedInUserGuid.Value, form.Username, form.Password, CancellationToken)) {
|
switch (result.Variant()) {
|
||||||
case Success success:
|
case Ok<CreateUserResult>(Success success):
|
||||||
await UserAdded.InvokeAsync(success.User);
|
await UserAdded.InvokeAsync(success.User);
|
||||||
await Js.InvokeVoidAsync("closeModal", ModalId);
|
await Js.InvokeVoidAsync("closeModal", ModalId);
|
||||||
form.SubmitModel.StopSubmitting();
|
form.SubmitModel.StopSubmitting();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case CreationFailed fail:
|
case Ok<CreateUserResult>(CreationFailed fail):
|
||||||
form.SubmitModel.StopSubmitting(fail.Error.ToSentences("\n"));
|
form.SubmitModel.StopSubmitting(fail.Error.ToSentences("\n"));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case Err<UserActionFailure>(UserActionFailure.NotAuthorized):
|
||||||
|
form.SubmitModel.StopSubmitting("You do not have permission to add users.");
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
form.SubmitModel.StopSubmitting("Unknown error.");
|
form.SubmitModel.StopSubmitting("Unknown error.");
|
||||||
break;
|
break;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
@using Phantom.Common.Data.Web.Users
|
@using Phantom.Common.Data.Web.Users
|
||||||
|
@using Phantom.Web.Services.Authentication
|
||||||
@using Phantom.Web.Services.Users
|
@using Phantom.Web.Services.Users
|
||||||
@inherits UserEditDialogBase
|
@inherits UserEditDialogBase
|
||||||
@inject UserManager UserManager
|
@inject UserManager UserManager
|
||||||
@ -17,8 +18,13 @@
|
|||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
|
||||||
protected override async Task DoEdit(Guid loggedInUserGuid, UserInfo user) {
|
protected override async Task<UserActionFailure?> DoEdit(AuthenticatedUser? authenticatedUser, UserInfo editedUser) {
|
||||||
switch (await UserManager.DeleteByGuid(loggedInUserGuid, user.Guid, CancellationToken)) {
|
var result = await UserManager.DeleteByGuid(authenticatedUser, editedUser.Guid, CancellationToken);
|
||||||
|
if (!result) {
|
||||||
|
return result.Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (result.Value) {
|
||||||
case DeleteUserResult.Deleted:
|
case DeleteUserResult.Deleted:
|
||||||
case DeleteUserResult.NotFound:
|
case DeleteUserResult.NotFound:
|
||||||
await OnEditSuccess();
|
await OnEditSuccess();
|
||||||
@ -28,6 +34,8 @@
|
|||||||
OnEditFailure("Could not delete user.");
|
OnEditFailure("Could not delete user.");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ using Microsoft.JSInterop;
|
|||||||
using Phantom.Common.Data.Web.Users;
|
using Phantom.Common.Data.Web.Users;
|
||||||
using Phantom.Web.Components;
|
using Phantom.Web.Components;
|
||||||
using Phantom.Web.Components.Forms;
|
using Phantom.Web.Components.Forms;
|
||||||
|
using Phantom.Web.Services.Authentication;
|
||||||
|
|
||||||
namespace Phantom.Web.Shared;
|
namespace Phantom.Web.Shared;
|
||||||
|
|
||||||
@ -16,7 +17,7 @@ public abstract class UserEditDialogBase : PhantomComponent {
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback<UserInfo> UserModified { get; set; }
|
public EventCallback<UserInfo> UserModified { get; set; }
|
||||||
|
|
||||||
protected readonly FormButtonSubmit.SubmitModel SubmitModel = new();
|
protected readonly FormButtonSubmit.SubmitModel SubmitModel = new ();
|
||||||
|
|
||||||
private UserInfo? EditedUser { get; set; } = null;
|
private UserInfo? EditedUser { get; set; } = null;
|
||||||
protected string EditedUserName { get; private set; } = string.Empty;
|
protected string EditedUserName { get; private set; } = string.Empty;
|
||||||
@ -41,19 +42,26 @@ public abstract class UserEditDialogBase : PhantomComponent {
|
|||||||
protected async Task Submit() {
|
protected async Task Submit() {
|
||||||
await SubmitModel.StartSubmitting();
|
await SubmitModel.StartSubmitting();
|
||||||
|
|
||||||
var loggedInUserGuid = await GetUserGuid();
|
if (EditedUser == null) {
|
||||||
if (loggedInUserGuid == null || !await CheckPermission(Permission.EditUsers)) {
|
|
||||||
SubmitModel.StopSubmitting("You do not have permission to edit users.");
|
|
||||||
}
|
|
||||||
else if (EditedUser == null) {
|
|
||||||
SubmitModel.StopSubmitting("Invalid user.");
|
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() {
|
protected async Task OnEditSuccess() {
|
||||||
await UserModified.InvokeAsync(EditedUser);
|
await UserModified.InvokeAsync(EditedUser);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
@using System.Collections.Immutable
|
@using System.Collections.Immutable
|
||||||
@using Phantom.Common.Data.Web.Users
|
@using Phantom.Common.Data.Web.Users
|
||||||
|
@using Phantom.Web.Services.Authentication
|
||||||
@using Phantom.Web.Services.Users
|
@using Phantom.Web.Services.Users
|
||||||
@inherits UserEditDialogBase
|
@inherits UserEditDialogBase
|
||||||
@inject RoleManager RoleManager
|
@inject RoleManager RoleManager
|
||||||
@ -36,8 +37,8 @@
|
|||||||
this.items = allRoles.Select(role => new RoleItem(role, currentRoleGuids.Contains(role.Guid))).ToList();
|
this.items = allRoles.Select(role => new RoleItem(role, currentRoleGuids.Contains(role.Guid))).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task DoEdit(Guid loggedInUserGuid, UserInfo user) {
|
protected override async Task<UserActionFailure?> DoEdit(AuthenticatedUser? authenticatedUser, UserInfo editedUser) {
|
||||||
var currentRoleGuids = await UserRoleManager.GetUserRoles(user.Guid, CancellationToken);
|
var currentRoleGuids = await UserRoleManager.GetUserRoles(editedUser.Guid, CancellationToken);
|
||||||
var addToRoleGuids = ImmutableHashSet.CreateBuilder<Guid>();
|
var addToRoleGuids = ImmutableHashSet.CreateBuilder<Guid>();
|
||||||
var removeFromRoleGuids = 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) {
|
private async Task<UserActionFailure?> DoChangeUserRoles(AuthenticatedUser? authenticatedUser, UserInfo editedUser, ImmutableHashSet<Guid> addToRoleGuids, ImmutableHashSet<Guid> removeFromRoleGuids) {
|
||||||
var result = await UserRoleManager.ChangeUserRoles(loggedInUserGuid, user.Guid, addToRoleGuids, removeFromRoleGuids, CancellationToken);
|
var result = await UserRoleManager.ChangeUserRoles(authenticatedUser, editedUser.Guid, addToRoleGuids, removeFromRoleGuids, CancellationToken);
|
||||||
|
if (!result) {
|
||||||
|
return result.Error;
|
||||||
|
}
|
||||||
|
|
||||||
var failedToAdd = addToRoleGuids.Except(result.AddedToRoleGuids);
|
var failedToAdd = addToRoleGuids.Except(result.Value.AddedToRoleGuids);
|
||||||
var failedToRemove = removeFromRoleGuids.Except(result.RemovedFromRoleGuids);
|
var failedToRemove = removeFromRoleGuids.Except(result.Value.RemovedFromRoleGuids);
|
||||||
|
|
||||||
if (failedToAdd.IsEmpty && failedToRemove.IsEmpty) {
|
if (failedToAdd.IsEmpty && failedToRemove.IsEmpty) {
|
||||||
await OnEditSuccess();
|
await OnEditSuccess();
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var errors = new List<string>();
|
var errors = new List<string>();
|
||||||
@ -81,6 +85,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
OnEditFailure(string.Join("\n", errors));
|
OnEditFailure(string.Join("\n", errors));
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetRoleName(Guid roleGuid) {
|
private string GetRoleName(Guid roleGuid) {
|
||||||
|
Loading…
Reference in New Issue
Block a user