1
0
mirror of https://github.com/chylex/Minecraft-Phantom-Panel.git synced 2025-09-15 15:32:10 +02:00

1 Commits

Author SHA1 Message Date
599177409e WIP 2024-04-05 13:39:11 +02:00
289 changed files with 2232 additions and 3465 deletions

View File

@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "8.0.3",
"version": "7.0.0-rc.1.22426.7",
"commands": [
"dotnet-ef"
]

View File

@@ -1,4 +1,5 @@
using System.Text;
using System.Collections.ObjectModel;
using System.Text;
using Phantom.Agent.Minecraft.Instance;
using Phantom.Agent.Minecraft.Java;
using Phantom.Agent.Minecraft.Server;
@@ -11,6 +12,7 @@ public abstract class BaseLauncher : IServerLauncher {
private readonly InstanceProperties instanceProperties;
protected string MinecraftVersion => instanceProperties.ServerVersion;
protected string InstanceFolder => instanceProperties.InstanceFolder;
private protected BaseLauncher(InstanceProperties instanceProperties) {
this.instanceProperties = instanceProperties;
@@ -51,16 +53,14 @@ public abstract class BaseLauncher : IServerLauncher {
var processConfigurator = new ProcessConfigurator {
FileName = javaRuntimeExecutable.ExecutablePath,
WorkingDirectory = instanceProperties.InstanceFolder,
WorkingDirectory = InstanceFolder,
RedirectInput = true,
UseShellExecute = false
};
var processArguments = processConfigurator.ArgumentList;
PrepareJvmArguments(serverJar).Build(processArguments);
processArguments.Add("-jar");
processArguments.Add(serverJar.FilePath);
processArguments.Add("nogui");
PrepareJavaProcessArguments(processArguments, serverJar.FilePath);
var process = processConfigurator.CreateProcess();
var instanceProcess = new InstanceProcess(instanceProperties, process);
@@ -99,6 +99,12 @@ public abstract class BaseLauncher : IServerLauncher {
private protected virtual void CustomizeJvmArguments(JvmArgumentBuilder arguments) {}
protected virtual void PrepareJavaProcessArguments(Collection<string> processArguments, string serverJarFilePath) {
processArguments.Add("-jar");
processArguments.Add(serverJarFilePath);
processArguments.Add("nogui");
}
private protected virtual Task<ServerJarInfo> PrepareServerJar(ILogger logger, string serverJarPath, CancellationToken cancellationToken) {
return Task.FromResult(new ServerJarInfo(serverJarPath));
}

View File

@@ -0,0 +1,29 @@
using System.Collections.ObjectModel;
using Phantom.Agent.Minecraft.Instance;
using Phantom.Agent.Minecraft.Java;
using Serilog;
namespace Phantom.Agent.Minecraft.Launcher.Types;
public sealed class ForgeLauncher : BaseLauncher {
public ForgeLauncher(InstanceProperties instanceProperties) : base(instanceProperties) {}
private protected override void CustomizeJvmArguments(JvmArgumentBuilder arguments) {
arguments.AddProperty("terminal.ansi", "true"); // TODO
}
protected override void PrepareJavaProcessArguments(Collection<string> processArguments, string serverJarFilePath) {
if (OperatingSystem.IsWindows()) {
processArguments.Add("@libraries/net/minecraftforge/forge/1.20.1-47.2.0/win_args.txt");
}
else {
processArguments.Add("@libraries/net/minecraftforge/forge/1.20.1-47.2.0/unix_args.txt");
}
processArguments.Add("nogui");
}
private protected override Task<ServerJarInfo> PrepareServerJar(ILogger logger, string serverJarPath, CancellationToken cancellationToken) {
return Task.FromResult(new ServerJarInfo(Path.Combine(InstanceFolder, "run.sh")));
}
}

View File

@@ -18,5 +18,4 @@ static class MinecraftServerProperties {
public static readonly MinecraftServerProperty<ushort> ServerPort = new UnsignedShort("server-port");
public static readonly MinecraftServerProperty<ushort> RconPort = new UnsignedShort("rcon.port");
public static readonly MinecraftServerProperty<bool> EnableRcon = new Boolean("enable-rcon");
public static readonly MinecraftServerProperty<bool> SyncChunkWrites = new Boolean("sync-chunk-writes");
}

View File

@@ -5,13 +5,11 @@ namespace Phantom.Agent.Minecraft.Properties;
public sealed record ServerProperties(
ushort ServerPort,
ushort RconPort,
bool EnableRcon = true,
bool SyncChunkWrites = false
bool EnableRcon = true
) {
internal void SetTo(JavaPropertiesFileEditor properties) {
MinecraftServerProperties.ServerPort.Set(properties, ServerPort);
MinecraftServerProperties.RconPort.Set(properties, RconPort);
MinecraftServerProperties.EnableRcon.Set(properties, EnableRcon);
MinecraftServerProperties.SyncChunkWrites.Set(properties, SyncChunkWrites);
}
}

View File

@@ -3,12 +3,28 @@ using System.Buffers.Binary;
using System.Net;
using System.Net.Sockets;
using System.Text;
using Phantom.Common.Data.Instance;
using Phantom.Utils.Logging;
using Serilog;
namespace Phantom.Agent.Minecraft.Server;
public static class ServerStatusProtocol {
public static async Task<InstancePlayerCounts> GetPlayerCounts(ushort serverPort, CancellationToken cancellationToken) {
public sealed class ServerStatusProtocol {
private readonly ILogger logger;
public ServerStatusProtocol(string loggerName) {
this.logger = PhantomLogger.Create<ServerStatusProtocol>(loggerName);
}
public async Task<int?> GetOnlinePlayerCount(int serverPort, CancellationToken cancellationToken) {
try {
return await GetOnlinePlayerCountOrThrow(serverPort, cancellationToken);
} catch (Exception e) {
logger.Error(e, "Caught exception while checking if players are online.");
return null;
}
}
private async Task<int?> GetOnlinePlayerCountOrThrow(int serverPort, CancellationToken cancellationToken) {
using var tcpClient = new TcpClient();
await tcpClient.ConnectAsync(IPAddress.Loopback, serverPort, cancellationToken);
var tcpStream = tcpClient.GetStream();
@@ -17,22 +33,24 @@ public static class ServerStatusProtocol {
tcpStream.WriteByte(0xFE);
await tcpStream.FlushAsync(cancellationToken);
short messageLength = await ReadStreamHeader(tcpStream, cancellationToken);
return await ReadPlayerCounts(tcpStream, messageLength * 2, cancellationToken);
short? messageLength = await ReadStreamHeader(tcpStream, cancellationToken);
return messageLength == null ? null : await ReadOnlinePlayerCount(tcpStream, messageLength.Value * 2, cancellationToken);
}
private static async Task<short> ReadStreamHeader(NetworkStream tcpStream, CancellationToken cancellationToken) {
private async Task<short?> ReadStreamHeader(NetworkStream tcpStream, CancellationToken cancellationToken) {
var headerBuffer = ArrayPool<byte>.Shared.Rent(3);
try {
await tcpStream.ReadExactlyAsync(headerBuffer, 0, 3, cancellationToken);
if (headerBuffer[0] != 0xFF) {
throw new ProtocolException("Unexpected first byte in response from server: " + headerBuffer[0]);
logger.Error("Unexpected first byte in response from server: {FirstByte}.", headerBuffer[0]);
return null;
}
short messageLength = BinaryPrimitives.ReadInt16BigEndian(headerBuffer.AsSpan(1));
if (messageLength <= 0) {
throw new ProtocolException("Unexpected message length in response from server: " + messageLength);
logger.Error("Unexpected message length in response from server: {MessageLength}.", messageLength);
return null;
}
return messageLength;
@@ -41,54 +59,35 @@ public static class ServerStatusProtocol {
}
}
private static async Task<InstancePlayerCounts> ReadPlayerCounts(NetworkStream tcpStream, int messageLength, CancellationToken cancellationToken) {
private async Task<int?> ReadOnlinePlayerCount(NetworkStream tcpStream, int messageLength, CancellationToken cancellationToken) {
var messageBuffer = ArrayPool<byte>.Shared.Rent(messageLength);
try {
await tcpStream.ReadExactlyAsync(messageBuffer, 0, messageLength, cancellationToken);
return ReadPlayerCountsFromResponse(messageBuffer.AsSpan(0, messageLength));
// Valid response separator encoded in UTF-16BE is 0x00 0xA7 (§).
const byte SeparatorSecondByte = 0xA7;
static bool IsValidSeparator(ReadOnlySpan<byte> buffer, int index) {
return index > 0 && buffer[index - 1] == 0x00;
}
int separator2 = Array.LastIndexOf(messageBuffer, SeparatorSecondByte);
int separator1 = separator2 == -1 ? -1 : Array.LastIndexOf(messageBuffer, SeparatorSecondByte, separator2 - 1);
if (!IsValidSeparator(messageBuffer, separator1) || !IsValidSeparator(messageBuffer, separator2)) {
logger.Error("Could not find message separators in response from server.");
return null;
}
string onlinePlayerCountStr = Encoding.BigEndianUnicode.GetString(messageBuffer.AsSpan((separator1 + 1)..(separator2 - 1)));
if (!int.TryParse(onlinePlayerCountStr, out int onlinePlayerCount)) {
logger.Error("Could not parse online player count in response from server: {OnlinePlayerCount}.", onlinePlayerCountStr);
return null;
}
logger.Debug("Detected {OnlinePlayerCount} online player(s).", onlinePlayerCount);
return onlinePlayerCount;
} finally {
ArrayPool<byte>.Shared.Return(messageBuffer);
}
}
/// <summary>
/// Legacy query protocol uses the paragraph symbol (§) as separator encoded in UTF-16BE.
/// </summary>
private static readonly byte[] Separator = { 0x00, 0xA7 };
private static InstancePlayerCounts ReadPlayerCountsFromResponse(ReadOnlySpan<byte> messageBuffer) {
int lastSeparator = messageBuffer.LastIndexOf(Separator);
int middleSeparator = messageBuffer[..lastSeparator].LastIndexOf(Separator);
if (lastSeparator == -1 || middleSeparator == -1) {
throw new ProtocolException("Could not find message separators in response from server.");
}
var onlinePlayerCountBuffer = messageBuffer[(middleSeparator + Separator.Length)..lastSeparator];
var maximumPlayerCountBuffer = messageBuffer[(lastSeparator + Separator.Length)..];
// Player counts are integers, whose maximum string length is 10 characters.
Span<char> integerStringBuffer = stackalloc char[10];
return new InstancePlayerCounts(
DecodeAndParsePlayerCount(onlinePlayerCountBuffer, integerStringBuffer, "online"),
DecodeAndParsePlayerCount(maximumPlayerCountBuffer, integerStringBuffer, "maximum")
);
}
private static int DecodeAndParsePlayerCount(ReadOnlySpan<byte> inputBuffer, Span<char> tempCharBuffer, string countType) {
if (!Encoding.BigEndianUnicode.TryGetChars(inputBuffer, tempCharBuffer, out int charCount)) {
throw new ProtocolException("Could not decode " + countType + " player count in response from server.");
}
if (!int.TryParse(tempCharBuffer, out int playerCount)) {
throw new ProtocolException("Could not parse " + countType + " player count in response from server: " + tempCharBuffer[..charCount].ToString());
}
return playerCount;
}
public sealed class ProtocolException : Exception {
internal ProtocolException(string message) : base(message) {}
}
}

View File

@@ -1,8 +1,10 @@
using Phantom.Agent.Services.Instances;
using Phantom.Agent.Services.Instances.State;
using Phantom.Agent.Minecraft.Instance;
using Phantom.Agent.Minecraft.Server;
using Phantom.Agent.Services.Instances;
using Phantom.Common.Data.Backups;
using Phantom.Utils.Logging;
using Phantom.Utils.Tasks;
using Phantom.Utils.Threading;
namespace Phantom.Agent.Services.Backups;
@@ -14,16 +16,20 @@ sealed class BackupScheduler : CancellableBackgroundTask {
private readonly BackupManager backupManager;
private readonly InstanceContext context;
private readonly InstanceProcess process;
private readonly SemaphoreSlim backupSemaphore = new (1, 1);
private readonly int serverPort;
private readonly ServerStatusProtocol serverStatusProtocol;
private readonly ManualResetEventSlim serverOutputWhileWaitingForOnlinePlayers = new ();
private readonly InstancePlayerCountTracker playerCountTracker;
public event EventHandler<BackupCreationResult>? BackupCompleted;
public BackupScheduler(InstanceContext context, InstancePlayerCountTracker playerCountTracker) : base(PhantomLogger.Create<BackupScheduler>(context.ShortName)) {
public BackupScheduler(InstanceContext context, InstanceProcess process, int serverPort) : base(PhantomLogger.Create<BackupScheduler>(context.ShortName)) {
this.backupManager = context.Services.BackupManager;
this.context = context;
this.playerCountTracker = playerCountTracker;
this.process = process;
this.serverPort = serverPort;
this.serverStatusProtocol = new ServerStatusProtocol(context.ShortName);
Start();
}
@@ -63,18 +69,43 @@ sealed class BackupScheduler : CancellableBackgroundTask {
}
private async Task WaitForOnlinePlayers() {
var task = playerCountTracker.WaitForOnlinePlayers(CancellationToken);
if (!task.IsCompleted) {
Logger.Information("Waiting for someone to join before starting a new backup.");
bool needsToLogOfflinePlayersMessage = true;
process.AddOutputListener(ServerOutputListener, maxLinesToReadFromHistory: 0);
try {
while (!CancellationToken.IsCancellationRequested) {
serverOutputWhileWaitingForOnlinePlayers.Reset();
var onlinePlayerCount = await serverStatusProtocol.GetOnlinePlayerCount(serverPort, CancellationToken);
if (onlinePlayerCount == null) {
Logger.Warning("Could not detect whether any players are online, starting a new backup.");
break;
}
try {
await task;
if (onlinePlayerCount > 0) {
Logger.Information("Players are online, starting a new backup.");
} catch (OperationCanceledException) {
throw;
} catch (Exception) {
Logger.Warning("Could not detect whether any players are online, starting a new backup.");
break;
}
if (needsToLogOfflinePlayersMessage) {
needsToLogOfflinePlayersMessage = false;
Logger.Information("No players are online, waiting for someone to join before starting a new backup.");
}
await Task.Delay(TimeSpan.FromSeconds(10), CancellationToken);
Logger.Debug("Waiting for server output before checking for online players again...");
await serverOutputWhileWaitingForOnlinePlayers.WaitHandle.WaitOneAsync(CancellationToken);
}
} finally {
process.RemoveOutputListener(ServerOutputListener);
}
}
private void ServerOutputListener(object? sender, string line) {
if (!serverOutputWhileWaitingForOnlinePlayers.IsSet) {
serverOutputWhileWaitingForOnlinePlayers.Set();
Logger.Debug("Detected server output, signalling to check for online players again.");
}
}

View File

@@ -55,6 +55,7 @@ sealed partial class BackupServerCommandDispatcher : IDisposable {
}
public async Task SaveAllChunks() {
// TODO Try if not flushing and waiting a few seconds before flushing reduces lag.
await process.SendCommand(MinecraftCommand.SaveAll(flush: true), cancellationToken);
await savedTheGame.Task.WaitAsync(TimeSpan.FromMinutes(1), cancellationToken);
}

View File

@@ -102,6 +102,7 @@ sealed class InstanceManagerActor : ReceiveActor<InstanceManagerActor.ICommand>
IServerLauncher launcher = configuration.MinecraftServerKind switch {
MinecraftServerKind.Vanilla => new VanillaLauncher(properties),
MinecraftServerKind.Fabric => new FabricLauncher(properties),
MinecraftServerKind.Forge => new ForgeLauncher(properties),
_ => InvalidLauncher.Instance
};

View File

@@ -5,6 +5,7 @@ using Phantom.Common.Data.Instance;
using Phantom.Common.Data.Replies;
using Phantom.Common.Messages.Agent.ToController;
using Phantom.Utils.Logging;
using Phantom.Utils.Tasks;
using Serilog;
namespace Phantom.Agent.Services.Instances;

View File

@@ -1,140 +0,0 @@
using Phantom.Agent.Minecraft.Instance;
using Phantom.Agent.Minecraft.Server;
using Phantom.Agent.Rpc;
using Phantom.Common.Data.Instance;
using Phantom.Common.Messages.Agent.ToController;
using Phantom.Utils.Logging;
using Phantom.Utils.Tasks;
using Phantom.Utils.Threading;
namespace Phantom.Agent.Services.Instances.State;
sealed class InstancePlayerCountTracker : CancellableBackgroundTask {
private readonly ControllerConnection controllerConnection;
private readonly Guid instanceGuid;
private readonly ushort serverPort;
private readonly InstanceProcess process;
private readonly TaskCompletionSource firstDetection = AsyncTasks.CreateCompletionSource();
private readonly ManualResetEventSlim serverOutputEvent = new ();
private InstancePlayerCounts? playerCounts;
public InstancePlayerCounts? PlayerCounts {
get {
lock (this) {
return playerCounts;
}
}
private set {
EventHandler<int?>? onlinePlayerCountChanged;
lock (this) {
if (playerCounts == value) {
return;
}
playerCounts = value;
onlinePlayerCountChanged = OnlinePlayerCountChanged;
}
onlinePlayerCountChanged?.Invoke(this, value?.Online);
controllerConnection.Send(new ReportInstancePlayerCountsMessage(instanceGuid, value));
}
}
private event EventHandler<int?>? OnlinePlayerCountChanged;
private bool isDisposed = false;
public InstancePlayerCountTracker(InstanceContext context, InstanceProcess process, ushort serverPort) : base(PhantomLogger.Create<InstancePlayerCountTracker>(context.ShortName)) {
this.controllerConnection = context.Services.ControllerConnection;
this.instanceGuid = context.InstanceGuid;
this.process = process;
this.serverPort = serverPort;
Start();
}
protected override async Task RunTask() {
// Give the server time to start accepting connections.
await Task.Delay(TimeSpan.FromSeconds(10), CancellationToken);
serverOutputEvent.Set();
process.AddOutputListener(OnOutput, maxLinesToReadFromHistory: 0);
while (!CancellationToken.IsCancellationRequested) {
serverOutputEvent.Reset();
PlayerCounts = await TryGetPlayerCounts();
if (!firstDetection.Task.IsCompleted) {
firstDetection.SetResult();
}
await Task.Delay(TimeSpan.FromSeconds(10), CancellationToken);
await serverOutputEvent.WaitHandle.WaitOneAsync(CancellationToken);
await Task.Delay(TimeSpan.FromSeconds(1), CancellationToken);
}
}
private async Task<InstancePlayerCounts?> TryGetPlayerCounts() {
try {
var result = await ServerStatusProtocol.GetPlayerCounts(serverPort, CancellationToken);
Logger.Debug("Detected {OnlinePlayerCount} / {MaximumPlayerCount} online player(s).", result.Online, result.Maximum);
return result;
} catch (ServerStatusProtocol.ProtocolException e) {
Logger.Error(e.Message);
return null;
} catch (Exception e) {
Logger.Error(e, "Caught exception while checking online player count.");
return null;
}
}
public async Task WaitForOnlinePlayers(CancellationToken cancellationToken) {
await firstDetection.Task.WaitAsync(cancellationToken);
var onlinePlayersDetected = AsyncTasks.CreateCompletionSource();
lock (this) {
if (playerCounts is { Online: > 0 }) {
return;
}
else if (playerCounts == null) {
throw new InvalidOperationException();
}
OnlinePlayerCountChanged += OnOnlinePlayerCountChanged;
void OnOnlinePlayerCountChanged(object? sender, int? newPlayerCount) {
if (newPlayerCount == null) {
onlinePlayersDetected.TrySetException(new InvalidOperationException());
OnlinePlayerCountChanged -= OnOnlinePlayerCountChanged;
}
else if (newPlayerCount > 0) {
onlinePlayersDetected.TrySetResult();
OnlinePlayerCountChanged -= OnOnlinePlayerCountChanged;
}
}
}
await onlinePlayersDetected.Task;
}
private void OnOutput(object? sender, string? line) {
lock (this) {
if (!isDisposed) {
serverOutputEvent.Set();
}
}
}
protected override void Dispose() {
lock (this) {
isDisposed = true;
playerCounts = null;
}
process.RemoveOutputListener(OnOutput);
serverOutputEvent.Dispose();
}
}

View File

@@ -19,7 +19,6 @@ sealed class InstanceRunningState : IDisposable {
private readonly CancellationToken cancellationToken;
private readonly InstanceLogSender logSender;
private readonly InstancePlayerCountTracker playerCountTracker;
private readonly BackupScheduler backupScheduler;
private bool isDisposed;
@@ -33,9 +32,8 @@ sealed class InstanceRunningState : IDisposable {
this.cancellationToken = cancellationToken;
this.logSender = new InstanceLogSender(context.Services.ControllerConnection, context.InstanceGuid, context.ShortName);
this.playerCountTracker = new InstancePlayerCountTracker(context, process, configuration.ServerPort);
this.backupScheduler = new BackupScheduler(context, playerCountTracker);
this.backupScheduler = new BackupScheduler(context, process, configuration.ServerPort);
this.backupScheduler.BackupCompleted += OnScheduledBackupCompleted;
}
@@ -95,11 +93,6 @@ sealed class InstanceRunningState : IDisposable {
}
}
public void OnStopInitiated() {
backupScheduler.Stop();
playerCountTracker.Stop();
}
private bool TryDispose() {
lock (this) {
if (isDisposed) {
@@ -109,8 +102,8 @@ sealed class InstanceRunningState : IDisposable {
isDisposed = true;
}
OnStopInitiated();
logSender.Stop();
backupScheduler.Stop();
Process.Dispose();

View File

@@ -25,8 +25,6 @@ static class InstanceStopProcedure {
try {
// Too late to cancel the stop procedure now.
runningState.OnStopInitiated();
if (!process.HasEnded) {
context.Logger.Information("Session stopping now.");
await DoStop(context, process);
@@ -87,7 +85,7 @@ static class InstanceStopProcedure {
private static async Task WaitForSessionToEnd(InstanceContext context, InstanceProcess process) {
try {
await process.WaitForExit(TimeSpan.FromSeconds(55));
} catch (TimeoutException) {
} catch (OperationCanceledException) {
try {
context.Logger.Warning("Waiting timed out, killing session...");
process.Kill();

View File

@@ -8,10 +8,9 @@ public sealed partial record Instance(
[property: MemoryPackOrder(0)] Guid InstanceGuid,
[property: MemoryPackOrder(1)] InstanceConfiguration Configuration,
[property: MemoryPackOrder(2)] IInstanceStatus Status,
[property: MemoryPackOrder(3)] InstancePlayerCounts? PlayerCounts,
[property: MemoryPackOrder(4)] bool LaunchAutomatically
[property: MemoryPackOrder(3)] bool LaunchAutomatically
) {
public static Instance Offline(Guid instanceGuid, InstanceConfiguration configuration, bool launchAutomatically = false) {
return new Instance(instanceGuid, configuration, InstanceStatus.Offline, PlayerCounts: null, launchAutomatically);
return new Instance(instanceGuid, configuration, InstanceStatus.Offline, launchAutomatically);
}
}

View File

@@ -1,5 +1,4 @@
using System.Collections.Immutable;
using MemoryPack;
using MemoryPack;
namespace Phantom.Common.Data.Web.Users;
@@ -7,18 +6,5 @@ namespace Phantom.Common.Data.Web.Users;
public sealed partial record AuthenticatedUserInfo(
[property: MemoryPackOrder(0)] Guid Guid,
[property: MemoryPackOrder(1)] string Name,
[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);
}
}
[property: MemoryPackOrder(2)] PermissionSet Permissions
);

View File

@@ -6,5 +6,5 @@ namespace Phantom.Common.Data.Web.Users;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record LogInSuccess(
[property: MemoryPackOrder(0)] AuthenticatedUserInfo UserInfo,
[property: MemoryPackOrder(1)] ImmutableArray<byte> AuthToken
[property: MemoryPackOrder(1)] ImmutableArray<byte> Token
);

View File

@@ -14,9 +14,6 @@ public sealed record Permission(string Id, Permission? Parent) {
return Register(id, this);
}
public const string ManageAllAgentsPolicy = "Agents.ManageAll";
public static readonly Permission ManageAllAgents = Register(ManageAllAgentsPolicy);
public const string ViewInstancesPolicy = "Instances.View";
public static readonly Permission ViewInstances = Register(ViewInstancesPolicy);

View File

@@ -1,5 +0,0 @@
namespace Phantom.Common.Data.Web.Users;
public enum UserActionFailure {
NotAuthorized
}

View File

@@ -1,25 +0,0 @@
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;

View File

@@ -1,9 +0,0 @@
using MemoryPack;
namespace Phantom.Common.Data.Instance;
[MemoryPackable(GenerateType.VersionTolerant)]
public readonly partial record struct InstancePlayerCounts(
[property: MemoryPackOrder(0)] int Online,
[property: MemoryPackOrder(1)] int Maximum
);

View File

@@ -2,5 +2,6 @@
public enum MinecraftServerKind : ushort {
Vanilla = 1,
Fabric = 2
Fabric = 2,
Forge = 3
}

View File

@@ -1,6 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using MemoryPack;
using Phantom.Utils.Result;
namespace Phantom.Common.Data;
@@ -34,18 +33,10 @@ public sealed partial class Result<TValue, TError> {
return hasValue && EqualityComparer<TValue>.Default.Equals(value, expectedValue);
}
public TOutput Into<TOutput>(Func<TValue, TOutput> valueConverter, Func<TError, TOutput> errorConverter) {
public TOutput Map<TOutput>(Func<TValue, TOutput> valueConverter, Func<TError, TOutput> errorConverter) {
return hasValue ? valueConverter(value!) : errorConverter(error!);
}
public Result<TValue, TNewError> MapError<TNewError>(Func<TError, TNewError> errorConverter) {
return hasValue ? value! : errorConverter(error!);
}
public Utils.Result.Result Variant() {
return hasValue ? new Ok<TValue>(Value) : new Err<TError>(Error);
}
public static implicit operator Result<TValue, TError>(TValue value) {
return new Result<TValue, TError>(hasValue: true, value, default);
}

View File

@@ -31,7 +31,6 @@ public static class AgentMessageRegistries {
ToController.Add<InstanceOutputMessage>(5);
ToController.Add<ReportAgentStatusMessage>(6);
ToController.Add<ReportInstanceEventMessage>(7);
ToController.Add<ReportInstancePlayerCountsMessage>(8);
ToController.Add<ReplyMessage>(127);
}

View File

@@ -1,10 +0,0 @@
using MemoryPack;
using Phantom.Common.Data.Instance;
namespace Phantom.Common.Messages.Agent.ToController;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record ReportInstancePlayerCountsMessage(
[property: MemoryPackOrder(0)] Guid InstanceGuid,
[property: MemoryPackOrder(1)] InstancePlayerCounts? PlayerCounts
) : IMessageToController;

View File

@@ -1,6 +1,5 @@
using System.Collections.Immutable;
using MemoryPack;
using Phantom.Common.Data;
using Phantom.Common.Data.Web.Users;
using Phantom.Utils.Actor;
@@ -8,8 +7,8 @@ namespace Phantom.Common.Messages.Web.ToController;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record ChangeUserRolesMessage(
[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
[property: MemoryPackOrder(1)] Guid SubjectUserGuid,
[property: MemoryPackOrder(2)] ImmutableHashSet<Guid> AddToRoleGuids,
[property: MemoryPackOrder(3)] ImmutableHashSet<Guid> RemoveFromRoleGuids
) : IMessageToController, ICanReply<Result<ChangeUserRolesResult, UserActionFailure>>;
) : IMessageToController, ICanReply<ChangeUserRolesResult>;

View File

@@ -1,16 +1,15 @@
using System.Collections.Immutable;
using MemoryPack;
using MemoryPack;
using Phantom.Common.Data;
using Phantom.Common.Data.Instance;
using Phantom.Common.Data.Replies;
using Phantom.Common.Data.Web.Instance;
using Phantom.Common.Data.Web.Users;
using Phantom.Utils.Actor;
namespace Phantom.Common.Messages.Web.ToController;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record CreateOrUpdateInstanceMessage(
[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
[property: MemoryPackOrder(1)] Guid InstanceGuid,
[property: MemoryPackOrder(2)] InstanceConfiguration Configuration
) : IMessageToController, ICanReply<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>;
) : IMessageToController, ICanReply<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>;

View File

@@ -1,6 +1,4 @@
using System.Collections.Immutable;
using MemoryPack;
using Phantom.Common.Data;
using MemoryPack;
using Phantom.Common.Data.Web.Users;
using Phantom.Utils.Actor;
@@ -8,7 +6,7 @@ namespace Phantom.Common.Messages.Web.ToController;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record CreateUserMessage(
[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
[property: MemoryPackOrder(1)] string Username,
[property: MemoryPackOrder(2)] string Password
) : IMessageToController, ICanReply<Result<CreateUserResult, UserActionFailure>>;
) : IMessageToController, ICanReply<CreateUserResult>;

View File

@@ -1,6 +1,4 @@
using System.Collections.Immutable;
using MemoryPack;
using Phantom.Common.Data;
using MemoryPack;
using Phantom.Common.Data.Web.Users;
using Phantom.Utils.Actor;
@@ -8,6 +6,6 @@ namespace Phantom.Common.Messages.Web.ToController;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record DeleteUserMessage(
[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
[property: MemoryPackOrder(1)] Guid SubjectUserGuid
) : IMessageToController, ICanReply<Result<DeleteUserResult, UserActionFailure>>;
) : IMessageToController, ICanReply<DeleteUserResult>;

View File

@@ -1,14 +1,11 @@
using System.Collections.Immutable;
using MemoryPack;
using Phantom.Common.Data;
using Phantom.Common.Data.Web.AuditLog;
using Phantom.Common.Data.Web.Users;
using Phantom.Utils.Actor;
namespace Phantom.Common.Messages.Web.ToController;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record GetAuditLogMessage(
[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
[property: MemoryPackOrder(1)] int Count
) : IMessageToController, ICanReply<Result<ImmutableArray<AuditLogItem>, UserActionFailure>>;
[property: MemoryPackOrder(0)] int Count
) : IMessageToController, ICanReply<ImmutableArray<AuditLogItem>>;

View File

@@ -9,5 +9,5 @@ namespace Phantom.Common.Messages.Web.ToController;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record GetAuthenticatedUser(
[property: MemoryPackOrder(0)] Guid UserGuid,
[property: MemoryPackOrder(1)] ImmutableArray<byte> AuthToken
[property: MemoryPackOrder(1)] ImmutableArray<byte> SessionToken
) : IMessageToController, ICanReply<Optional<AuthenticatedUserInfo>>;

View File

@@ -1,14 +1,11 @@
using System.Collections.Immutable;
using MemoryPack;
using Phantom.Common.Data;
using Phantom.Common.Data.Web.EventLog;
using Phantom.Common.Data.Web.Users;
using Phantom.Utils.Actor;
namespace Phantom.Common.Messages.Web.ToController;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record GetEventLogMessage(
[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
[property: MemoryPackOrder(1)] int Count
) : IMessageToController, ICanReply<Result<ImmutableArray<EventLogItem>, UserActionFailure>>;
[property: MemoryPackOrder(0)] int Count
) : IMessageToController, ICanReply<ImmutableArray<EventLogItem>>;

View File

@@ -1,15 +1,13 @@
using System.Collections.Immutable;
using MemoryPack;
using MemoryPack;
using Phantom.Common.Data;
using Phantom.Common.Data.Replies;
using Phantom.Common.Data.Web.Users;
using Phantom.Utils.Actor;
namespace Phantom.Common.Messages.Web.ToController;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record LaunchInstanceMessage(
[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
[property: MemoryPackOrder(1)] Guid AgentGuid,
[property: MemoryPackOrder(2)] Guid InstanceGuid
) : IMessageToController, ICanReply<Result<LaunchInstanceResult, UserInstanceActionFailure>>;
) : IMessageToController, ICanReply<Result<LaunchInstanceResult, InstanceActionFailure>>;

View File

@@ -1,5 +1,4 @@
using MemoryPack;
using Phantom.Common.Data;
using Phantom.Common.Data.Web.Users;
using Phantom.Utils.Actor;
@@ -9,4 +8,4 @@ namespace Phantom.Common.Messages.Web.ToController;
public sealed partial record LogInMessage(
[property: MemoryPackOrder(0)] string Username,
[property: MemoryPackOrder(1)] string Password
) : IMessageToController, ICanReply<Optional<LogInSuccess>>;
) : IMessageToController, ICanReply<LogInSuccess?>;

View File

@@ -1,16 +1,14 @@
using System.Collections.Immutable;
using MemoryPack;
using MemoryPack;
using Phantom.Common.Data;
using Phantom.Common.Data.Replies;
using Phantom.Common.Data.Web.Users;
using Phantom.Utils.Actor;
namespace Phantom.Common.Messages.Web.ToController;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record SendCommandToInstanceMessage(
[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
[property: MemoryPackOrder(1)] Guid AgentGuid,
[property: MemoryPackOrder(2)] Guid InstanceGuid,
[property: MemoryPackOrder(3)] string Command
) : IMessageToController, ICanReply<Result<SendCommandToInstanceResult, UserInstanceActionFailure>>;
) : IMessageToController, ICanReply<Result<SendCommandToInstanceResult, InstanceActionFailure>>;

View File

@@ -1,17 +1,15 @@
using System.Collections.Immutable;
using MemoryPack;
using MemoryPack;
using Phantom.Common.Data;
using Phantom.Common.Data.Minecraft;
using Phantom.Common.Data.Replies;
using Phantom.Common.Data.Web.Users;
using Phantom.Utils.Actor;
namespace Phantom.Common.Messages.Web.ToController;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record StopInstanceMessage(
[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken,
[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
[property: MemoryPackOrder(1)] Guid AgentGuid,
[property: MemoryPackOrder(2)] Guid InstanceGuid,
[property: MemoryPackOrder(3)] MinecraftStopStrategy StopStrategy
) : IMessageToController, ICanReply<Result<StopInstanceResult, UserInstanceActionFailure>>;
) : IMessageToController, ICanReply<Result<StopInstanceResult, InstanceActionFailure>>;

View File

@@ -1,8 +0,0 @@
using MemoryPack;
namespace Phantom.Common.Messages.Web.ToWeb;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record RefreshUserSessionMessage(
[property: MemoryPackOrder(0)] Guid UserGuid
) : IMessageToWeb;

View File

@@ -24,31 +24,30 @@ public static class WebMessageRegistries {
static WebMessageRegistries() {
ToController.Add<RegisterWebMessage>(0);
ToController.Add<UnregisterWebMessage>(1);
ToController.Add<LogInMessage, Optional<LogInSuccess>>(2);
ToController.Add<LogInMessage, LogInSuccess?>(2);
ToController.Add<LogOutMessage>(3);
ToController.Add<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(4);
ToController.Add<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(5);
ToController.Add<CreateUserMessage, Result<CreateUserResult, UserActionFailure>>(6);
ToController.Add<DeleteUserMessage, Result<DeleteUserResult, UserActionFailure>>(7);
ToController.Add<CreateUserMessage, CreateUserResult>(6);
ToController.Add<DeleteUserMessage, DeleteUserResult>(7);
ToController.Add<GetUsersMessage, ImmutableArray<UserInfo>>(8);
ToController.Add<GetRolesMessage, ImmutableArray<RoleInfo>>(9);
ToController.Add<GetUserRolesMessage, ImmutableDictionary<Guid, ImmutableArray<Guid>>>(10);
ToController.Add<ChangeUserRolesMessage, Result<ChangeUserRolesResult, UserActionFailure>>(11);
ToController.Add<CreateOrUpdateInstanceMessage, Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>(12);
ToController.Add<LaunchInstanceMessage, Result<LaunchInstanceResult, UserInstanceActionFailure>>(13);
ToController.Add<StopInstanceMessage, Result<StopInstanceResult, UserInstanceActionFailure>>(14);
ToController.Add<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, UserInstanceActionFailure>>(15);
ToController.Add<ChangeUserRolesMessage, ChangeUserRolesResult>(11);
ToController.Add<CreateOrUpdateInstanceMessage, Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(12);
ToController.Add<LaunchInstanceMessage, Result<LaunchInstanceResult, InstanceActionFailure>>(13);
ToController.Add<StopInstanceMessage, Result<StopInstanceResult, InstanceActionFailure>>(14);
ToController.Add<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, InstanceActionFailure>>(15);
ToController.Add<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(16);
ToController.Add<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(17);
ToController.Add<GetAuditLogMessage, Result<ImmutableArray<AuditLogItem>, UserActionFailure>>(18);
ToController.Add<GetEventLogMessage, Result<ImmutableArray<EventLogItem>, UserActionFailure>>(19);
ToController.Add<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(18);
ToController.Add<GetEventLogMessage, ImmutableArray<EventLogItem>>(19);
ToController.Add<ReplyMessage>(127);
ToWeb.Add<RegisterWebResultMessage>(0);
ToWeb.Add<RefreshAgentsMessage>(1);
ToWeb.Add<RefreshInstancesMessage>(2);
ToWeb.Add<InstanceOutputMessage>(3);
ToWeb.Add<RefreshUserSessionMessage>(4);
ToWeb.Add<ReplyMessage>(127);
}

View File

@@ -1,353 +0,0 @@
// <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
}
}
}

View File

@@ -1,56 +0,0 @@
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");
}
}
}

View File

@@ -18,7 +18,7 @@ namespace Phantom.Controller.Database.Postgres.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.0")
.HasAnnotation("ProductVersion", "7.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
@@ -204,21 +204,6 @@ namespace Phantom.Controller.Database.Postgres.Migrations
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")
@@ -296,21 +281,6 @@ namespace Phantom.Controller.Database.Postgres.Migrations
.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)

View File

@@ -20,7 +20,6 @@ public class ApplicationDbContext : DbContext {
public DbSet<UserRoleEntity> UserRoles { get; init; } = null!;
public DbSet<UserPermissionEntity> UserPermissions { 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<InstanceEntity> Instances { get; init; } = null!;
@@ -63,12 +62,6 @@ public class ApplicationDbContext : DbContext {
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);
});
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) {

View File

@@ -1,14 +0,0 @@
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;
}
}

View File

@@ -17,12 +17,11 @@ public sealed class EventLogRepository {
db.Ctx.EventLog.Add(new EventLogEntity(eventGuid, utcTime, agentGuid, eventType, subjectId, extra));
}
public Task<ImmutableArray<EventLogItem>> GetMostRecentItems(ImmutableHashSet<Guid> agentGuids, int count, CancellationToken cancellationToken) {
public Task<ImmutableArray<EventLogItem>> GetMostRecentItems(int count, CancellationToken cancellationToken) {
return db.Ctx
.EventLog
.AsQueryable()
.OrderByDescending(static entity => entity.UtcTime)
.Where(entity => entity.AgentGuid == null || agentGuids.Contains(entity.AgentGuid.Value))
.Take(count)
.AsAsyncEnumerable()
.Select(static entity => new EventLogItem(entity.UtcTime, entity.AgentGuid, entity.EventType, entity.SubjectType, entity.SubjectId, entity.Data?.RootElement.ToString()))

View File

@@ -1,35 +0,0 @@
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();
}
}

View File

@@ -96,7 +96,6 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
Receive<UpdateJavaRuntimesCommand>(UpdateJavaRuntimes);
ReceiveAndReplyLater<CreateOrUpdateInstanceCommand, Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(CreateOrUpdateInstance);
Receive<UpdateInstanceStatusCommand>(UpdateInstanceStatus);
Receive<UpdateInstancePlayerCountsCommand>(UpdateInstancePlayerCounts);
ReceiveAndReplyLater<LaunchInstanceCommand, Result<LaunchInstanceResult, InstanceActionFailure>>(LaunchInstance);
ReceiveAndReplyLater<StopInstanceCommand, Result<StopInstanceResult, InstanceActionFailure>>(StopInstance);
ReceiveAndReplyLater<SendCommandToInstanceCommand, Result<SendCommandToInstanceResult, InstanceActionFailure>>(SendMinecraftCommand);
@@ -147,20 +146,20 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
}
}
private async Task<Result<TReply, InstanceActionFailure>> RequestInstance<TCommand, TReply>(Guid instanceGuid, TCommand command) where TCommand : InstanceActor.ICommand, ICanReply<Result<TReply, InstanceActionFailure>> {
private 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)) {
return await instance.Request(command, cancellationToken);
return instance.Request(command, cancellationToken);
}
else {
Logger.Warning("Could not deliver command {CommandType} to instance {InstanceGuid}, instance not found.", command.GetType().Name, instanceGuid);
return InstanceActionFailure.InstanceDoesNotExist;
return Task.FromResult<Result<TReply, InstanceActionFailure>>(InstanceActionFailure.InstanceDoesNotExist);
}
}
private async Task<ImmutableArray<ConfigureInstanceMessage>> PrepareInitialConfigurationMessages() {
var configurationMessages = ImmutableArray.CreateBuilder<ConfigureInstanceMessage>();
foreach (var (instanceGuid, instanceConfiguration, _, _, launchAutomatically) in instanceDataByGuid.Values.ToImmutableArray()) {
foreach (var (instanceGuid, instanceConfiguration, _, launchAutomatically) in instanceDataByGuid.Values.ToImmutableArray()) {
var serverExecutableInfo = await minecraftVersions.GetServerExecutableInfo(instanceConfiguration.MinecraftVersion, cancellationToken);
configurationMessages.Add(new ConfigureInstanceMessage(instanceGuid, instanceConfiguration, new InstanceLaunchProperties(serverExecutableInfo), launchAutomatically));
}
@@ -184,17 +183,15 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
public sealed record UpdateJavaRuntimesCommand(ImmutableArray<TaggedJavaRuntime> JavaRuntimes) : ICommand;
public sealed record CreateOrUpdateInstanceCommand(Guid LoggedInUserGuid, Guid InstanceGuid, InstanceConfiguration Configuration) : ICommand, ICanReply<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>;
public sealed record CreateOrUpdateInstanceCommand(Guid AuditLogUserGuid, Guid InstanceGuid, InstanceConfiguration Configuration) : ICommand, ICanReply<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>;
public sealed record UpdateInstanceStatusCommand(Guid InstanceGuid, IInstanceStatus Status) : ICommand;
public sealed record UpdateInstancePlayerCountsCommand(Guid InstanceGuid, InstancePlayerCounts? PlayerCounts) : 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 LoggedInUserGuid, Guid InstanceGuid, string Command) : ICommand, ICanReply<Result<SendCommandToInstanceResult, InstanceActionFailure>>;
public sealed record SendCommandToInstanceCommand(Guid InstanceGuid, Guid AuditLogUserGuid, string Command) : ICommand, ICanReply<Result<SendCommandToInstanceResult, InstanceActionFailure>>;
public sealed record ReceiveInstanceDataCommand(Instance Instance) : ICommand, IJumpAhead;
@@ -311,7 +308,7 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
instanceActorRef = CreateNewInstance(Instance.Offline(command.InstanceGuid, instanceConfiguration));
}
var configureInstanceCommand = new InstanceActor.ConfigureInstanceCommand(command.LoggedInUserGuid, command.InstanceGuid, instanceConfiguration, new InstanceLaunchProperties(serverExecutableInfo), isCreatingInstance);
var configureInstanceCommand = new InstanceActor.ConfigureInstanceCommand(command.AuditLogUserGuid, command.InstanceGuid, instanceConfiguration, new InstanceLaunchProperties(serverExecutableInfo), isCreatingInstance);
return instanceActorRef.Request(configureInstanceCommand, cancellationToken)
.ContinueOnActor(CreateOrUpdateInstance2, configureInstanceCommand);
@@ -333,7 +330,7 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
else {
string action = isCreating ? "adding" : "editing";
string relation = isCreating ? "to agent" : "in agent";
string reason = result.Into(ConfigureInstanceResultExtensions.ToSentence, InstanceActionFailureExtensions.ToSentence);
string reason = result.Map(ConfigureInstanceResultExtensions.ToSentence, InstanceActionFailureExtensions.ToSentence);
Logger.Information("Failed " + action + " instance \"{InstanceName}\" (GUID {InstanceGuid}) " + relation + " \"{AgentName}\". {ErrorMessage}", instanceName, instanceGuid, configuration.AgentName, reason);
@@ -345,20 +342,16 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> {
TellInstance(command.InstanceGuid, new InstanceActor.SetStatusCommand(command.Status));
}
private void UpdateInstancePlayerCounts(UpdateInstancePlayerCountsCommand command) {
TellInstance(command.InstanceGuid, new InstanceActor.SetPlayerCountsCommand(command.PlayerCounts));
}
private Task<Result<LaunchInstanceResult, InstanceActionFailure>> LaunchInstance(LaunchInstanceCommand command) {
return RequestInstance<InstanceActor.LaunchInstanceCommand, LaunchInstanceResult>(command.InstanceGuid, new InstanceActor.LaunchInstanceCommand(command.LoggedInUserGuid));
return RequestInstance<InstanceActor.LaunchInstanceCommand, LaunchInstanceResult>(command.InstanceGuid, new InstanceActor.LaunchInstanceCommand(command.AuditLogUserGuid));
}
private Task<Result<StopInstanceResult, InstanceActionFailure>> StopInstance(StopInstanceCommand command) {
return RequestInstance<InstanceActor.StopInstanceCommand, StopInstanceResult>(command.InstanceGuid, new InstanceActor.StopInstanceCommand(command.LoggedInUserGuid, command.StopStrategy));
return RequestInstance<InstanceActor.StopInstanceCommand, StopInstanceResult>(command.InstanceGuid, new InstanceActor.StopInstanceCommand(command.AuditLogUserGuid, command.StopStrategy));
}
private Task<Result<SendCommandToInstanceResult, InstanceActionFailure>> SendMinecraftCommand(SendCommandToInstanceCommand command) {
return RequestInstance<InstanceActor.SendCommandToInstanceCommand, SendCommandToInstanceResult>(command.InstanceGuid, new InstanceActor.SendCommandToInstanceCommand(command.LoggedInUserGuid, command.Command));
return RequestInstance<InstanceActor.SendCommandToInstanceCommand, SendCommandToInstanceResult>(command.InstanceGuid, new InstanceActor.SendCommandToInstanceCommand(command.AuditLogUserGuid, command.Command));
}
private void ReceiveInstanceData(ReceiveInstanceDataCommand command) {

View File

@@ -1,16 +1,13 @@
using System.Collections.Concurrent;
using System.Collections.Immutable;
using Akka.Actor;
using Phantom.Common.Data;
using Phantom.Common.Data.Agent;
using Phantom.Common.Data.Replies;
using Phantom.Common.Data.Web.Agent;
using Phantom.Common.Data.Web.Users;
using Phantom.Common.Messages.Agent;
using Phantom.Common.Messages.Agent.ToAgent;
using Phantom.Controller.Database;
using Phantom.Controller.Minecraft;
using Phantom.Controller.Services.Users.Sessions;
using Phantom.Utils.Actor;
using Phantom.Utils.Logging;
using Phantom.Utils.Rpc.Runtime;
@@ -25,19 +22,17 @@ sealed class AgentManager {
private readonly AuthToken authToken;
private readonly ControllerState controllerState;
private readonly MinecraftVersions minecraftVersions;
private readonly UserLoginManager userLoginManager;
private readonly IDbContextProvider dbProvider;
private readonly CancellationToken cancellationToken;
private readonly ConcurrentDictionary<Guid, ActorRef<AgentActor.ICommand>> agentsByGuid = new ();
private readonly Func<Guid, AgentConfiguration, ActorRef<AgentActor.ICommand>> addAgentActorFactory;
public AgentManager(IActorRefFactory actorSystem, AuthToken authToken, ControllerState controllerState, MinecraftVersions minecraftVersions, UserLoginManager userLoginManager, IDbContextProvider dbProvider, CancellationToken cancellationToken) {
public AgentManager(IActorRefFactory actorSystem, AuthToken authToken, ControllerState controllerState, MinecraftVersions minecraftVersions, IDbContextProvider dbProvider, CancellationToken cancellationToken) {
this.actorSystem = actorSystem;
this.authToken = authToken;
this.controllerState = controllerState;
this.minecraftVersions = minecraftVersions;
this.userLoginManager = userLoginManager;
this.dbProvider = dbProvider;
this.cancellationToken = cancellationToken;
@@ -88,18 +83,7 @@ sealed class AgentManager {
}
}
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>> {
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);
public async Task<Result<TReply, InstanceActionFailure>> DoInstanceAction<TCommand, TReply>(Guid agentGuid, TCommand command) where TCommand : class, AgentActor.ICommand, ICanReply<Result<TReply, InstanceActionFailure>> {
return agentsByGuid.TryGetValue(agentGuid, out var agent) ? await agent.Request(command, cancellationToken) : InstanceActionFailure.AgentDoesNotExist;
}
}

View File

@@ -11,7 +11,6 @@ using Phantom.Controller.Services.Events;
using Phantom.Controller.Services.Instances;
using Phantom.Controller.Services.Rpc;
using Phantom.Controller.Services.Users;
using Phantom.Controller.Services.Users.Sessions;
using Phantom.Utils.Actor;
using Phantom.Utils.Rpc.Runtime;
using IMessageFromAgentToController = Phantom.Common.Messages.Agent.IMessageToController;
@@ -25,19 +24,18 @@ public sealed class ControllerServices : IDisposable {
private ControllerState ControllerState { get; }
private MinecraftVersions MinecraftVersions { get; }
private AuthenticatedUserCache AuthenticatedUserCache { get; }
private UserManager UserManager { get; }
private RoleManager RoleManager { get; }
private UserRoleManager UserRoleManager { get; }
private UserLoginManager UserLoginManager { get; }
private PermissionManager PermissionManager { get; }
private AgentManager AgentManager { get; }
private InstanceLogManager InstanceLogManager { get; }
private AuditLogManager AuditLogManager { get; }
private EventLogManager EventLogManager { get; }
private UserManager UserManager { get; }
private RoleManager RoleManager { get; }
private PermissionManager PermissionManager { get; }
private UserRoleManager UserRoleManager { get; }
private UserLoginManager UserLoginManager { get; }
private AuditLogManager AuditLogManager { get; }
public IRegistrationHandler<IMessageToAgent, IMessageFromAgentToController, RegisterAgentMessage> AgentRegistrationHandler { get; }
public IRegistrationHandler<IMessageToWeb, IMessageFromWebToController, RegisterWebMessage> WebRegistrationHandler { get; }
@@ -53,18 +51,17 @@ public sealed class ControllerServices : IDisposable {
this.ControllerState = new ControllerState();
this.MinecraftVersions = new MinecraftVersions();
this.AuthenticatedUserCache = new AuthenticatedUserCache();
this.UserManager = new UserManager(AuthenticatedUserCache, ControllerState, 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.AgentManager = new AgentManager(ActorSystem, agentAuthToken, ControllerState, MinecraftVersions, UserLoginManager, dbProvider, cancellationToken);
this.AgentManager = new AgentManager(ActorSystem, agentAuthToken, ControllerState, MinecraftVersions, dbProvider, cancellationToken);
this.InstanceLogManager = new InstanceLogManager();
this.UserManager = new UserManager(dbProvider);
this.RoleManager = new RoleManager(dbProvider);
this.PermissionManager = new PermissionManager(dbProvider);
this.UserRoleManager = new UserRoleManager(dbProvider);
this.UserLoginManager = new UserLoginManager(UserManager, PermissionManager, dbProvider);
this.AuditLogManager = new AuditLogManager(dbProvider);
this.EventLogManager = new EventLogManager(ControllerState, ActorSystem, dbProvider, shutdownCancellationToken);
this.EventLogManager = new EventLogManager(ActorSystem, dbProvider, shutdownCancellationToken);
this.AgentRegistrationHandler = new AgentRegistrationHandler(AgentManager, InstanceLogManager, EventLogManager);
this.WebRegistrationHandler = new WebRegistrationHandler(webAuthToken, ControllerState, InstanceLogManager, UserManager, RoleManager, UserRoleManager, UserLoginManager, AuditLogManager, AgentManager, MinecraftVersions, EventLogManager);

View File

@@ -19,8 +19,6 @@ sealed class ControllerState {
public ObservableState<ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>.Receiver AgentJavaRuntimesByGuidReceiver => agentJavaRuntimesByGuid.ReceiverSide;
public ObservableState<ImmutableDictionary<Guid, Instance>>.Receiver InstancesByGuidReceiver => instancesByGuid.ReceiverSide;
public event EventHandler<Guid>? UserUpdatedOrDeleted;
public void UpdateAgent(Agent agent) {
agentsByGuid.PublisherSide.Publish(static (agentsByGuid, agent) => agentsByGuid.SetItem(agent.AgentGuid, agent), agent);
}
@@ -32,8 +30,4 @@ sealed class ControllerState {
public void UpdateInstance(Instance instance) {
instancesByGuid.PublisherSide.Publish(static (instancesByGuid, instance) => instancesByGuid.SetItem(instance.InstanceGuid, instance), instance);
}
public void UpdateOrDeleteUser(Guid userGuid) {
UserUpdatedOrDeleted?.Invoke(null, userGuid);
}
}

View File

@@ -1,23 +1,18 @@
using System.Collections.Immutable;
using Akka.Actor;
using Phantom.Common.Data;
using Phantom.Common.Data.Web.EventLog;
using Phantom.Common.Data.Web.Users;
using Phantom.Controller.Database;
using Phantom.Controller.Database.Repositories;
using Phantom.Controller.Services.Users.Sessions;
using Phantom.Utils.Actor;
namespace Phantom.Controller.Services.Events;
sealed partial class EventLogManager {
private readonly ControllerState controllerState;
private readonly ActorRef<EventLogDatabaseStorageActor.ICommand> databaseStorageActor;
private readonly IDbContextProvider dbProvider;
private readonly CancellationToken cancellationToken;
public EventLogManager(ControllerState controllerState, IActorRefFactory actorSystem, IDbContextProvider dbProvider, CancellationToken cancellationToken) {
this.controllerState = controllerState;
public EventLogManager(IActorRefFactory actorSystem, IDbContextProvider dbProvider, CancellationToken cancellationToken) {
this.databaseStorageActor = actorSystem.ActorOf(EventLogDatabaseStorageActor.Factory(new EventLogDatabaseStorageActor.Init(dbProvider, cancellationToken)), "EventLogDatabaseStorage");
this.dbProvider = dbProvider;
this.cancellationToken = cancellationToken;
@@ -27,14 +22,8 @@ sealed partial class EventLogManager {
databaseStorageActor.Tell(new EventLogDatabaseStorageActor.StoreEventCommand(eventGuid, utcTime, agentGuid, eventType, subjectId, extra));
}
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());
public async Task<ImmutableArray<EventLogItem>> GetMostRecentItems(int count) {
await using var db = dbProvider.Lazy();
return await new EventLogRepository(db).GetMostRecentItems(accessibleAgentGuids, count, cancellationToken);
return await new EventLogRepository(db).GetMostRecentItems(count, cancellationToken);
}
}

View File

@@ -26,7 +26,6 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> {
private InstanceConfiguration configuration;
private IInstanceStatus status;
private InstancePlayerCounts? playerCounts;
private bool launchAutomatically;
private readonly ActorRef<InstanceDatabaseStorageActor.ICommand> databaseStorageActor;
@@ -36,12 +35,11 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> {
this.agentConnection = init.AgentConnection;
this.cancellationToken = init.CancellationToken;
(this.instanceGuid, this.configuration, this.status, this.playerCounts, this.launchAutomatically) = init.Instance;
(this.instanceGuid, this.configuration, this.status, this.launchAutomatically) = init.Instance;
this.databaseStorageActor = Context.ActorOf(InstanceDatabaseStorageActor.Factory(new InstanceDatabaseStorageActor.Init(instanceGuid, init.DbProvider, init.CancellationToken)), "DatabaseStorage");
Receive<SetStatusCommand>(SetStatus);
Receive<SetPlayerCountsCommand>(SetPlayerCounts);
ReceiveAsyncAndReply<ConfigureInstanceCommand, Result<ConfigureInstanceResult, InstanceActionFailure>>(ConfigureInstance);
ReceiveAsyncAndReply<LaunchInstanceCommand, Result<LaunchInstanceResult, InstanceActionFailure>>(LaunchInstance);
ReceiveAsyncAndReply<StopInstanceCommand, Result<StopInstanceResult, InstanceActionFailure>>(StopInstance);
@@ -49,7 +47,7 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> {
}
private void NotifyInstanceUpdated() {
agentActor.Tell(new AgentActor.ReceiveInstanceDataCommand(new Instance(instanceGuid, configuration, status, playerCounts, launchAutomatically)));
agentActor.Tell(new AgentActor.ReceiveInstanceDataCommand(new Instance(instanceGuid, configuration, status, launchAutomatically)));
}
private void SetLaunchAutomatically(bool newValue) {
@@ -68,8 +66,6 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> {
public sealed record SetStatusCommand(IInstanceStatus Status) : ICommand;
public sealed record SetPlayerCountsCommand(InstancePlayerCounts? PlayerCounts) : ICommand;
public sealed record ConfigureInstanceCommand(Guid AuditLogUserGuid, Guid InstanceGuid, InstanceConfiguration Configuration, InstanceLaunchProperties LaunchProperties, bool IsCreatingInstance) : ICommand, ICanReply<Result<ConfigureInstanceResult, InstanceActionFailure>>;
public sealed record LaunchInstanceCommand(Guid AuditLogUserGuid) : ICommand, ICanReply<Result<LaunchInstanceResult, InstanceActionFailure>>;
@@ -80,16 +76,6 @@ sealed class InstanceActor : ReceiveActor<InstanceActor.ICommand> {
private void SetStatus(SetStatusCommand command) {
status = command.Status;
if (!status.IsRunning() && status != InstanceStatus.Offline /* Guard against temporary disconnects */) {
playerCounts = null;
}
NotifyInstanceUpdated();
}
private void SetPlayerCounts(SetPlayerCountsCommand command) {
playerCounts = command.PlayerCounts;
NotifyInstanceUpdated();
}

View File

@@ -39,7 +39,6 @@ sealed class AgentMessageHandlerActor : ReceiveActor<IMessageToController> {
Receive<AdvertiseJavaRuntimesMessage>(HandleAdvertiseJavaRuntimes);
Receive<ReportAgentStatusMessage>(HandleReportAgentStatus);
Receive<ReportInstanceStatusMessage>(HandleReportInstanceStatus);
Receive<ReportInstancePlayerCountsMessage>(HandleReportInstancePlayerCounts);
Receive<ReportInstanceEventMessage>(HandleReportInstanceEvent);
Receive<InstanceOutputMessage>(HandleInstanceOutput);
Receive<ReplyMessage>(HandleReply);
@@ -75,10 +74,6 @@ sealed class AgentMessageHandlerActor : ReceiveActor<IMessageToController> {
agentManager.TellAgent(agentGuid, new AgentActor.UpdateInstanceStatusCommand(message.InstanceGuid, message.InstanceStatus));
}
private void HandleReportInstancePlayerCounts(ReportInstancePlayerCountsMessage message) {
agentManager.TellAgent(agentGuid, new AgentActor.UpdateInstancePlayerCountsCommand(message.InstanceGuid, message.PlayerCounts));
}
private void HandleReportInstanceEvent(ReportInstanceEventMessage message) {
message.Event.Accept(eventLogManager.CreateInstanceEventVisitor(message.EventGuid, message.UtcTime, agentGuid, message.InstanceGuid));
}

View File

@@ -30,31 +30,22 @@ sealed class WebMessageDataUpdateSenderActor : ReceiveActor<WebMessageDataUpdate
ReceiveAsync<RefreshAgentsCommand>(RefreshAgents);
ReceiveAsync<RefreshInstancesCommand>(RefreshInstances);
ReceiveAsync<ReceiveInstanceLogsCommand>(ReceiveInstanceLogs);
ReceiveAsync<RefreshUserSessionCommand>(RefreshUserSession);
}
protected override void PreStart() {
controllerState.AgentsByGuidReceiver.Register(SelfTyped, static state => new RefreshAgentsCommand(state));
controllerState.InstancesByGuidReceiver.Register(SelfTyped, static state => new RefreshInstancesCommand(state));
controllerState.UserUpdatedOrDeleted += OnUserUpdatedOrDeleted;
instanceLogManager.LogsReceived += OnInstanceLogsReceived;
}
protected override void PostStop() {
instanceLogManager.LogsReceived -= OnInstanceLogsReceived;
controllerState.UserUpdatedOrDeleted -= OnUserUpdatedOrDeleted;
controllerState.AgentsByGuidReceiver.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) {
selfCached.Tell(new ReceiveInstanceLogsCommand(e.InstanceGuid, e.Lines));
}
@@ -67,8 +58,6 @@ sealed class WebMessageDataUpdateSenderActor : ReceiveActor<WebMessageDataUpdate
private sealed record ReceiveInstanceLogsCommand(Guid InstanceGuid, ImmutableArray<string> Lines) : ICommand;
private sealed record RefreshUserSessionCommand(Guid UserGuid) : ICommand;
private Task RefreshAgents(RefreshAgentsCommand command) {
return connection.Send(new RefreshAgentsMessage(command.Agents.Values.ToImmutableArray()));
}
@@ -80,8 +69,4 @@ sealed class WebMessageDataUpdateSenderActor : ReceiveActor<WebMessageDataUpdate
private Task ReceiveInstanceLogs(ReceiveInstanceLogsCommand command) {
return connection.Send(new InstanceOutputMessage(command.InstanceGuid, command.Lines));
}
private Task RefreshUserSession(RefreshUserSessionCommand command) {
return connection.Send(new RefreshUserSessionMessage(command.UserGuid));
}
}

View File

@@ -15,7 +15,6 @@ using Phantom.Controller.Services.Agents;
using Phantom.Controller.Services.Events;
using Phantom.Controller.Services.Instances;
using Phantom.Controller.Services.Users;
using Phantom.Controller.Services.Users.Sessions;
using Phantom.Utils.Actor;
using Phantom.Utils.Rpc.Runtime;
@@ -71,24 +70,24 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
ReceiveAsync<RegisterWebMessage>(HandleRegisterWeb);
Receive<UnregisterWebMessage>(HandleUnregisterWeb);
ReceiveAndReplyLater<LogInMessage, Optional<LogInSuccess>>(HandleLogIn);
ReceiveAndReplyLater<LogInMessage, LogInSuccess?>(HandleLogIn);
Receive<LogOutMessage>(HandleLogOut);
ReceiveAndReply<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(GetAuthenticatedUser);
ReceiveAndReplyLater<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(HandleCreateOrUpdateAdministratorUser);
ReceiveAndReplyLater<CreateUserMessage, Result<CreateUserResult, UserActionFailure>>(HandleCreateUser);
ReceiveAndReplyLater<CreateUserMessage, CreateUserResult>(HandleCreateUser);
ReceiveAndReplyLater<GetUsersMessage, ImmutableArray<UserInfo>>(HandleGetUsers);
ReceiveAndReplyLater<GetRolesMessage, ImmutableArray<RoleInfo>>(HandleGetRoles);
ReceiveAndReplyLater<GetUserRolesMessage, ImmutableDictionary<Guid, ImmutableArray<Guid>>>(HandleGetUserRoles);
ReceiveAndReplyLater<ChangeUserRolesMessage, Result<ChangeUserRolesResult, UserActionFailure>>(HandleChangeUserRoles);
ReceiveAndReplyLater<DeleteUserMessage, Result<DeleteUserResult, UserActionFailure>>(HandleDeleteUser);
ReceiveAndReplyLater<CreateOrUpdateInstanceMessage, Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>(HandleCreateOrUpdateInstance);
ReceiveAndReplyLater<LaunchInstanceMessage, Result<LaunchInstanceResult, UserInstanceActionFailure>>(HandleLaunchInstance);
ReceiveAndReplyLater<StopInstanceMessage, Result<StopInstanceResult, UserInstanceActionFailure>>(HandleStopInstance);
ReceiveAndReplyLater<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, UserInstanceActionFailure>>(HandleSendCommandToInstance);
ReceiveAndReplyLater<ChangeUserRolesMessage, ChangeUserRolesResult>(HandleChangeUserRoles);
ReceiveAndReplyLater<DeleteUserMessage, DeleteUserResult>(HandleDeleteUser);
ReceiveAndReplyLater<CreateOrUpdateInstanceMessage, Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(HandleCreateOrUpdateInstance);
ReceiveAndReplyLater<LaunchInstanceMessage, Result<LaunchInstanceResult, InstanceActionFailure>>(HandleLaunchInstance);
ReceiveAndReplyLater<StopInstanceMessage, Result<StopInstanceResult, InstanceActionFailure>>(HandleStopInstance);
ReceiveAndReplyLater<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, InstanceActionFailure>>(HandleSendCommandToInstance);
ReceiveAndReplyLater<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(HandleGetMinecraftVersions);
ReceiveAndReply<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(HandleGetAgentJavaRuntimes);
ReceiveAndReplyLater<GetAuditLogMessage, Result<ImmutableArray<AuditLogItem>, UserActionFailure>>(HandleGetAuditLog);
ReceiveAndReplyLater<GetEventLogMessage, Result<ImmutableArray<EventLogItem>, UserActionFailure>>(HandleGetEventLog);
ReceiveAndReplyLater<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(HandleGetAuditLog);
ReceiveAndReplyLater<GetEventLogMessage, ImmutableArray<EventLogItem>>(HandleGetEventLog);
Receive<ReplyMessage>(HandleReply);
}
@@ -100,7 +99,7 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
connection.Close();
}
private Task<Optional<LogInSuccess>> HandleLogIn(LogInMessage message) {
private Task<LogInSuccess?> HandleLogIn(LogInMessage message) {
return userLoginManager.LogIn(message.Username, message.Password);
}
@@ -109,15 +108,15 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
}
private Optional<AuthenticatedUserInfo> GetAuthenticatedUser(GetAuthenticatedUser message) {
return userLoginManager.GetAuthenticatedUser(message.UserGuid, message.AuthToken);
return userLoginManager.GetAuthenticatedUser(message.UserGuid, message.SessionToken);
}
private Task<CreateOrUpdateAdministratorUserResult> HandleCreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUserMessage message) {
return userManager.CreateOrUpdateAdministrator(message.Username, message.Password);
}
private Task<Result<CreateUserResult, UserActionFailure>> HandleCreateUser(CreateUserMessage message) {
return userManager.Create(userLoginManager.GetLoggedInUser(message.AuthToken), message.Username, message.Password);
private Task<CreateUserResult> HandleCreateUser(CreateUserMessage message) {
return userManager.Create(message.LoggedInUserGuid, message.Username, message.Password);
}
private Task<ImmutableArray<UserInfo>> HandleGetUsers(GetUsersMessage message) {
@@ -132,48 +131,28 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
return userRoleManager.GetUserRoles(message.UserGuids);
}
private Task<Result<ChangeUserRolesResult, UserActionFailure>> HandleChangeUserRoles(ChangeUserRolesMessage message) {
return userRoleManager.ChangeUserRoles(userLoginManager.GetLoggedInUser(message.AuthToken), message.SubjectUserGuid, message.AddToRoleGuids, message.RemoveFromRoleGuids);
private Task<ChangeUserRolesResult> HandleChangeUserRoles(ChangeUserRolesMessage message) {
return userRoleManager.ChangeUserRoles(message.LoggedInUserGuid, message.SubjectUserGuid, message.AddToRoleGuids, message.RemoveFromRoleGuids);
}
private Task<Result<DeleteUserResult, UserActionFailure>> HandleDeleteUser(DeleteUserMessage message) {
return userManager.DeleteByGuid(userLoginManager.GetLoggedInUser(message.AuthToken), message.SubjectUserGuid);
private Task<DeleteUserResult> HandleDeleteUser(DeleteUserMessage message) {
return userManager.DeleteByGuid(message.LoggedInUserGuid, message.SubjectUserGuid);
}
private Task<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>> HandleCreateOrUpdateInstance(CreateOrUpdateInstanceMessage message) {
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<CreateOrUpdateInstanceResult, InstanceActionFailure>> HandleCreateOrUpdateInstance(CreateOrUpdateInstanceMessage message) {
return agentManager.DoInstanceAction<AgentActor.CreateOrUpdateInstanceCommand, CreateOrUpdateInstanceResult>(message.Configuration.AgentGuid, new AgentActor.CreateOrUpdateInstanceCommand(message.LoggedInUserGuid, message.InstanceGuid, message.Configuration));
}
private Task<Result<LaunchInstanceResult, UserInstanceActionFailure>> HandleLaunchInstance(LaunchInstanceMessage message) {
return agentManager.DoInstanceAction<AgentActor.LaunchInstanceCommand, LaunchInstanceResult>(
Permission.ControlInstances,
message.AuthToken,
message.AgentGuid,
loggedInUserGuid => new AgentActor.LaunchInstanceCommand(loggedInUserGuid, message.InstanceGuid)
);
private Task<Result<LaunchInstanceResult, InstanceActionFailure>> HandleLaunchInstance(LaunchInstanceMessage message) {
return agentManager.DoInstanceAction<AgentActor.LaunchInstanceCommand, LaunchInstanceResult>(message.AgentGuid, new AgentActor.LaunchInstanceCommand(message.InstanceGuid, message.LoggedInUserGuid));
}
private Task<Result<StopInstanceResult, UserInstanceActionFailure>> HandleStopInstance(StopInstanceMessage message) {
return agentManager.DoInstanceAction<AgentActor.StopInstanceCommand, StopInstanceResult>(
Permission.ControlInstances,
message.AuthToken,
message.AgentGuid,
loggedInUserGuid => new AgentActor.StopInstanceCommand(loggedInUserGuid, message.InstanceGuid, message.StopStrategy)
);
private Task<Result<StopInstanceResult, InstanceActionFailure>> HandleStopInstance(StopInstanceMessage message) {
return agentManager.DoInstanceAction<AgentActor.StopInstanceCommand, StopInstanceResult>(message.AgentGuid, new AgentActor.StopInstanceCommand(message.InstanceGuid, message.LoggedInUserGuid, message.StopStrategy));
}
private Task<Result<SendCommandToInstanceResult, UserInstanceActionFailure>> HandleSendCommandToInstance(SendCommandToInstanceMessage message) {
return agentManager.DoInstanceAction<AgentActor.SendCommandToInstanceCommand, SendCommandToInstanceResult>(
Permission.ControlInstances,
message.AuthToken,
message.AgentGuid,
loggedInUserGuid => new AgentActor.SendCommandToInstanceCommand(loggedInUserGuid, message.InstanceGuid, message.Command)
);
private Task<Result<SendCommandToInstanceResult, InstanceActionFailure>> HandleSendCommandToInstance(SendCommandToInstanceMessage message) {
return agentManager.DoInstanceAction<AgentActor.SendCommandToInstanceCommand, SendCommandToInstanceResult>(message.AgentGuid, new AgentActor.SendCommandToInstanceCommand(message.InstanceGuid, message.LoggedInUserGuid, message.Command));
}
private Task<ImmutableArray<MinecraftVersion>> HandleGetMinecraftVersions(GetMinecraftVersionsMessage message) {
@@ -184,12 +163,12 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> {
return controllerState.AgentJavaRuntimesByGuid;
}
private Task<Result<ImmutableArray<AuditLogItem>, UserActionFailure>> HandleGetAuditLog(GetAuditLogMessage message) {
return auditLogManager.GetMostRecentItems(userLoginManager.GetLoggedInUser(message.AuthToken), message.Count);
private Task<ImmutableArray<AuditLogItem>> HandleGetAuditLog(GetAuditLogMessage message) {
return auditLogManager.GetMostRecentItems(message.Count);
}
private Task<Result<ImmutableArray<EventLogItem>, UserActionFailure>> HandleGetEventLog(GetEventLogMessage message) {
return eventLogManager.GetMostRecentItems(userLoginManager.GetLoggedInUser(message.AuthToken), message.Count);
private Task<ImmutableArray<EventLogItem>> HandleGetEventLog(GetEventLogMessage message) {
return eventLogManager.GetMostRecentItems(message.Count);
}
private void HandleReply(ReplyMessage message) {

View File

@@ -7,7 +7,6 @@ using Phantom.Controller.Services.Agents;
using Phantom.Controller.Services.Events;
using Phantom.Controller.Services.Instances;
using Phantom.Controller.Services.Users;
using Phantom.Controller.Services.Users.Sessions;
using Phantom.Utils.Actor;
using Phantom.Utils.Logging;
using Phantom.Utils.Rpc.Runtime;

View File

@@ -1,10 +1,7 @@
using System.Collections.Immutable;
using Phantom.Common.Data;
using Phantom.Common.Data.Web.AuditLog;
using Phantom.Common.Data.Web.Users;
using Phantom.Controller.Database;
using Phantom.Controller.Database.Repositories;
using Phantom.Controller.Services.Users.Sessions;
namespace Phantom.Controller.Services.Users;
@@ -15,11 +12,7 @@ sealed class AuditLogManager {
this.dbProvider = dbProvider;
}
public async Task<Result<ImmutableArray<AuditLogItem>, UserActionFailure>> GetMostRecentItems(LoggedInUser loggedInUser, int count) {
if (!loggedInUser.CheckPermission(Permission.ViewAudit)) {
return UserActionFailure.NotAuthorized;
}
public async Task<ImmutableArray<AuditLogItem>> GetMostRecentItems(int count) {
await using var db = dbProvider.Lazy();
return await new AuditLogRepository(db).GetMostRecentItems(count, CancellationToken.None);
}

View File

@@ -36,6 +36,34 @@ sealed class PermissionManager {
}
}
public async Task<PermissionSet> FetchPermissionsForAllUsers(Guid userId) {
await using var ctx = dbProvider.Eager();
var userPermissions = ctx.UserPermissions
.Where(up => up.UserGuid == userId)
.Select(static up => up.PermissionId);
var rolePermissions = ctx.UserRoles
.Where(ur => ur.UserGuid == userId)
.Join(ctx.RolePermissions, static ur => ur.RoleGuid, static rp => rp.RoleGuid, static (ur, rp) => rp.PermissionId);
return new PermissionSet(await userPermissions.Union(rolePermissions).AsAsyncEnumerable().ToImmutableSetAsync());
}
public async Task<PermissionSet> FetchPermissionsForUserId(Guid userId) {
await using var ctx = dbProvider.Eager();
var userPermissions = ctx.UserPermissions
.Where(up => up.UserGuid == userId)
.Select(static up => up.PermissionId);
var rolePermissions = ctx.UserRoles
.Where(ur => ur.UserGuid == userId)
.Join(ctx.RolePermissions, static ur => ur.RoleGuid, static rp => rp.RoleGuid, static (ur, rp) => rp.PermissionId);
return new PermissionSet(await userPermissions.Union(rolePermissions).AsAsyncEnumerable().ToImmutableSetAsync());
}
public static ImmutableArray<string> GetMissingPermissionsOrdered(IEnumerable<Permission> allPermissions, ImmutableHashSet<string> existingPermissionIds) {
return allPermissions.Select(static permission => permission.Id).Except(existingPermissionIds).Order().ToImmutableArray();
}

View File

@@ -1,29 +0,0 @@
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 _);
}
}

View File

@@ -1,20 +0,0 @@
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;
}
}

View File

@@ -1,140 +0,0 @@
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();
}
}
}

View File

@@ -0,0 +1,107 @@
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();
}
}
}

View File

@@ -1,11 +1,8 @@
using System.Collections.Immutable;
using Phantom.Common.Data;
using Phantom.Common.Data.Web.Users;
using Phantom.Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults;
using Phantom.Controller.Database;
using Phantom.Controller.Database.Entities;
using Phantom.Controller.Database.Repositories;
using Phantom.Controller.Services.Users.Sessions;
using Phantom.Utils.Logging;
using Serilog;
@@ -14,13 +11,9 @@ namespace Phantom.Controller.Services.Users;
sealed class UserManager {
private static readonly ILogger Logger = PhantomLogger.Create<UserManager>();
private readonly AuthenticatedUserCache authenticatedUserCache;
private readonly ControllerState controllerState;
private readonly IDbContextProvider dbProvider;
public UserManager(AuthenticatedUserCache authenticatedUserCache, ControllerState controllerState, IDbContextProvider dbProvider) {
this.authenticatedUserCache = authenticatedUserCache;
this.controllerState = controllerState;
public UserManager(IDbContextProvider dbProvider) {
this.dbProvider = dbProvider;
}
@@ -57,12 +50,12 @@ sealed class UserManager {
wasCreated = true;
}
else {
return new CreationFailed(result.Error);
return new Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults.CreationFailed(result.Error);
}
}
else {
if (userRepository.SetUserPassword(user, password).TryGetError(out var error)) {
return new UpdatingFailed(error);
return new Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults.UpdatingFailed(error);
}
auditLogWriter.AdministratorUserModified(user);
@@ -71,7 +64,7 @@ sealed class UserManager {
var role = await new RoleRepository(db).GetByGuid(Role.Administrator.Guid);
if (role == null) {
return new AddingToRoleFailed();
return new Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults.AddingToRoleFailed();
}
await new UserRoleRepository(db).Add(user, role);
@@ -85,21 +78,17 @@ sealed class UserManager {
Logger.Information("Updated administrator user \"{Username}\" (GUID {Guid}).", username, user.UserGuid);
}
return new Success(user.ToUserInfo());
return new Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults.Success(user.ToUserInfo());
} catch (Exception e) {
Logger.Error(e, "Could not create or update administrator user \"{Username}\".", username);
return new UnknownError();
return new Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults.UnknownError();
}
}
public async Task<Result<CreateUserResult, UserActionFailure>> Create(LoggedInUser loggedInUser, string username, string password) {
if (!loggedInUser.CheckPermission(Permission.EditUsers)) {
return UserActionFailure.NotAuthorized;
}
public async Task<CreateUserResult> Create(Guid loggedInUserGuid, string username, string password) {
await using var db = dbProvider.Lazy();
var userRepository = new UserRepository(db);
var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUser.Guid);
var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUserGuid);
try {
var result = await userRepository.CreateUser(username, password);
@@ -120,11 +109,7 @@ sealed class UserManager {
}
}
public async Task<Result<DeleteUserResult, UserActionFailure>> DeleteByGuid(LoggedInUser loggedInUser, Guid userGuid) {
if (!loggedInUser.CheckPermission(Permission.EditUsers)) {
return UserActionFailure.NotAuthorized;
}
public async Task<DeleteUserResult> DeleteByGuid(Guid loggedInUserGuid, Guid userGuid) {
await using var db = dbProvider.Lazy();
var userRepository = new UserRepository(db);
@@ -133,18 +118,12 @@ sealed class UserManager {
return DeleteUserResult.NotFound;
}
authenticatedUserCache.Remove(userGuid);
var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUser.Guid);
var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUserGuid);
try {
userRepository.DeleteUser(user);
auditLogWriter.UserDeleted(user);
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);
return DeleteUserResult.Deleted;
} catch (Exception e) {

View File

@@ -1,9 +1,7 @@
using System.Collections.Immutable;
using Phantom.Common.Data;
using Phantom.Common.Data.Web.Users;
using Phantom.Controller.Database;
using Phantom.Controller.Database.Repositories;
using Phantom.Controller.Services.Users.Sessions;
using Phantom.Utils.Logging;
using Serilog;
@@ -12,13 +10,9 @@ namespace Phantom.Controller.Services.Users;
sealed class UserRoleManager {
private static readonly ILogger Logger = PhantomLogger.Create<UserRoleManager>();
private readonly AuthenticatedUserCache authenticatedUserCache;
private readonly ControllerState controllerState;
private readonly IDbContextProvider dbProvider;
public UserRoleManager(AuthenticatedUserCache authenticatedUserCache, ControllerState controllerState, IDbContextProvider dbProvider) {
this.authenticatedUserCache = authenticatedUserCache;
this.controllerState = controllerState;
public UserRoleManager(IDbContextProvider dbProvider) {
this.dbProvider = dbProvider;
}
@@ -27,11 +21,7 @@ sealed class UserRoleManager {
return await new UserRoleRepository(db).GetRoleGuidsByUserGuid(userGuids);
}
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;
}
public async Task<ChangeUserRolesResult> ChangeUserRoles(Guid loggedInUserGuid, Guid subjectUserGuid, ImmutableHashSet<Guid> addToRoleGuids, ImmutableHashSet<Guid> removeFromRoleGuids) {
await using var db = dbProvider.Lazy();
var userRepository = new UserRepository(db);
@@ -42,7 +32,7 @@ sealed class UserRoleManager {
var roleRepository = new RoleRepository(db);
var userRoleRepository = new UserRoleRepository(db);
var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUser.Guid);
var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUserGuid);
var rolesByGuid = await roleRepository.GetByGuids(addToRoleGuids.Union(removeFromRoleGuids));
@@ -72,9 +62,6 @@ sealed class UserRoleManager {
auditLogWriter.UserRolesChanged(user, addedToRoleNames, removedFromRoleNames);
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);
return new ChangeUserRolesResult(addedToRoleGuids.ToImmutable(), removedFromRoleGuids.ToImmutable());
} catch (Exception e) {

View File

@@ -1,8 +1,8 @@
<Project>
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>13</LangVersion>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>11</LangVersion>
</PropertyGroup>
<PropertyGroup>

View File

@@ -1,7 +1,7 @@
# +---------------+
# | Prepare build |
# +---------------+
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/nightly/sdk:9.0 AS phantom-builder
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/nightly/sdk:8.0 AS phantom-builder
ARG TARGETARCH
ADD . /app
@@ -19,7 +19,7 @@ RUN find .artifacts/publish/*/* -maxdepth 0 -execdir mv '{}' 'release' \;
# +---------------------+
# | Phantom Agent image |
# +---------------------+
FROM mcr.microsoft.com/dotnet/nightly/runtime:9.0 AS phantom-agent
FROM mcr.microsoft.com/dotnet/nightly/runtime:8.0 AS phantom-agent
RUN mkdir /data && chmod 777 /data
WORKDIR /data
@@ -27,7 +27,7 @@ WORKDIR /data
COPY --from=eclipse-temurin:8-jre /opt/java/openjdk /opt/java/8
COPY --from=eclipse-temurin:16-jdk /opt/java/openjdk /opt/java/16
COPY --from=eclipse-temurin:17-jre /opt/java/openjdk /opt/java/17
COPY --from=eclipse-temurin:21-jre /opt/java/openjdk /opt/java/21
COPY --from=eclipse-temurin:20-jre /opt/java/openjdk /opt/java/20
ARG DEBIAN_FRONTEND=noninteractive
@@ -46,7 +46,7 @@ ENTRYPOINT ["dotnet", "/app/Phantom.Agent.dll"]
# +--------------------------+
# | Phantom Controller image |
# +--------------------------+
FROM mcr.microsoft.com/dotnet/nightly/runtime:9.0 AS phantom-controller
FROM mcr.microsoft.com/dotnet/nightly/runtime:8.0 AS phantom-controller
RUN mkdir /data && chmod 777 /data
WORKDIR /data
@@ -59,7 +59,7 @@ ENTRYPOINT ["dotnet", "/app/Phantom.Controller.dll"]
# +-------------------+
# | Phantom Web image |
# +-------------------+
FROM mcr.microsoft.com/dotnet/nightly/aspnet:9.0 AS phantom-web
FROM mcr.microsoft.com/dotnet/nightly/aspnet:8.0 AS phantom-web
RUN mkdir /data && chmod 777 /data
WORKDIR /data

View File

@@ -17,10 +17,6 @@ public static class TaskExtensions {
return task.ContinueOnActor(result => mapper(result, arg));
}
public static Task<TResult> ContinueOnActor<TSource, TArg1, TArg2, TResult>(this Task<TSource> task, Func<TSource, TArg1, TArg2, TResult> mapper, TArg1 arg1, TArg2 arg2) {
return task.ContinueOnActor(result => mapper(result, arg1, arg2));
}
private static Task<TResult> MapResult<TSource, TResult>(Task<TSource> task, Func<TSource, TResult> mapper, TaskCompletionSource<TResult> completionSource) {
if (task.IsFaulted) {
completionSource.SetException(task.Exception.InnerExceptions);

View File

@@ -1,3 +0,0 @@
namespace Phantom.Utils.Result;
public sealed record Err<T>(T Error) : Result;

View File

@@ -1,3 +0,0 @@
namespace Phantom.Utils.Result;
public sealed record Ok<T>(T Value) : Result;

View File

@@ -1,5 +0,0 @@
namespace Phantom.Utils.Result;
public abstract record Result {
private protected Result() {}
}

View File

@@ -1,6 +1,6 @@
@using Phantom.Web.Components.Utils
@if (messageLines.Length > 0) {
<div class="form-submit-errors text-danger" role="alert">
<div class="form-submit-errors text-danger">
@for (int i = 0; i < messageLines.Length; i++) {
@messageLines[i]
if (i < messageLines.Length - 1) {

View File

@@ -17,9 +17,9 @@ public abstract class PhantomComponent : ComponentBase, IDisposable {
protected CancellationToken CancellationToken => cancellationTokenSource.Token;
protected async Task<AuthenticatedUser?> GetAuthenticatedUser() {
protected async Task<Guid?> GetUserGuid() {
var authenticationState = await AuthenticationStateTask;
return authenticationState.GetAuthenticatedUser();
return authenticationState.TryGetGuid();
}
protected async Task<bool> CheckPermission(Permission permission) {

View File

@@ -2,7 +2,6 @@
using Phantom.Common.Data.Web.Agent;
using Phantom.Utils.Events;
using Phantom.Utils.Logging;
using Phantom.Web.Services.Authentication;
namespace Phantom.Web.Services.Agents;
@@ -19,13 +18,7 @@ public sealed class AgentManager {
return agents.Value;
}
public ImmutableDictionary<Guid, Agent> ToDictionaryByGuid(AuthenticatedUser? authenticatedUser) {
if (authenticatedUser == null) {
return ImmutableDictionary<Guid, Agent>.Empty;
}
return agents.Value
.Where(agent => authenticatedUser.Info.HasAccessToAgent(agent.AgentGuid))
.ToImmutableDictionary(static agent => agent.AgentGuid);
public ImmutableDictionary<Guid, Agent> ToDictionaryByGuid() {
return agents.Value.ToImmutableDictionary(static agent => agent.AgentGuid);
}
}

View File

@@ -1,6 +0,0 @@
using System.Collections.Immutable;
using Phantom.Common.Data.Web.Users;
namespace Phantom.Web.Services.Authentication;
public sealed record AuthenticatedUser(AuthenticatedUserInfo Info, ImmutableArray<byte> Token);

View File

@@ -5,27 +5,23 @@ using Phantom.Common.Data.Web.Users;
namespace Phantom.Web.Services.Authentication;
public static class AuthenticationStateExtensions {
public static AuthenticatedUser? GetAuthenticatedUser(this AuthenticationState authenticationState) {
return authenticationState.User.GetAuthenticatedUser();
public static Guid? TryGetGuid(this AuthenticationState authenticationState) {
return authenticationState.User is CustomClaimsPrincipal customUser ? customUser.UserInfo.Guid : null;
}
public static AuthenticatedUser? GetAuthenticatedUser(this ClaimsPrincipal claimsPrincipal) {
return claimsPrincipal is CustomClaimsPrincipal principal ? principal.User : null;
public static PermissionSet GetPermissions(this ClaimsPrincipal user) {
return user is CustomClaimsPrincipal customUser ? customUser.UserInfo.Permissions : PermissionSet.None;
}
public static bool CheckPermission(this ClaimsPrincipal user, Permission permission) {
return user.GetPermissions().Check(permission);
}
public static PermissionSet GetPermissions(this AuthenticationState authenticationState) {
return authenticationState.User.GetPermissions();
}
public static PermissionSet GetPermissions(this ClaimsPrincipal claimsPrincipal) {
return claimsPrincipal.GetAuthenticatedUser() is {} user ? user.Info.Permissions : PermissionSet.None;
}
public static bool CheckPermission(this AuthenticationState authenticationState, Permission permission) {
return authenticationState.User.CheckPermission(permission);
}
public static bool CheckPermission(this ClaimsPrincipal claimsPrincipal, Permission permission) {
return claimsPrincipal.GetPermissions().Check(permission);
}
}

View File

@@ -4,118 +4,41 @@ using Microsoft.AspNetCore.Components.Server;
using Phantom.Common.Data;
using Phantom.Common.Data.Web.Users;
using Phantom.Common.Messages.Web.ToController;
using Phantom.Utils.Logging;
using Phantom.Web.Services.Rpc;
using ILogger = Serilog.ILogger;
namespace Phantom.Web.Services.Authentication;
public sealed class CustomAuthenticationStateProvider : ServerAuthenticationStateProvider, IAsyncDisposable {
private static readonly ILogger Logger = PhantomLogger.Create<CustomAuthenticationStateProvider>();
private readonly UserSessionRefreshManager sessionRefreshManager;
public sealed class CustomAuthenticationStateProvider : ServerAuthenticationStateProvider {
private readonly UserSessionBrowserStorage sessionBrowserStorage;
private readonly ControllerConnection controllerConnection;
private bool isLoaded;
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;
public CustomAuthenticationStateProvider(UserSessionBrowserStorage sessionBrowserStorage, ControllerConnection controllerConnection) {
this.sessionBrowserStorage = sessionBrowserStorage;
this.controllerConnection = controllerConnection;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync() {
if (!isLoaded) {
await LoadSession();
var stored = await sessionBrowserStorage.Get();
if (stored != null) {
var session = await controllerConnection.Send<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(new GetAuthenticatedUser(stored.UserGuid, stored.Token), TimeSpan.FromSeconds(30));
if (session.Value is {} userInfo) {
SetLoadedSession(userInfo);
}
}
}
return await base.GetAuthenticationStateAsync();
}
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))));
internal void SetLoadedSession(AuthenticatedUserInfo user) {
isLoaded = true;
SetAuthenticationState(Task.FromResult(new AuthenticationState(new CustomClaimsPrincipal(user))));
}
internal void SetUnloadedSession() {
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();
SetAuthenticationState(Task.FromResult(new AuthenticationState(new ClaimsPrincipal())));
}
}

View File

@@ -4,10 +4,10 @@ using Phantom.Common.Data.Web.Users;
namespace Phantom.Web.Services.Authentication;
sealed class CustomClaimsPrincipal : ClaimsPrincipal {
internal AuthenticatedUser User { get; }
internal AuthenticatedUserInfo UserInfo { get; }
internal CustomClaimsPrincipal(AuthenticatedUser user) : base(GetIdentity(user.Info)) {
User = user;
internal CustomClaimsPrincipal(AuthenticatedUserInfo userInfo) : base(GetIdentity(userInfo)) {
UserInfo = userInfo;
}
private static ClaimsIdentity GetIdentity(AuthenticatedUserInfo userInfo) {

View File

@@ -1,5 +1,4 @@
using Phantom.Common.Data;
using Phantom.Common.Data.Web.Users;
using Phantom.Common.Data.Web.Users;
using Phantom.Common.Messages.Web.ToController;
using Phantom.Utils.Logging;
using Phantom.Web.Services.Rpc;
@@ -23,23 +22,24 @@ public sealed class UserLoginManager {
}
public async Task<bool> LogIn(string username, string password, string? returnUrl = null) {
Optional<LogInSuccess> result;
LogInSuccess? success;
try {
result = await controllerConnection.Send<LogInMessage, Optional<LogInSuccess>>(new LogInMessage(username, password), TimeSpan.FromSeconds(30));
success = await controllerConnection.Send<LogInMessage, LogInSuccess?>(new LogInMessage(username, password), TimeSpan.FromSeconds(30));
} catch (Exception e) {
Logger.Error(e, "Could not log in {Username}.", username);
return false;
}
if (result.Value is not var (userInfo, authToken)) {
if (success == null) {
return false;
}
Logger.Information("Successfully logged in {Username}.", username);
authenticationStateProvider.SetUnloadedSession();
await sessionBrowserStorage.Store(userInfo.Guid, authToken);
await authenticationStateProvider.GetAuthenticationStateAsync();
var userInfo = success.UserInfo;
await sessionBrowserStorage.Store(userInfo.Guid, success.Token);
authenticationStateProvider.SetLoadedSession(userInfo);
await navigation.NavigateTo(returnUrl ?? string.Empty);
return true;

View File

@@ -1,25 +0,0 @@
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);
}
}
}

View File

@@ -1,9 +1,6 @@
using System.Collections.Immutable;
using Phantom.Common.Data;
using Phantom.Common.Data.Web.EventLog;
using Phantom.Common.Data.Web.Users;
using Phantom.Common.Messages.Web.ToController;
using Phantom.Web.Services.Authentication;
using Phantom.Web.Services.Rpc;
namespace Phantom.Web.Services.Events;
@@ -15,13 +12,8 @@ public sealed class EventLogManager {
this.controllerConnection = controllerConnection;
}
public async Task<Result<ImmutableArray<EventLogItem>, UserActionFailure>> GetMostRecentItems(AuthenticatedUser? authenticatedUser, int count, CancellationToken cancellationToken) {
if (authenticatedUser != null && authenticatedUser.Info.CheckPermission(Permission.ViewEvents)) {
var message = new GetEventLogMessage(authenticatedUser.Token, count);
return await controllerConnection.Send<GetEventLogMessage, Result<ImmutableArray<EventLogItem>, UserActionFailure>>(message, cancellationToken);
}
else {
return UserActionFailure.NotAuthorized;
}
public Task<ImmutableArray<EventLogItem>> GetMostRecentItems(int count, CancellationToken cancellationToken) {
var message = new GetEventLogMessage(count);
return controllerConnection.Send<GetEventLogMessage, ImmutableArray<EventLogItem>>(message, cancellationToken);
}
}

View File

@@ -4,11 +4,9 @@ using Phantom.Common.Data.Instance;
using Phantom.Common.Data.Minecraft;
using Phantom.Common.Data.Replies;
using Phantom.Common.Data.Web.Instance;
using Phantom.Common.Data.Web.Users;
using Phantom.Common.Messages.Web.ToController;
using Phantom.Utils.Events;
using Phantom.Utils.Logging;
using Phantom.Web.Services.Authentication;
using Phantom.Web.Services.Rpc;
namespace Phantom.Web.Services.Instances;
@@ -33,52 +31,27 @@ public sealed class InstanceManager {
return instances.Value;
}
public Instance? GetByGuid(AuthenticatedUser? authenticatedUser, Guid instanceGuid) {
if (authenticatedUser == null) {
return null;
public Instance? GetByGuid(Guid instanceGuid) {
return instances.Value.GetValueOrDefault(instanceGuid);
}
var instance = instances.Value.GetValueOrDefault(instanceGuid);
return instance != null && authenticatedUser.Info.HasAccessToAgent(instance.Configuration.AgentGuid) ? instance : null;
public Task<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>> CreateOrUpdateInstance(Guid loggedInUserGuid, Guid instanceGuid, InstanceConfiguration configuration, CancellationToken cancellationToken) {
var message = new CreateOrUpdateInstanceMessage(loggedInUserGuid, instanceGuid, configuration);
return controllerConnection.Send<CreateOrUpdateInstanceMessage, Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(message, cancellationToken);
}
public async Task<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>> CreateOrUpdateInstance(AuthenticatedUser? authenticatedUser, Guid instanceGuid, InstanceConfiguration configuration, CancellationToken cancellationToken) {
if (authenticatedUser != null && authenticatedUser.Info.CheckPermission(Permission.CreateInstances)) {
var message = new CreateOrUpdateInstanceMessage(authenticatedUser.Token, instanceGuid, configuration);
return await controllerConnection.Send<CreateOrUpdateInstanceMessage, Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>(message, cancellationToken);
}
else {
return (UserInstanceActionFailure) UserActionFailure.NotAuthorized;
}
public Task<Result<LaunchInstanceResult, InstanceActionFailure>> LaunchInstance(Guid loggedInUserGuid, Guid agentGuid, Guid instanceGuid, CancellationToken cancellationToken) {
var message = new LaunchInstanceMessage(loggedInUserGuid, agentGuid, instanceGuid);
return controllerConnection.Send<LaunchInstanceMessage, Result<LaunchInstanceResult, InstanceActionFailure>>(message, cancellationToken);
}
public async Task<Result<LaunchInstanceResult, UserInstanceActionFailure>> LaunchInstance(AuthenticatedUser? authenticatedUser, Guid agentGuid, Guid instanceGuid, CancellationToken cancellationToken) {
if (authenticatedUser != null && authenticatedUser.Info.CheckPermission(Permission.ControlInstances)) {
var message = new LaunchInstanceMessage(authenticatedUser.Token, agentGuid, instanceGuid);
return await controllerConnection.Send<LaunchInstanceMessage, Result<LaunchInstanceResult, UserInstanceActionFailure>>(message, cancellationToken);
}
else {
return (UserInstanceActionFailure) UserActionFailure.NotAuthorized;
}
public Task<Result<StopInstanceResult, InstanceActionFailure>> StopInstance(Guid loggedInUserGuid, Guid agentGuid, Guid instanceGuid, MinecraftStopStrategy stopStrategy, CancellationToken cancellationToken) {
var message = new StopInstanceMessage(loggedInUserGuid, agentGuid, instanceGuid, stopStrategy);
return controllerConnection.Send<StopInstanceMessage, Result<StopInstanceResult, InstanceActionFailure>>(message, cancellationToken);
}
public async Task<Result<StopInstanceResult, UserInstanceActionFailure>> StopInstance(AuthenticatedUser? authenticatedUser, Guid agentGuid, Guid instanceGuid, MinecraftStopStrategy stopStrategy, CancellationToken cancellationToken) {
if (authenticatedUser != null && authenticatedUser.Info.CheckPermission(Permission.ControlInstances)) {
var message = new StopInstanceMessage(authenticatedUser.Token, agentGuid, instanceGuid, stopStrategy);
return await controllerConnection.Send<StopInstanceMessage, Result<StopInstanceResult, UserInstanceActionFailure>>(message, cancellationToken);
}
else {
return (UserInstanceActionFailure) UserActionFailure.NotAuthorized;
}
}
public 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;
}
public Task<Result<SendCommandToInstanceResult, InstanceActionFailure>> SendCommandToInstance(Guid loggedInUserGuid, Guid agentGuid, Guid instanceGuid, string command, CancellationToken cancellationToken) {
var message = new SendCommandToInstanceMessage(loggedInUserGuid, agentGuid, instanceGuid, command);
return controllerConnection.Send<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, InstanceActionFailure>>(message, cancellationToken);
}
}

View File

@@ -23,7 +23,6 @@ public static class PhantomWebServices {
services.AddSingleton<UserManager>();
services.AddSingleton<AuditLogManager>();
services.AddSingleton<UserSessionRefreshManager>();
services.AddScoped<UserLoginManager>();
services.AddScoped<UserSessionBrowserStorage>();

View File

@@ -4,20 +4,12 @@ using Phantom.Common.Messages.Web.ToWeb;
using Phantom.Utils.Actor;
using Phantom.Utils.Rpc.Runtime;
using Phantom.Web.Services.Agents;
using Phantom.Web.Services.Authentication;
using Phantom.Web.Services.Instances;
namespace Phantom.Web.Services.Rpc;
sealed class ControllerMessageHandlerActor : ReceiveActor<IMessageToWeb> {
public readonly record struct Init(
RpcConnectionToServer<IMessageToController> Connection,
AgentManager AgentManager,
InstanceManager InstanceManager,
InstanceLogManager InstanceLogManager,
UserSessionRefreshManager UserSessionRefreshManager,
TaskCompletionSource<bool> RegisterSuccessWaiter
);
public readonly record struct Init(RpcConnectionToServer<IMessageToController> Connection, AgentManager AgentManager, InstanceManager InstanceManager, InstanceLogManager InstanceLogManager, TaskCompletionSource<bool> RegisterSuccessWaiter);
public static Props<IMessageToWeb> Factory(Init init) {
return Props<IMessageToWeb>.Create(() => new ControllerMessageHandlerActor(init), new ActorConfiguration { SupervisorStrategy = SupervisorStrategies.Resume });
@@ -27,7 +19,6 @@ sealed class ControllerMessageHandlerActor : ReceiveActor<IMessageToWeb> {
private readonly AgentManager agentManager;
private readonly InstanceManager instanceManager;
private readonly InstanceLogManager instanceLogManager;
private readonly UserSessionRefreshManager userSessionRefreshManager;
private readonly TaskCompletionSource<bool> registerSuccessWaiter;
private ControllerMessageHandlerActor(Init init) {
@@ -35,14 +26,12 @@ sealed class ControllerMessageHandlerActor : ReceiveActor<IMessageToWeb> {
this.agentManager = init.AgentManager;
this.instanceManager = init.InstanceManager;
this.instanceLogManager = init.InstanceLogManager;
this.userSessionRefreshManager = init.UserSessionRefreshManager;
this.registerSuccessWaiter = init.RegisterSuccessWaiter;
Receive<RegisterWebResultMessage>(HandleRegisterWebResult);
Receive<RefreshAgentsMessage>(HandleRefreshAgents);
Receive<RefreshInstancesMessage>(HandleRefreshInstances);
Receive<InstanceOutputMessage>(HandleInstanceOutput);
Receive<RefreshUserSessionMessage>(HandleRefreshUserSession);
Receive<ReplyMessage>(HandleReply);
}
@@ -62,10 +51,6 @@ sealed class ControllerMessageHandlerActor : ReceiveActor<IMessageToWeb> {
instanceLogManager.AddLines(message.InstanceGuid, message.Lines);
}
private void HandleRefreshUserSession(RefreshUserSessionMessage message) {
userSessionRefreshManager.RefreshUser(message.UserGuid);
}
private void HandleReply(ReplyMessage message) {
connection.Receive(message);
}

View File

@@ -4,7 +4,6 @@ using Phantom.Utils.Actor;
using Phantom.Utils.Rpc.Runtime;
using Phantom.Utils.Tasks;
using Phantom.Web.Services.Agents;
using Phantom.Web.Services.Authentication;
using Phantom.Web.Services.Instances;
namespace Phantom.Web.Services.Rpc;
@@ -14,7 +13,6 @@ public sealed class ControllerMessageHandlerFactory {
private readonly AgentManager agentManager;
private readonly InstanceManager instanceManager;
private readonly InstanceLogManager instanceLogManager;
private readonly UserSessionRefreshManager userSessionRefreshManager;
private readonly TaskCompletionSource<bool> registerSuccessWaiter = AsyncTasks.CreateCompletionSource<bool>();
@@ -22,17 +20,15 @@ public sealed class ControllerMessageHandlerFactory {
private int messageHandlerId = 0;
public ControllerMessageHandlerFactory(RpcConnectionToServer<IMessageToController> connection, AgentManager agentManager, InstanceManager instanceManager, InstanceLogManager instanceLogManager, UserSessionRefreshManager userSessionRefreshManager) {
public ControllerMessageHandlerFactory(RpcConnectionToServer<IMessageToController> connection, AgentManager agentManager, InstanceManager instanceManager, InstanceLogManager instanceLogManager) {
this.connection = connection;
this.agentManager = agentManager;
this.instanceManager = instanceManager;
this.instanceLogManager = instanceLogManager;
this.userSessionRefreshManager = userSessionRefreshManager;
}
public ActorRef<IMessageToWeb> Create(IActorRefFactory actorSystem) {
var init = new ControllerMessageHandlerActor.Init(connection, agentManager, instanceManager, instanceLogManager, userSessionRefreshManager, registerSuccessWaiter);
var name = "ControllerMessageHandler-" + Interlocked.Increment(ref messageHandlerId);
return actorSystem.ActorOf(ControllerMessageHandlerActor.Factory(init), name);
int id = Interlocked.Increment(ref messageHandlerId);
return actorSystem.ActorOf(ControllerMessageHandlerActor.Factory(new ControllerMessageHandlerActor.Init(connection, agentManager, instanceManager, instanceLogManager, registerSuccessWaiter)), "ControllerMessageHandler-" + id);
}
}

View File

@@ -1,9 +1,6 @@
using System.Collections.Immutable;
using Phantom.Common.Data;
using Phantom.Common.Data.Web.AuditLog;
using Phantom.Common.Data.Web.Users;
using Phantom.Common.Messages.Web.ToController;
using Phantom.Web.Services.Authentication;
using Phantom.Web.Services.Rpc;
namespace Phantom.Web.Services.Users;
@@ -15,13 +12,8 @@ public sealed class AuditLogManager {
this.controllerConnection = controllerConnection;
}
public async Task<Result<ImmutableArray<AuditLogItem>, UserActionFailure>> GetMostRecentItems(AuthenticatedUser? authenticatedUser, int count, CancellationToken cancellationToken) {
if (authenticatedUser != null && authenticatedUser.Info.CheckPermission(Permission.ViewAudit)) {
var message = new GetAuditLogMessage(authenticatedUser.Token, count);
return await controllerConnection.Send<GetAuditLogMessage, Result<ImmutableArray<AuditLogItem>, UserActionFailure>>(message, cancellationToken);
}
else {
return UserActionFailure.NotAuthorized;
}
public Task<ImmutableArray<AuditLogItem>> GetMostRecentItems(int count, CancellationToken cancellationToken) {
var message = new GetAuditLogMessage(count);
return controllerConnection.Send<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(message, cancellationToken);
}
}

View File

@@ -1,8 +1,6 @@
using System.Collections.Immutable;
using Phantom.Common.Data;
using Phantom.Common.Data.Web.Users;
using Phantom.Common.Messages.Web.ToController;
using Phantom.Web.Services.Authentication;
using Phantom.Web.Services.Rpc;
namespace Phantom.Web.Services.Users;
@@ -18,21 +16,11 @@ public sealed class UserManager {
return controllerConnection.Send<GetUsersMessage, ImmutableArray<UserInfo>>(new GetUsersMessage(), cancellationToken);
}
public async Task<Result<CreateUserResult, UserActionFailure>> Create(AuthenticatedUser? authenticatedUser, string username, string password, CancellationToken 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<CreateUserResult> Create(Guid loggedInUserGuid, string username, string password, CancellationToken cancellationToken) {
return controllerConnection.Send<CreateUserMessage, CreateUserResult>(new CreateUserMessage(loggedInUserGuid, username, password), cancellationToken);
}
public async Task<Result<DeleteUserResult, UserActionFailure>> DeleteByGuid(AuthenticatedUser? authenticatedUser, Guid userGuid, CancellationToken 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;
}
public Task<DeleteUserResult> DeleteByGuid(Guid loggedInUserGuid, Guid userGuid, CancellationToken cancellationToken) {
return controllerConnection.Send<DeleteUserMessage, DeleteUserResult>(new DeleteUserMessage(loggedInUserGuid, userGuid), cancellationToken);
}
}

View File

@@ -1,8 +1,6 @@
using System.Collections.Immutable;
using Phantom.Common.Data;
using Phantom.Common.Data.Web.Users;
using Phantom.Common.Messages.Web.ToController;
using Phantom.Web.Services.Authentication;
using Phantom.Web.Services.Rpc;
namespace Phantom.Web.Services.Users;
@@ -22,12 +20,7 @@ public sealed class UserRoleManager {
return (await GetUserRoles(ImmutableHashSet.Create(userGuid), cancellationToken)).GetValueOrDefault(userGuid, ImmutableArray<Guid>.Empty);
}
public async Task<Result<ChangeUserRolesResult, UserActionFailure>> ChangeUserRoles(AuthenticatedUser? authenticatedUser, Guid subjectUserGuid, ImmutableHashSet<Guid> addToRoleGuids, ImmutableHashSet<Guid> removeFromRoleGuids, CancellationToken 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;
}
public Task<ChangeUserRolesResult> ChangeUserRoles(Guid loggedInUserGuid, Guid subjectUserGuid, ImmutableHashSet<Guid> addToRoleGuids, ImmutableHashSet<Guid> removeFromRoleGuids, CancellationToken cancellationToken) {
return controllerConnection.Send<ChangeUserRolesMessage, ChangeUserRolesResult>(new ChangeUserRolesMessage(loggedInUserGuid, subjectUserGuid, addToRoleGuids, removeFromRoleGuids), cancellationToken);
}
}

View File

@@ -11,7 +11,7 @@
<p role="alert">You do not have permission to visit this page.</p>
}
else {
_ = Navigation.NavigateTo("login" + QueryString.Create("return", Navigation.CreateReturnUrl()), forceLoad: true);
Navigation.NavigateTo("login" + QueryString.Create("return", Navigation.CreateReturnUrl()), forceLoad: true);
}
</NotAuthorized>
</AuthorizeRouteView>

View File

@@ -1,6 +1,6 @@
@using Phantom.Common.Data.Web.Users
@using Phantom.Web.Services.Authentication
@using Phantom.Web.Services
@using Phantom.Web.Services.Authentication
@using Phantom.Common.Data.Web.Users
@inject ApplicationProperties ApplicationProperties
<div class="navbar navbar-dark">
@@ -17,6 +17,7 @@
<div class="navbar-menu @NavMenuCssClass" @onclick="ToggleNavMenu">
<nav>
<NavMenuItem Label="Home" Icon="home" Match="NavLinkMatch.All" />
<AuthorizeView>
<NotAuthorized>
<NavMenuItem Label="Login" Icon="account-login" Href="login" />
@@ -24,8 +25,6 @@
<Authorized>
@{ var permissions = context.GetPermissions(); }
<NavMenuItem Label="Home" Icon="home" Match="NavLinkMatch.All" />
@if (permissions.Check(Permission.ViewInstances)) {
<NavMenuItem Label="Instances" Icon="folder" Href="instances" />
}

View File

@@ -1,5 +1,4 @@
@page "/agents"
@using System.Collections.Immutable
@using Phantom.Common.Data.Web.Agent
@using Phantom.Utils.Collections
@using Phantom.Web.Services.Agents
@@ -47,45 +46,33 @@
<Cell class="fw-semibold text-center text-success">Online</Cell>
<Cell class="text-end">-</Cell>
break;
case AgentIsOffline:
<Cell class="fw-semibold text-center">Offline</Cell>
<Cell class="text-end">N/A</Cell>
break;
case AgentIsDisconnected status:
<Cell class="fw-semibold text-center">Offline</Cell>
<Cell class="text-end">
<TimeWithOffset Time="status.LastPingTime" />
</Cell>
break;
default:
<Cell class="fw-semibold text-center">N/A</Cell>
break;
}
</ItemRow>
<NoItemsRow>
No agents found.
No agents registered.
</NoItemsRow>
</Table>
@code {
private TableData<Agent, Guid>? agentTable;
protected override async Task OnInitializedAsync() {
var authenticatedUser = await GetAuthenticatedUser();
if (authenticatedUser == null) {
return;
}
private readonly TableData<Agent, Guid> agentTable = new();
protected override void OnInitialized() {
AgentManager.AgentsChanged.Subscribe(this, agents => {
var sortedAgents = agents.Where(agent => authenticatedUser.Info.HasAccessToAgent(agent.AgentGuid))
.OrderBy(static agent => agent.Configuration.AgentName)
.ToImmutableArray();
agentTable ??= new TableData<Agent, Guid>();
var sortedAgents = agents.Sort(static (a1, a2) => a1.Configuration.AgentName.CompareTo(a2.Configuration.AgentName));
agentTable.UpdateFrom(sortedAgents, static agent => agent.AgentGuid, static agent => agent, static (agent, _) => agent);
InvokeAsync(StateHasChanged);
});

View File

@@ -3,20 +3,15 @@
@using System.Collections.Immutable
@using Phantom.Common.Data.Web.AuditLog
@using Phantom.Common.Data.Web.Users
@using Phantom.Web.Services.Instances
@using Phantom.Web.Services.Users
@inherits PhantomComponent
@using Phantom.Web.Services.Instances
@inherits Phantom.Web.Components.PhantomComponent
@inject AuditLogManager AuditLogManager
@inject InstanceManager InstanceManager
@inject UserManager UserManager
<h1>Audit Log</h1>
@if (loadError is {} error) {
<p role="alert">@error</p>
return;
}
<Table TItem="AuditLogItem" Items="logItems">
<HeaderRow>
<Column Class="text-end" MinWidth="200px">Time</Column>
@@ -51,25 +46,21 @@
@code {
private CancellationTokenSource? initializationCancellationTokenSource;
private ImmutableArray<AuditLogItem>? logItems;
private string? loadError;
private ImmutableDictionary<Guid, string>? userNamesByGuid;
private ImmutableDictionary<Guid, string> instanceNamesByGuid = ImmutableDictionary<Guid, string>.Empty;
protected override async Task OnInitializedAsync() {
var result = await AuditLogManager.GetMostRecentItems(await GetAuthenticatedUser(), 50, CancellationToken);
if (result) {
logItems = result.Value;
userNamesByGuid = (await UserManager.GetAll(CancellationToken)).ToImmutableDictionary(static user => user.Guid, static user => user.Name);
initializationCancellationTokenSource = new CancellationTokenSource();
var cancellationToken = initializationCancellationTokenSource.Token;
try {
logItems = await AuditLogManager.GetMostRecentItems(50, cancellationToken);
userNamesByGuid = (await UserManager.GetAll(cancellationToken)).ToImmutableDictionary(static user => user.Guid, static user => user.Name);
instanceNamesByGuid = InstanceManager.GetAll().Values.ToImmutableDictionary(static instance => instance.InstanceGuid, static instance => instance.Configuration.InstanceName);
}
else {
logItems = ImmutableArray<AuditLogItem>.Empty;
loadError = result.Error switch {
UserActionFailure.NotAuthorized => "You do not have permission to view the audit log.",
_ => "Unknown error."
};
} finally {
initializationCancellationTokenSource.Dispose();
}
}
@@ -81,4 +72,10 @@
};
}
protected override void OnDisposed() {
try {
initializationCancellationTokenSource?.Cancel();
} catch (ObjectDisposedException) {}
}
}

View File

@@ -6,18 +6,13 @@
@using Phantom.Web.Services.Agents
@using Phantom.Web.Services.Events
@using Phantom.Web.Services.Instances
@inherits PhantomComponent
@inherits Phantom.Web.Components.PhantomComponent
@inject AgentManager AgentManager
@inject EventLogManager EventLogManager
@inject InstanceManager InstanceManager
<h1>Event Log</h1>
@if (loadError is {} error) {
<p role="alert">@error</p>
return;
}
<Table TItem="EventLogItem" Items="logItems">
<HeaderRow>
<Column Class="text-end" MinWidth="200px">Time</Column>
@@ -55,25 +50,21 @@
@code {
private CancellationTokenSource? initializationCancellationTokenSource;
private ImmutableArray<EventLogItem>? logItems;
private string? loadError;
private ImmutableDictionary<Guid, string> agentNamesByGuid = ImmutableDictionary<Guid, string>.Empty;
private ImmutableDictionary<Guid, string> instanceNamesByGuid = ImmutableDictionary<Guid, string>.Empty;
protected override async Task OnInitializedAsync() {
var result = await EventLogManager.GetMostRecentItems(await GetAuthenticatedUser(), 50, CancellationToken);
if (result) {
logItems = result.Value;
initializationCancellationTokenSource = new CancellationTokenSource();
var cancellationToken = initializationCancellationTokenSource.Token;
try {
logItems = await EventLogManager.GetMostRecentItems(50, cancellationToken);
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);
}
else {
logItems = ImmutableArray<EventLogItem>.Empty;
loadError = result.Error switch {
UserActionFailure.NotAuthorized => "You do not have permission to view the event log.",
_ => "Unknown error."
};
} finally {
initializationCancellationTokenSource.Dispose();
}
}
@@ -88,4 +79,10 @@
};
}
protected override void OnDisposed() {
try {
initializationCancellationTokenSource?.Cancel();
} catch (ObjectDisposedException) {}
}
}

View File

@@ -1,18 +1,12 @@
@page "/"
@inherits PhantomComponent
@attribute [AllowAnonymous]
<h1>Home</h1>
<h1>Hello, world!</h1>
@if (username != null) {
<p>Welcome back, @username!</p>
}
Welcome to your new app.
@code {
private string? username = null;
protected override async Task OnInitializedAsync() {
username = (await GetAuthenticatedUser())?.Info.Name;
}
}
<AuthorizeView>
<Authorized>
You are logged in as @context.User.Identity!.Name.
</Authorized>
</AuthorizeView>

View File

@@ -1,27 +1,19 @@
@page "/instances/{InstanceGuid:guid}"
@attribute [Authorize(Permission.ViewInstancesPolicy)]
@using Phantom.Common.Data.Instance
@using Phantom.Common.Data.Replies
@using Phantom.Common.Data.Web.Instance
@using Phantom.Common.Data.Web.Users
@using Phantom.Utils.Result
@using Phantom.Common.Data.Instance
@using Phantom.Web.Services.Instances
@using Phantom.Web.Services.Authorization
@inherits PhantomComponent
@inherits Phantom.Web.Components.PhantomComponent
@inject InstanceManager InstanceManager
@if (isLoading) {
<h1>Instance</h1>
<p>Loading...</p>
return;
}
@if (Instance == null) {
<h1>Instance Not Found</h1>
<p>Return to <a href="instances">all instances</a>.</p>
return;
}
else {
<div class="d-flex flex-row align-items-center gap-3 mb-3">
<h1 class="mb-0">Instance: @Instance.Configuration.InstanceName</h1>
<span class="fs-4 text-muted">//</span>
@@ -40,7 +32,7 @@
</PermissionView>
</div>
@if (lastError != null) {
<p class="text-danger mt-2" role="alert">@lastError</p>
<p class="text-danger mt-2">@lastError</p>
}
<PermissionView Permission="Permission.ViewInstanceLogs">
@@ -48,29 +40,25 @@
</PermissionView>
<PermissionView Permission="Permission.ControlInstances">
<div class="my-3">
<div class="mb-3">
<InstanceCommandInput AgentGuid="Instance.Configuration.AgentGuid" InstanceGuid="InstanceGuid" Disabled="@(!Instance.Status.CanSendCommand())" />
</div>
<InstanceStopDialog AgentGuid="Instance.Configuration.AgentGuid" InstanceGuid="InstanceGuid" ModalId="stop-instance" Disabled="@(!Instance.Status.CanStop())" />
</PermissionView>
}
@code {
[Parameter]
public Guid InstanceGuid { get; init; }
private Instance? Instance { get; set; }
private bool isLoading = true;
private string? lastError = null;
private bool isLaunchingInstance = false;
protected override async Task OnInitializedAsync() {
Instance = InstanceManager.GetByGuid(await GetAuthenticatedUser(), InstanceGuid);
isLoading = false;
private Instance? Instance { get; set; }
if (Instance != null) {
protected override void OnInitialized() {
InstanceManager.InstancesChanged.Subscribe(this, instances => {
var newInstance = instances.TryGetValue(InstanceGuid, out var instance) ? instance : null;
if (newInstance != Instance) {
@@ -79,39 +67,26 @@
}
});
}
}
private async Task LaunchInstance() {
isLaunchingInstance = true;
lastError = null;
try {
var loggedInUserGuid = await GetUserGuid();
if (loggedInUserGuid == null || !await CheckPermission(Permission.ControlInstances)) {
lastError = "You do not have permission to launch instances.";
return;
}
if (Instance == null) {
lastError = "Instance not found.";
return;
}
var result = await InstanceManager.LaunchInstance(await GetAuthenticatedUser(), Instance.Configuration.AgentGuid, InstanceGuid, CancellationToken);
switch (result.Variant()) {
case Ok<LaunchInstanceResult>(LaunchInstanceResult.LaunchInitiated):
break;
case Ok<LaunchInstanceResult>(var launchInstanceResult):
lastError = launchInstanceResult.ToSentence();
break;
case Err<UserInstanceActionFailure>(OfInstanceActionFailure(var failure)):
lastError = failure.ToSentence();
break;
case Err<UserInstanceActionFailure>(OfUserActionFailure(UserActionFailure.NotAuthorized)):
lastError = "You do not have permission to launch this instance.";
break;
default:
lastError = "Unknown error.";
break;
var result = await InstanceManager.LaunchInstance(loggedInUserGuid.Value, Instance.Configuration.AgentGuid, InstanceGuid, CancellationToken);
if (!result.Is(LaunchInstanceResult.LaunchInitiated)) {
lastError = result.Map(Messages.ToSentence, InstanceActionFailureExtensions.ToSentence);
}
} finally {
isLaunchingInstance = false;

View File

@@ -6,20 +6,14 @@
@inherits PhantomComponent
@inject InstanceManager InstanceManager
@if (isLoading) {
<h1>Edit Instance</h1>
<p>Loading...</p>
return;
}
@if (Instance == null) {
<h1>Instance Not Found</h1>
<p>Return to <a href="instances">all instances</a>.</p>
return;
}
else {
<h1>Edit Instance: @Instance.Configuration.InstanceName</h1>
<InstanceAddOrEditForm EditedInstance="Instance" />
}
@code {
@@ -27,11 +21,9 @@
public Guid InstanceGuid { get; init; }
private Instance? Instance { get; set; }
private bool isLoading = true;
protected override async Task OnInitializedAsync() {
Instance = InstanceManager.GetByGuid(await GetAuthenticatedUser(), InstanceGuid);
isLoading = false;
protected override void OnInitialized() {
Instance = InstanceManager.GetByGuid(InstanceGuid);
}
}

View File

@@ -21,7 +21,6 @@
<Column Width="40%">Agent</Column>
<Column Width="40%">Name</Column>
<Column MinWidth="215px">Status</Column>
<Column Class="text-center" MinWidth="120px">Players</Column>
<Column Width="20%">Version</Column>
<Column Class="text-center" MinWidth="110px">Server Port</Column>
<Column Class="text-center" MinWidth="110px">Rcon Port</Column>
@@ -41,14 +40,6 @@
<Cell>
<InstanceStatusText Status="instance.Status" />
</Cell>
<Cell class="text-center">
@if (instance.PlayerCounts is var (online, maximum)) {
<p class="font-monospace">@online.ToString() / @maximum.ToString()</p>
}
else {
<p class="font-monospace">-</p>
}
</Cell>
<Cell>@configuration.MinecraftServerKind @configuration.MinecraftVersion</Cell>
<Cell class="text-center">
<p class="font-monospace">@configuration.ServerPort.ToString()</p>
@@ -78,18 +69,9 @@
this.agentNamesByGuid = agents.ToImmutableDictionary(static agent => agent.AgentGuid, static agent => agent.Configuration.AgentName);
InvokeAsync(StateHasChanged);
});
}
protected override async Task OnInitializedAsync() {
var authenticatedUser = await GetAuthenticatedUser();
if (authenticatedUser == null) {
instances = ImmutableArray<Instance>.Empty;
return;
}
InstanceManager.InstancesChanged.Subscribe(this, instances => {
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)
.ThenBy(static instance => instance.Configuration.InstanceName)
.ToImmutableArray();

View File

@@ -1,7 +1,7 @@
@page "/login"
@using System.ComponentModel.DataAnnotations
@using Phantom.Web.Services
@using Phantom.Web.Services.Authentication
@using System.ComponentModel.DataAnnotations
@attribute [AllowAnonymous]
@inject Navigation Navigation
@inject UserLoginManager LoginManager

View File

@@ -1,6 +1,4 @@
@page "/setup"
@using System.ComponentModel.DataAnnotations
@using System.Security.Cryptography
@using Phantom.Common.Data
@using Phantom.Common.Data.Web.Users
@using Phantom.Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults
@@ -9,6 +7,8 @@
@using Phantom.Web.Services
@using Phantom.Web.Services.Authentication
@using Phantom.Web.Services.Rpc
@using System.ComponentModel.DataAnnotations
@using System.Security.Cryptography
@attribute [AllowAnonymous]
@inject ApplicationProperties ApplicationProperties
@inject UserLoginManager LoginManager

View File

@@ -1,11 +1,11 @@
@page "/users"
@attribute [Authorize(Permission.ViewUsersPolicy)]
@using System.Collections.Immutable
@using Phantom.Common.Data.Web.Users
@using Phantom.Web.Services.Authentication
@using Phantom.Web.Services.Authorization
@using Phantom.Web.Services.Users
@inherits PhantomComponent
@using Phantom.Common.Data.Web.Users
@inherits Phantom.Web.Components.PhantomComponent
@inject UserManager UserManager
@inject RoleManager RoleManager
@inject UserRoleManager UserRoleManager
@@ -60,7 +60,7 @@
@code {
private Guid? me = null;
private Guid? me = Guid.Empty;
private ImmutableArray<UserInfo>? allUsers;
private ImmutableDictionary<Guid, RoleInfo> allRolesByGuid = ImmutableDictionary<Guid, RoleInfo>.Empty;
private readonly Dictionary<Guid, string> userGuidToRoleDescription = new ();
@@ -71,7 +71,7 @@
private UserDeleteDialog userDeleteDialog = null!;
protected override async Task OnInitializedAsync() {
me = (await GetAuthenticatedUser())?.Info.Guid;
me = await GetUserGuid();
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);

View File

@@ -2,14 +2,13 @@
@using System.Collections.Immutable
@using System.ComponentModel.DataAnnotations
@using System.Diagnostics.CodeAnalysis
@using Phantom.Common.Data.Minecraft
@using Phantom.Common.Data.Replies
@using Phantom.Common.Data.Web.Agent
@using Phantom.Common.Data.Web.Instance
@using Phantom.Common.Data.Web.Minecraft
@using Phantom.Common.Data.Web.Users
@using Phantom.Common.Messages.Web.ToController
@using Phantom.Utils.Result
@using Phantom.Common.Data.Replies
@using Phantom.Common.Data.Web.Agent
@using Phantom.Common.Data.Minecraft
@using Phantom.Common.Data.Java
@using Phantom.Common.Data
@using Phantom.Common.Data.Instance
@@ -30,8 +29,7 @@
@{
static RenderFragment GetAgentOption(Agent agent) {
var configuration = agent.Configuration;
return
@<option value="@agent.AgentGuid">
return @<option value="@agent.AgentGuid">
@configuration.AgentName
&bullet;
@(agent.Stats?.RunningInstanceCount.ToString() ?? "?")/@(configuration.MaxInstances) @(configuration.MaxInstances == 1 ? "Instance" : "Instances")
@@ -278,11 +276,10 @@
}
protected override async Task OnInitializedAsync() {
var authenticatedUser = await GetAuthenticatedUser();
var agentJavaRuntimesTask = ControllerConnection.Send<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(new GetAgentJavaRuntimesMessage(), TimeSpan.FromSeconds(30));
var minecraftVersionsTask = ControllerConnection.Send<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(new GetMinecraftVersionsMessage(), TimeSpan.FromSeconds(30));
allAgentsByGuid = AgentManager.ToDictionaryByGuid(authenticatedUser);
allAgentsByGuid = AgentManager.ToDictionaryByGuid();
allAgentJavaRuntimes = await agentJavaRuntimesTask;
allMinecraftVersions = await minecraftVersionsTask;
@@ -327,6 +324,12 @@
await form.SubmitModel.StartSubmitting();
var loggedInUserGuid = await GetUserGuid();
if (loggedInUserGuid == null || !await CheckPermission(Permission.CreateInstances)) {
form.SubmitModel.StopSubmitting("You do not have permission to edit instances.");
return;
}
var instanceGuid = EditedInstance?.InstanceGuid ?? Guid.NewGuid();
var instanceConfiguration = new InstanceConfiguration(
EditedInstance?.Configuration.AgentGuid ?? selectedAgent.AgentGuid,
@@ -340,28 +343,12 @@
JvmArgumentsHelper.Split(form.JvmArguments)
);
var result = await InstanceManager.CreateOrUpdateInstance(await GetAuthenticatedUser(), instanceGuid, instanceConfiguration, CancellationToken);
switch (result.Variant()) {
case Ok<CreateOrUpdateInstanceResult>(CreateOrUpdateInstanceResult.Success):
var result = await InstanceManager.CreateOrUpdateInstance(loggedInUserGuid.Value, instanceGuid, instanceConfiguration, CancellationToken);
if (result.Is(CreateOrUpdateInstanceResult.Success)) {
await Navigation.NavigateTo("instances/" + instanceGuid);
break;
case Ok<CreateOrUpdateInstanceResult>(var createOrUpdateInstanceResult):
form.SubmitModel.StopSubmitting(createOrUpdateInstanceResult.ToSentence());
break;
case Err<UserInstanceActionFailure>(OfInstanceActionFailure(var failure)):
form.SubmitModel.StopSubmitting(failure.ToSentence());
break;
case Err<UserInstanceActionFailure>(OfUserActionFailure(UserActionFailure.NotAuthorized)):
form.SubmitModel.StopSubmitting("You do not have permission to create or edit instances.");
break;
default:
form.SubmitModel.StopSubmitting("Unknown error.");
break;
}
else {
form.SubmitModel.StopSubmitting(result.Map(CreateOrUpdateInstanceResultExtensions.ToSentence, InstanceActionFailureExtensions.ToSentence));
}
}

View File

@@ -1,12 +1,11 @@
@using Phantom.Common.Data.Replies
@using Phantom.Web.Services.Instances
@using Phantom.Common.Data.Web.Users
@using Phantom.Utils.Result
@using Phantom.Web.Services.Instances
@using Phantom.Common.Data.Replies
@inherits Phantom.Web.Components.PhantomComponent
@inject InstanceManager InstanceManager
<Form Model="form" OnSubmit="ExecuteCommand">
<label for="command-input" class="form-label">Execute Command</label>
<label for="command-input" class="form-label">Instance Name</label>
<div class="input-group flex-nowrap">
<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" />
@@ -37,29 +36,19 @@
private async Task ExecuteCommand(EditContext context) {
await form.SubmitModel.StartSubmitting();
var result = await InstanceManager.SendCommandToInstance(await GetAuthenticatedUser(), AgentGuid, InstanceGuid, form.Command, CancellationToken);
var loggedInUserGuid = await GetUserGuid();
if (loggedInUserGuid == null || !await CheckPermission(Permission.ControlInstances)) {
form.SubmitModel.StopSubmitting("You do not have permission to execute commands.");
return;
}
switch (result.Variant()) {
case Ok<SendCommandToInstanceResult>(SendCommandToInstanceResult.Success):
var result = await InstanceManager.SendCommandToInstance(loggedInUserGuid.Value, AgentGuid, InstanceGuid, form.Command, CancellationToken);
if (result.Is(SendCommandToInstanceResult.Success)) {
form.Command = string.Empty;
form.SubmitModel.StopSubmitting();
break;
case Ok<SendCommandToInstanceResult>(var sendCommandToInstanceResult):
form.SubmitModel.StopSubmitting(sendCommandToInstanceResult.ToSentence());
break;
case Err<UserInstanceActionFailure>(OfInstanceActionFailure(var failure)):
form.SubmitModel.StopSubmitting(failure.ToSentence());
break;
case Err<UserInstanceActionFailure>(OfUserActionFailure(UserActionFailure.NotAuthorized)):
form.SubmitModel.StopSubmitting("You do not have permission to send commands to this instance.");
break;
default:
form.SubmitModel.StopSubmitting("Unknown error.");
break;
}
else {
form.SubmitModel.StopSubmitting(result.Map(Messages.ToSentence, InstanceActionFailureExtensions.ToSentence));
}
StateHasChanged();

View File

@@ -1,9 +1,8 @@
@using Phantom.Common.Data.Replies
@using Phantom.Common.Data.Web.Users
@using Phantom.Utils.Result
@using Phantom.Web.Services.Instances
@using Phantom.Web.Services.Instances
@using System.ComponentModel.DataAnnotations
@using Phantom.Common.Data.Web.Users
@using Phantom.Common.Data.Minecraft
@using Phantom.Common.Data.Replies
@inherits Phantom.Web.Components.PhantomComponent
@inject IJSRuntime Js;
@inject InstanceManager InstanceManager;
@@ -54,29 +53,19 @@
private async Task StopInstance(EditContext context) {
await form.SubmitModel.StartSubmitting();
var result = await InstanceManager.StopInstance(await GetAuthenticatedUser(), AgentGuid, InstanceGuid, new MinecraftStopStrategy(form.StopInSeconds), CancellationToken);
var loggedInUserGuid = await GetUserGuid();
if (loggedInUserGuid == null || !await CheckPermission(Permission.ControlInstances)) {
form.SubmitModel.StopSubmitting("You do not have permission to stop instances.");
return;
}
switch (result.Variant()) {
case Ok<StopInstanceResult>(StopInstanceResult.StopInitiated):
var result = await InstanceManager.StopInstance(loggedInUserGuid.Value, AgentGuid, InstanceGuid, new MinecraftStopStrategy(form.StopInSeconds), CancellationToken);
if (result.Is(StopInstanceResult.StopInitiated)) {
await Js.InvokeVoidAsync("closeModal", ModalId);
form.SubmitModel.StopSubmitting();
break;
case Ok<StopInstanceResult>(var stopInstanceResult):
form.SubmitModel.StopSubmitting(stopInstanceResult.ToSentence());
break;
case Err<UserInstanceActionFailure>(OfInstanceActionFailure(var failure)):
form.SubmitModel.StopSubmitting(failure.ToSentence());
break;
case Err<UserInstanceActionFailure>(OfUserActionFailure(UserActionFailure.NotAuthorized)):
form.SubmitModel.StopSubmitting("You do not have permission to stop this instance.");
break;
default:
form.SubmitModel.StopSubmitting("Unknown error.");
break;
}
else {
form.SubmitModel.StopSubmitting(result.Map(Messages.ToSentence, InstanceActionFailureExtensions.ToSentence));
}
}

Some files were not shown because too many files have changed in this diff Show More