1
0
mirror of https://github.com/chylex/Minecraft-Phantom-Panel.git synced 2024-11-25 16:42:54 +01:00

Compare commits

...

1 Commits

Author SHA1 Message Date
55d8c6bcfc
Fully separate Controller and Web into their own services 2023-10-10 14:38:34 +02:00
37 changed files with 554 additions and 475 deletions

View File

@ -1,15 +1,15 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Server" type="DotNetProject" factoryName=".NET Project"> <configuration default="false" name="Controller" type="DotNetProject" factoryName=".NET Project">
<option name="EXE_PATH" value="$PROJECT_DIR$/.artifacts/bin/Phantom.Controller/debug/Phantom.Controller.exe" /> <option name="EXE_PATH" value="$PROJECT_DIR$/.artifacts/bin/Phantom.Controller/debug/Phantom.Controller.exe" />
<option name="PROGRAM_PARAMETERS" value="" /> <option name="PROGRAM_PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Server" /> <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Controller" />
<option name="PASS_PARENT_ENVS" value="1" /> <option name="PASS_PARENT_ENVS" value="1" />
<envs> <envs>
<env name="ASPNETCORE_ENVIRONMENT" value="Development" /> <env name="ASPNETCORE_ENVIRONMENT" value="Development" />
<env name="PG_DATABASE" value="postgres" /> <env name="PG_DATABASE" value="postgres" />
<env name="PG_HOST" value="localhost" /> <env name="PG_HOST" value="localhost" />
<env name="PG_PASS" value="development" /> <env name="PG_PASS" value="development" />
<env name="PG_PORT" value="9402" /> <env name="PG_PORT" value="9403" />
<env name="PG_USER" value="postgres" /> <env name="PG_USER" value="postgres" />
<env name="RPC_SERVER_HOST" value="localhost" /> <env name="RPC_SERVER_HOST" value="localhost" />
<env name="WEB_SERVER_HOST" value="localhost" /> <env name="WEB_SERVER_HOST" value="localhost" />

1
.workdir/Controller/.gitignore vendored Normal file
View File

@ -0,0 +1 @@


View File

@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure;
namespace Phantom.Controller.Database.Postgres;
public sealed class ApplicationDbContextFactory : IDatabaseProvider {
private readonly PooledDbContextFactory<ApplicationDbContext> factory;
public ApplicationDbContextFactory(string connectionString) {
this.factory = new PooledDbContextFactory<ApplicationDbContext>(CreateOptions(connectionString), poolSize: 32);
}
public ApplicationDbContext Provide() {
return factory.CreateDbContext();
}
private static DbContextOptions<ApplicationDbContext> CreateOptions(string connectionString) {
var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
builder.UseNpgsql(connectionString, ConfigureOptions);
return builder.Options;
}
private static void ConfigureOptions(NpgsqlDbContextOptionsBuilder options) {
options.CommandTimeout(10);
options.MigrationsAssembly(typeof(ApplicationDbContextDesignFactory).Assembly.FullName);
}
}

View File

@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore;
using Phantom.Common.Logging;
using Phantom.Utils.Tasks;
using Serilog;
namespace Phantom.Controller.Database;
public static class DatabaseMigrator {
private static readonly ILogger Logger = PhantomLogger.Create(nameof(DatabaseMigrator));
public static async Task Run(IDatabaseProvider databaseProvider, CancellationToken cancellationToken) {
await using var ctx = databaseProvider.Provide();
Logger.Information("Connecting to database...");
var retryConnection = new Throttler(TimeSpan.FromSeconds(10));
while (!await ctx.Database.CanConnectAsync(cancellationToken)) {
Logger.Warning("Cannot connect to database, retrying...");
await retryConnection.Wait();
}
Logger.Information("Running migrations...");
await ctx.Database.MigrateAsync(CancellationToken.None);
}
}

View File

@ -1,30 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
namespace Phantom.Controller.Database;
public sealed class DatabaseProvider {
private readonly IServiceScopeFactory serviceScopeFactory;
public DatabaseProvider(IServiceScopeFactory serviceScopeFactory) {
this.serviceScopeFactory = serviceScopeFactory;
}
public Scope CreateScope() {
return new Scope(serviceScopeFactory.CreateScope());
}
public readonly struct Scope : IDisposable {
private readonly IServiceScope scope;
public ApplicationDbContext Ctx { get; }
internal Scope(IServiceScope scope) {
this.scope = scope;
this.Ctx = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
}
public void Dispose() {
scope.Dispose();
}
}
}

View File

@ -0,0 +1,5 @@
namespace Phantom.Controller.Database;
public interface IDatabaseProvider {
ApplicationDbContext Provide();
}

View File

@ -15,6 +15,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Common\Phantom.Common.Data\Phantom.Common.Data.csproj" /> <ProjectReference Include="..\..\Common\Phantom.Common.Data\Phantom.Common.Data.csproj" />
<ProjectReference Include="..\..\Common\Phantom.Common.Logging\Phantom.Common.Logging.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -27,19 +27,19 @@ public sealed class AgentManager {
private readonly CancellationToken cancellationToken; private readonly CancellationToken cancellationToken;
private readonly AgentAuthToken authToken; private readonly AgentAuthToken authToken;
private readonly DatabaseProvider databaseProvider; private readonly IDatabaseProvider databaseProvider;
public AgentManager(ServiceConfiguration configuration, AgentAuthToken authToken, DatabaseProvider databaseProvider, TaskManager taskManager) { public AgentManager(AgentAuthToken authToken, IDatabaseProvider databaseProvider, TaskManager taskManager, CancellationToken cancellationToken) {
this.cancellationToken = configuration.CancellationToken;
this.authToken = authToken; this.authToken = authToken;
this.databaseProvider = databaseProvider; this.databaseProvider = databaseProvider;
this.cancellationToken = cancellationToken;
taskManager.Run("Refresh agent status loop", RefreshAgentStatus); taskManager.Run("Refresh agent status loop", RefreshAgentStatus);
} }
public async Task Initialize() { internal async Task Initialize() {
using var scope = databaseProvider.CreateScope(); await using var ctx = databaseProvider.Provide();
await foreach (var entity in scope.Ctx.Agents.AsAsyncEnumerable().WithCancellation(cancellationToken)) { await foreach (var entity in ctx.Agents.AsAsyncEnumerable().WithCancellation(cancellationToken)) {
var agent = new Agent(entity.AgentGuid, entity.Name, entity.ProtocolVersion, entity.BuildVersion, entity.MaxInstances, entity.MaxMemory); var agent = new Agent(entity.AgentGuid, entity.Name, entity.ProtocolVersion, entity.BuildVersion, entity.MaxInstances, entity.MaxMemory);
if (!agents.ByGuid.AddOrReplaceIf(agent.Guid, agent, static oldAgent => oldAgent.IsOffline)) { if (!agents.ByGuid.AddOrReplaceIf(agent.Guid, agent, static oldAgent => oldAgent.IsOffline)) {
// TODO // TODO
@ -68,8 +68,8 @@ public sealed class AgentManager {
oldAgent.Connection?.Close(); oldAgent.Connection?.Close();
} }
using (var scope = databaseProvider.CreateScope()) { await using (var ctx = databaseProvider.Provide()) {
var entity = scope.Ctx.AgentUpsert.Fetch(agent.Guid); var entity = ctx.AgentUpsert.Fetch(agent.Guid);
entity.Name = agent.Name; entity.Name = agent.Name;
entity.ProtocolVersion = agent.ProtocolVersion; entity.ProtocolVersion = agent.ProtocolVersion;
@ -77,7 +77,7 @@ public sealed class AgentManager {
entity.MaxInstances = agent.MaxInstances; entity.MaxInstances = agent.MaxInstances;
entity.MaxMemory = agent.MaxMemory; entity.MaxMemory = agent.MaxMemory;
await scope.Ctx.SaveChangesAsync(cancellationToken); await ctx.SaveChangesAsync(cancellationToken);
} }
Logger.Information("Registered agent \"{Name}\" (GUID {Guid}).", agent.Name, agent.Guid); Logger.Information("Registered agent \"{Name}\" (GUID {Guid}).", agent.Name, agent.Guid);

View File

@ -9,16 +9,16 @@ using Phantom.Utils.Tasks;
namespace Phantom.Controller.Services.Audit; namespace Phantom.Controller.Services.Audit;
public sealed partial class AuditLog { public sealed partial class AuditLog {
private readonly CancellationToken cancellationToken; private readonly IDatabaseProvider databaseProvider;
private readonly DatabaseProvider databaseProvider;
private readonly AuthenticationStateProvider authenticationStateProvider; private readonly AuthenticationStateProvider authenticationStateProvider;
private readonly TaskManager taskManager; private readonly TaskManager taskManager;
private readonly CancellationToken cancellationToken;
public AuditLog(ServiceConfiguration serviceConfiguration, DatabaseProvider databaseProvider, AuthenticationStateProvider authenticationStateProvider, TaskManager taskManager) { public AuditLog(IDatabaseProvider databaseProvider, AuthenticationStateProvider authenticationStateProvider, TaskManager taskManager, CancellationToken cancellationToken) {
this.cancellationToken = serviceConfiguration.CancellationToken;
this.databaseProvider = databaseProvider; this.databaseProvider = databaseProvider;
this.authenticationStateProvider = authenticationStateProvider; this.authenticationStateProvider = authenticationStateProvider;
this.taskManager = taskManager; this.taskManager = taskManager;
this.cancellationToken = cancellationToken;
} }
private async Task<Guid?> GetCurrentAuthenticatedUserId() { private async Task<Guid?> GetCurrentAuthenticatedUserId() {
@ -27,9 +27,9 @@ public sealed partial class AuditLog {
} }
private async Task AddEntityToDatabase(AuditLogEntity logEntity) { private async Task AddEntityToDatabase(AuditLogEntity logEntity) {
using var scope = databaseProvider.CreateScope(); await using var ctx = databaseProvider.Provide();
scope.Ctx.AuditLog.Add(logEntity); ctx.AuditLog.Add(logEntity);
await scope.Ctx.SaveChangesAsync(cancellationToken); await ctx.SaveChangesAsync(cancellationToken);
} }
private void AddItem(Guid? userGuid, AuditLogEventType eventType, string subjectId, Dictionary<string, object?>? extra = null) { private void AddItem(Guid? userGuid, AuditLogEventType eventType, string subjectId, Dictionary<string, object?>? extra = null) {
@ -42,13 +42,13 @@ public sealed partial class AuditLog {
} }
public async Task<AuditLogItem[]> GetItems(int count, CancellationToken cancellationToken) { public async Task<AuditLogItem[]> GetItems(int count, CancellationToken cancellationToken) {
using var scope = databaseProvider.CreateScope(); await using var ctx = databaseProvider.Provide();
return await scope.Ctx.AuditLog return await ctx.AuditLog
.Include(static entity => entity.User) .Include(static entity => entity.User)
.AsQueryable() .AsQueryable()
.OrderByDescending(static entity => entity.UtcTime) .OrderByDescending(static entity => entity.UtcTime)
.Take(count) .Take(count)
.Select(static entity => new AuditLogItem(entity.UtcTime, entity.UserGuid, entity.User == null ? null : entity.User.Name, entity.EventType, entity.SubjectType, entity.SubjectId, entity.Data)) .Select(static entity => new AuditLogItem(entity.UtcTime, entity.UserGuid, entity.User == null ? null : entity.User.Name, entity.EventType, entity.SubjectType, entity.SubjectId, entity.Data))
.ToArrayAsync(cancellationToken); .ToArrayAsync(cancellationToken);
} }
} }

View File

@ -0,0 +1,65 @@
using Phantom.Common.Data.Agent;
using Phantom.Common.Logging;
using Phantom.Controller.Database;
using Phantom.Controller.Minecraft;
using Phantom.Controller.Rpc;
using Phantom.Controller.Services.Agents;
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.Permissions;
using Phantom.Controller.Services.Users.Roles;
using Phantom.Utils.Tasks;
namespace Phantom.Controller.Services;
public sealed class ControllerServices {
private TaskManager TaskManager { get; }
private MinecraftVersions MinecraftVersions { get; }
private AgentManager AgentManager { get; }
private AgentJavaRuntimesManager AgentJavaRuntimesManager { get; }
private EventLog EventLog { get; }
private InstanceManager InstanceManager { get; }
private InstanceLogManager InstanceLogManager { get; }
private UserManager UserManager { get; }
private RoleManager RoleManager { get; }
private UserRoleManager UserRoleManager { get; }
private PermissionManager PermissionManager { get; }
private readonly IDatabaseProvider databaseProvider;
private readonly CancellationToken cancellationToken;
public ControllerServices(IDatabaseProvider databaseProvider, AgentAuthToken agentAuthToken, CancellationToken shutdownCancellationToken) {
this.TaskManager = new TaskManager(PhantomLogger.Create<TaskManager, ControllerServices>());
this.MinecraftVersions = new MinecraftVersions();
this.AgentManager = new AgentManager(agentAuthToken, databaseProvider, TaskManager, shutdownCancellationToken);
this.AgentJavaRuntimesManager = new AgentJavaRuntimesManager();
this.EventLog = new EventLog(databaseProvider, TaskManager, shutdownCancellationToken);
this.InstanceManager = new InstanceManager(AgentManager, MinecraftVersions, databaseProvider, shutdownCancellationToken);
this.InstanceLogManager = new InstanceLogManager();
this.UserManager = new UserManager(databaseProvider);
this.RoleManager = new RoleManager(databaseProvider);
this.UserRoleManager = new UserRoleManager(databaseProvider);
this.PermissionManager = new PermissionManager(databaseProvider);
this.databaseProvider = databaseProvider;
this.cancellationToken = shutdownCancellationToken;
}
public MessageToServerListener CreateMessageToServerListener(RpcClientConnection connection) {
return new MessageToServerListener(connection, AgentManager, AgentJavaRuntimesManager, InstanceManager, InstanceLogManager, EventLog, cancellationToken);
}
public async Task Initialize() {
await DatabaseMigrator.Run(databaseProvider, cancellationToken);
await PermissionManager.Initialize();
await RoleManager.Initialize();
await AgentManager.Initialize();
await InstanceManager.Initialize();
}
}

View File

@ -1,26 +1,28 @@
using Microsoft.EntityFrameworkCore; using System.Collections.Immutable;
using Microsoft.EntityFrameworkCore;
using Phantom.Controller.Database; using Phantom.Controller.Database;
using Phantom.Controller.Database.Entities; using Phantom.Controller.Database.Entities;
using Phantom.Controller.Database.Enums; using Phantom.Controller.Database.Enums;
using Phantom.Utils.Collections;
using Phantom.Utils.Tasks; using Phantom.Utils.Tasks;
namespace Phantom.Controller.Services.Events; namespace Phantom.Controller.Services.Events;
public sealed partial class EventLog { public sealed partial class EventLog {
private readonly CancellationToken cancellationToken; private readonly IDatabaseProvider databaseProvider;
private readonly DatabaseProvider databaseProvider;
private readonly TaskManager taskManager; private readonly TaskManager taskManager;
private readonly CancellationToken cancellationToken;
public EventLog(ServiceConfiguration serviceConfiguration, DatabaseProvider databaseProvider, TaskManager taskManager) { public EventLog(IDatabaseProvider databaseProvider, TaskManager taskManager, CancellationToken cancellationToken) {
this.cancellationToken = serviceConfiguration.CancellationToken;
this.databaseProvider = databaseProvider; this.databaseProvider = databaseProvider;
this.taskManager = taskManager; this.taskManager = taskManager;
this.cancellationToken = cancellationToken;
} }
private async Task AddEntityToDatabase(EventLogEntity logEntity) { private async Task AddEntityToDatabase(EventLogEntity logEntity) {
using var scope = databaseProvider.CreateScope(); await using var ctx = databaseProvider.Provide();
scope.Ctx.EventLog.Add(logEntity); ctx.EventLog.Add(logEntity);
await scope.Ctx.SaveChangesAsync(cancellationToken); await ctx.SaveChangesAsync(cancellationToken);
} }
private void AddItem(Guid eventGuid, DateTime utcTime, Guid? agentGuid, EventLogEventType eventType, string subjectId, Dictionary<string, object?>? extra = null) { private void AddItem(Guid eventGuid, DateTime utcTime, Guid? agentGuid, EventLogEventType eventType, string subjectId, Dictionary<string, object?>? extra = null) {
@ -28,13 +30,14 @@ public sealed partial class EventLog {
taskManager.Run("Store event log item to database", () => AddEntityToDatabase(logEntity)); taskManager.Run("Store event log item to database", () => AddEntityToDatabase(logEntity));
} }
public async Task<EventLogItem[]> GetItems(int count, CancellationToken cancellationToken) { public async Task<ImmutableArray<EventLogItem>> GetItems(int count, CancellationToken cancellationToken) {
using var scope = databaseProvider.CreateScope(); await using var ctx = databaseProvider.Provide();
return await scope.Ctx.EventLog return await ctx.EventLog
.AsQueryable() .AsQueryable()
.OrderByDescending(static entity => entity.UtcTime) .OrderByDescending(static entity => entity.UtcTime)
.Take(count) .Take(count)
.Select(static entity => new EventLogItem(entity.UtcTime, entity.AgentGuid, entity.EventType, entity.SubjectType, entity.SubjectId, entity.Data)) .Select(static entity => new EventLogItem(entity.UtcTime, entity.AgentGuid, entity.EventType, entity.SubjectType, entity.SubjectId, entity.Data))
.ToArrayAsync(cancellationToken); .AsAsyncEnumerable()
.ToImmutableArrayAsync(cancellationToken);
} }
} }

View File

@ -24,23 +24,23 @@ public sealed class InstanceManager {
public EventSubscribers<ImmutableDictionary<Guid, Instance>> InstancesChanged => instances.Subs; public EventSubscribers<ImmutableDictionary<Guid, Instance>> InstancesChanged => instances.Subs;
private readonly CancellationToken cancellationToken;
private readonly AgentManager agentManager; private readonly AgentManager agentManager;
private readonly MinecraftVersions minecraftVersions; private readonly MinecraftVersions minecraftVersions;
private readonly DatabaseProvider databaseProvider; private readonly IDatabaseProvider databaseProvider;
private readonly CancellationToken cancellationToken;
private readonly SemaphoreSlim modifyInstancesSemaphore = new (1, 1); private readonly SemaphoreSlim modifyInstancesSemaphore = new (1, 1);
public InstanceManager(ServiceConfiguration configuration, AgentManager agentManager, MinecraftVersions minecraftVersions, DatabaseProvider databaseProvider) { public InstanceManager(AgentManager agentManager, MinecraftVersions minecraftVersions, IDatabaseProvider databaseProvider, CancellationToken cancellationToken) {
this.cancellationToken = configuration.CancellationToken;
this.agentManager = agentManager; this.agentManager = agentManager;
this.minecraftVersions = minecraftVersions; this.minecraftVersions = minecraftVersions;
this.databaseProvider = databaseProvider; this.databaseProvider = databaseProvider;
this.cancellationToken = cancellationToken;
} }
public async Task Initialize() { public async Task Initialize() {
using var scope = databaseProvider.CreateScope(); await using var ctx = databaseProvider.Provide();
await foreach (var entity in ctx.Instances.AsAsyncEnumerable().WithCancellation(cancellationToken)) {
await foreach (var entity in scope.Ctx.Instances.AsAsyncEnumerable().WithCancellation(cancellationToken)) {
var configuration = new InstanceConfiguration( var configuration = new InstanceConfiguration(
entity.AgentGuid, entity.AgentGuid,
entity.InstanceGuid, entity.InstanceGuid,
@ -98,8 +98,8 @@ public sealed class InstanceManager {
}); });
if (result.Is(AddOrEditInstanceResult.Success)) { if (result.Is(AddOrEditInstanceResult.Success)) {
using var scope = databaseProvider.CreateScope(); await using var ctx = databaseProvider.Provide();
InstanceEntity entity = scope.Ctx.InstanceUpsert.Fetch(configuration.InstanceGuid); InstanceEntity entity = ctx.InstanceUpsert.Fetch(configuration.InstanceGuid);
entity.AgentGuid = configuration.AgentGuid; entity.AgentGuid = configuration.AgentGuid;
entity.InstanceName = configuration.InstanceName; entity.InstanceName = configuration.InstanceName;
@ -111,7 +111,7 @@ public sealed class InstanceManager {
entity.JavaRuntimeGuid = configuration.JavaRuntimeGuid; entity.JavaRuntimeGuid = configuration.JavaRuntimeGuid;
entity.JvmArguments = JvmArgumentsHelper.Join(configuration.JvmArguments); entity.JvmArguments = JvmArgumentsHelper.Join(configuration.JvmArguments);
await scope.Ctx.SaveChangesAsync(cancellationToken); await ctx.SaveChangesAsync(cancellationToken);
} }
else if (isNewInstance) { else if (isNewInstance) {
instances.ByGuid.Remove(configuration.InstanceGuid); instances.ByGuid.Remove(configuration.InstanceGuid);
@ -188,11 +188,11 @@ public sealed class InstanceManager {
try { try {
instances.ByGuid.TryReplace(instanceGuid, instance => instance with { LaunchAutomatically = shouldLaunchAutomatically }); instances.ByGuid.TryReplace(instanceGuid, instance => instance with { LaunchAutomatically = shouldLaunchAutomatically });
using var scope = databaseProvider.CreateScope(); await using var ctx = databaseProvider.Provide();
var entity = await scope.Ctx.Instances.FindAsync(instanceGuid, cancellationToken); var entity = await ctx.Instances.FindAsync(instanceGuid, cancellationToken);
if (entity != null) { if (entity != null) {
entity.LaunchAutomatically = shouldLaunchAutomatically; entity.LaunchAutomatically = shouldLaunchAutomatically;
await scope.Ctx.SaveChangesAsync(cancellationToken); await ctx.SaveChangesAsync(cancellationToken);
} }
} finally { } finally {
modifyInstancesSemaphore.Release(); modifyInstancesSemaphore.Release();

View File

@ -15,25 +15,25 @@ namespace Phantom.Controller.Services.Rpc;
public sealed class MessageToServerListener : IMessageToServerListener { public sealed class MessageToServerListener : IMessageToServerListener {
private readonly RpcClientConnection connection; private readonly RpcClientConnection connection;
private readonly CancellationToken cancellationToken;
private readonly AgentManager agentManager; private readonly AgentManager agentManager;
private readonly AgentJavaRuntimesManager agentJavaRuntimesManager; private readonly AgentJavaRuntimesManager agentJavaRuntimesManager;
private readonly InstanceManager instanceManager; private readonly InstanceManager instanceManager;
private readonly InstanceLogManager instanceLogManager; private readonly InstanceLogManager instanceLogManager;
private readonly EventLog eventLog; private readonly EventLog eventLog;
private readonly CancellationToken cancellationToken;
private readonly TaskCompletionSource<Guid> agentGuidWaiter = AsyncTasks.CreateCompletionSource<Guid>(); private readonly TaskCompletionSource<Guid> agentGuidWaiter = AsyncTasks.CreateCompletionSource<Guid>();
public bool IsDisposed { get; private set; } public bool IsDisposed { get; private set; }
internal MessageToServerListener(RpcClientConnection connection, ServiceConfiguration configuration, AgentManager agentManager, AgentJavaRuntimesManager agentJavaRuntimesManager, InstanceManager instanceManager, InstanceLogManager instanceLogManager, EventLog eventLog) { internal MessageToServerListener(RpcClientConnection connection, AgentManager agentManager, AgentJavaRuntimesManager agentJavaRuntimesManager, InstanceManager instanceManager, InstanceLogManager instanceLogManager, EventLog eventLog, CancellationToken cancellationToken) {
this.connection = connection; this.connection = connection;
this.cancellationToken = configuration.CancellationToken;
this.agentManager = agentManager; this.agentManager = agentManager;
this.agentJavaRuntimesManager = agentJavaRuntimesManager; this.agentJavaRuntimesManager = agentJavaRuntimesManager;
this.instanceManager = instanceManager; this.instanceManager = instanceManager;
this.instanceLogManager = instanceLogManager; this.instanceLogManager = instanceLogManager;
this.eventLog = eventLog; this.eventLog = eventLog;
this.cancellationToken = cancellationToken;
} }
public async Task<NoReply> HandleRegisterAgent(RegisterAgentMessage message) { public async Task<NoReply> HandleRegisterAgent(RegisterAgentMessage message) {

View File

@ -1,28 +0,0 @@
using Phantom.Controller.Rpc;
using Phantom.Controller.Services.Agents;
using Phantom.Controller.Services.Events;
using Phantom.Controller.Services.Instances;
namespace Phantom.Controller.Services.Rpc;
public sealed class MessageToServerListenerFactory {
private readonly ServiceConfiguration configuration;
private readonly AgentManager agentManager;
private readonly AgentJavaRuntimesManager agentJavaRuntimesManager;
private readonly InstanceManager instanceManager;
private readonly InstanceLogManager instanceLogManager;
private readonly EventLog eventLog;
public MessageToServerListenerFactory(ServiceConfiguration configuration, AgentManager agentManager, AgentJavaRuntimesManager agentJavaRuntimesManager, InstanceManager instanceManager, InstanceLogManager instanceLogManager, EventLog eventLog) {
this.configuration = configuration;
this.agentManager = agentManager;
this.agentJavaRuntimesManager = agentJavaRuntimesManager;
this.instanceManager = instanceManager;
this.instanceLogManager = instanceLogManager;
this.eventLog = eventLog;
}
public MessageToServerListener CreateListener(RpcClientConnection connection) {
return new MessageToServerListener(connection, configuration, agentManager, agentJavaRuntimesManager, instanceManager, instanceLogManager, eventLog);
}
}

View File

@ -1,7 +1,6 @@
using System.Collections.Immutable; using System.Collections.Immutable;
using Phantom.Web.Identity.Data;
namespace Phantom.Web.Identity.Authorization; namespace Phantom.Controller.Services.Users.Permissions;
public sealed class IdentityPermissions { public sealed class IdentityPermissions {
internal static IdentityPermissions None { get; } = new (); internal static IdentityPermissions None { get; } = new ();

View File

@ -1,4 +1,4 @@
namespace Phantom.Web.Identity.Data; namespace Phantom.Controller.Services.Users.Permissions;
public sealed record Permission(string Id, Permission? Parent) { public sealed record Permission(string Id, Permission? Parent) {
private static readonly List<Permission> AllPermissions = new (); private static readonly List<Permission> AllPermissions = new ();

View File

@ -0,0 +1,68 @@
using System.Collections.Immutable;
using System.Security.Claims;
using Microsoft.EntityFrameworkCore;
using Phantom.Common.Logging;
using Phantom.Controller.Database;
using Phantom.Controller.Database.Entities;
using Phantom.Utils.Collections;
using ILogger = Serilog.ILogger;
namespace Phantom.Controller.Services.Users.Permissions;
public sealed class PermissionManager {
private static readonly ILogger Logger = PhantomLogger.Create<PermissionManager>();
private readonly IDatabaseProvider databaseProvider;
private readonly Dictionary<Guid, IdentityPermissions> userIdsToPermissionIds = new ();
public PermissionManager(IDatabaseProvider databaseProvider) {
this.databaseProvider = databaseProvider;
}
internal async Task Initialize() {
Logger.Information("Adding default permissions to database.");
await using var ctx = databaseProvider.Provide();
var existingPermissionIds = await ctx.Permissions.Select(static p => p.Id).AsAsyncEnumerable().ToImmutableSetAsync();
var missingPermissionIds = GetMissingPermissionsOrdered(Permission.All, existingPermissionIds);
if (!missingPermissionIds.IsEmpty) {
Logger.Information("Adding default permissions: {Permissions}", string.Join(", ", missingPermissionIds));
foreach (var permissionId in missingPermissionIds) {
ctx.Permissions.Add(new PermissionEntity(permissionId));
}
await ctx.SaveChangesAsync();
}
}
internal static ImmutableArray<string> GetMissingPermissionsOrdered(IEnumerable<Permission> allPermissions, ImmutableHashSet<string> existingPermissionIds) {
return allPermissions.Select(static permission => permission.Id).Except(existingPermissionIds).Order().ToImmutableArray();
}
private IdentityPermissions FetchPermissionsForUserId(Guid userId) {
using var ctx = databaseProvider.Provide();
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 IdentityPermissions(userPermissions.Union(rolePermissions));
}
private IdentityPermissions GetPermissionsForUserId(Guid userId, bool refreshCache) {
if (!refreshCache && userIdsToPermissionIds.TryGetValue(userId, out var userPermissions)) {
return userPermissions;
}
else {
return userIdsToPermissionIds[userId] = FetchPermissionsForUserId(userId);
}
}
public IdentityPermissions GetPermissions(ClaimsPrincipal user, bool refreshCache = false) {
Guid? userId = UserManager.GetAuthenticatedUserId(user);
return userId == null ? IdentityPermissions.None : GetPermissionsForUserId(userId.Value, refreshCache);
}
public bool CheckPermission(ClaimsPrincipal user, Permission permission, bool refreshCache = false) {
return GetPermissions(user, refreshCache).Check(permission);
}
}

View File

@ -1,60 +0,0 @@
using System.Collections.Immutable;
using Microsoft.EntityFrameworkCore;
using Phantom.Common.Logging;
using Phantom.Controller.Database;
using Phantom.Controller.Database.Entities;
using Phantom.Utils.Collections;
using Phantom.Utils.Tasks;
using ILogger = Serilog.ILogger;
namespace Phantom.Controller.Services.Users;
public sealed class RoleManager {
private static readonly ILogger Logger = PhantomLogger.Create<RoleManager>();
private const int MaxRoleNameLength = 40;
private readonly ApplicationDbContext db;
public RoleManager(ApplicationDbContext db) {
this.db = db;
}
public Task<List<RoleEntity>> GetAll() {
return db.Roles.ToListAsync();
}
public Task<ImmutableHashSet<string>> GetAllNames() {
return db.Roles.Select(static role => role.Name).AsAsyncEnumerable().ToImmutableSetAsync();
}
public ValueTask<RoleEntity?> GetByGuid(Guid guid) {
return db.Roles.FindAsync(guid);
}
public async Task<Result<RoleEntity, AddRoleError>> Create(Guid guid, string name) {
if (string.IsNullOrWhiteSpace(name)) {
return Result.Fail<RoleEntity, AddRoleError>(AddRoleError.NameIsEmpty);
}
else if (name.Length > MaxRoleNameLength) {
return Result.Fail<RoleEntity, AddRoleError>(AddRoleError.NameIsTooLong);
}
try {
if (await db.Roles.AnyAsync(role => role.Name == name)) {
return Result.Fail<RoleEntity, AddRoleError>(AddRoleError.NameAlreadyExists);
}
var role = new RoleEntity(guid, name);
db.Roles.Add(role);
await db.SaveChangesAsync();
Logger.Information("Created role \"{Name}\" (GUID {Guid}).", name, guid);
return Result.Ok<RoleEntity, AddRoleError>(role);
} catch (Exception e) {
Logger.Error(e, "Could not create role \"{Name}\" (GUID {Guid}).", name, guid);
return Result.Fail<RoleEntity, AddRoleError>(AddRoleError.UnknownError);
}
}
}

View File

@ -1,4 +1,4 @@
namespace Phantom.Controller.Services.Users; namespace Phantom.Controller.Services.Users.Roles;
public enum AddRoleError : byte { public enum AddRoleError : byte {
NameIsEmpty, NameIsEmpty,

View File

@ -1,6 +1,7 @@
using System.Collections.Immutable; using System.Collections.Immutable;
using Phantom.Controller.Services.Users.Permissions;
namespace Phantom.Web.Identity.Data; namespace Phantom.Controller.Services.Users.Roles;
public sealed record Role(Guid Guid, string Name, ImmutableArray<Permission> Permissions) { public sealed record Role(Guid Guid, string Name, ImmutableArray<Permission> Permissions) {
private static readonly List<Role> AllRoles = new (); private static readonly List<Role> AllRoles = new ();

View File

@ -0,0 +1,99 @@
using System.Collections.Immutable;
using Microsoft.EntityFrameworkCore;
using Phantom.Common.Logging;
using Phantom.Controller.Database;
using Phantom.Controller.Database.Entities;
using Phantom.Controller.Services.Users.Permissions;
using Phantom.Utils.Collections;
using Phantom.Utils.Tasks;
using ILogger = Serilog.ILogger;
namespace Phantom.Controller.Services.Users.Roles;
public sealed class RoleManager {
private static readonly ILogger Logger = PhantomLogger.Create<RoleManager>();
private const int MaxRoleNameLength = 40;
private readonly IDatabaseProvider databaseProvider;
public RoleManager(IDatabaseProvider databaseProvider) {
this.databaseProvider = databaseProvider;
}
internal async Task Initialize() {
Logger.Information("Adding default roles to database.");
await using var ctx = databaseProvider.Provide();
var existingRoleNames = await ctx.Roles
.Select(static role => role.Name)
.AsAsyncEnumerable()
.ToImmutableSetAsync();
var existingPermissionIdsByRoleGuid = await ctx.RolePermissions
.GroupBy(static rp => rp.RoleGuid, static rp => rp.PermissionId)
.ToDictionaryAsync(static g => g.Key, static g => g.ToImmutableHashSet());
foreach (var role in Role.All) {
if (!existingRoleNames.Contains(role.Name)) {
Logger.Information("Adding default role \"{Name}\".", role.Name);
ctx.Roles.Add(new RoleEntity(role.Guid, role.Name));
}
var existingPermissionIds = existingPermissionIdsByRoleGuid.TryGetValue(role.Guid, out var ids) ? ids : ImmutableHashSet<string>.Empty;
var missingPermissionIds = PermissionManager.GetMissingPermissionsOrdered(role.Permissions, existingPermissionIds);
if (!missingPermissionIds.IsEmpty) {
Logger.Information("Assigning default permission to role \"{Name}\": {Permissions}", role.Name, string.Join(", ", missingPermissionIds));
foreach (var permissionId in missingPermissionIds) {
ctx.RolePermissions.Add(new RolePermissionEntity(role.Guid, permissionId));
}
}
}
await ctx.SaveChangesAsync();
}
public async Task<List<RoleEntity>> GetAll() {
await using var ctx = databaseProvider.Provide();
return await ctx.Roles.ToListAsync();
}
public async Task<ImmutableHashSet<string>> GetAllNames() {
await using var ctx = databaseProvider.Provide();
return await ctx.Roles.Select(static role => role.Name).AsAsyncEnumerable().ToImmutableSetAsync();
}
public async ValueTask<RoleEntity?> GetByGuid(Guid guid) {
await using var ctx = databaseProvider.Provide();
return await ctx.Roles.FindAsync(guid);
}
public async Task<Result<RoleEntity, AddRoleError>> Create(string name) {
if (string.IsNullOrWhiteSpace(name)) {
return Result.Fail<RoleEntity, AddRoleError>(AddRoleError.NameIsEmpty);
}
else if (name.Length > MaxRoleNameLength) {
return Result.Fail<RoleEntity, AddRoleError>(AddRoleError.NameIsTooLong);
}
RoleEntity newRole;
try {
await using var ctx = databaseProvider.Provide();
if (await ctx.Roles.AnyAsync(role => role.Name == name)) {
return Result.Fail<RoleEntity, AddRoleError>(AddRoleError.NameAlreadyExists);
}
newRole = new RoleEntity(Guid.NewGuid(), name);
ctx.Roles.Add(newRole);
await ctx.SaveChangesAsync();
} catch (Exception e) {
Logger.Error(e, "Could not create role \"{Name}\".", name);
return Result.Fail<RoleEntity, AddRoleError>(AddRoleError.UnknownError);
}
Logger.Information("Created role \"{Name}\" (GUID {Guid}).", name, newRole.RoleGuid);
return Result.Ok<RoleEntity, AddRoleError>(newRole);
}
}

View File

@ -0,0 +1,83 @@
using System.Collections.Immutable;
using Microsoft.EntityFrameworkCore;
using Phantom.Common.Logging;
using Phantom.Controller.Database;
using Phantom.Controller.Database.Entities;
using Phantom.Utils.Collections;
using ILogger = Serilog.ILogger;
namespace Phantom.Controller.Services.Users.Roles;
public sealed class UserRoleManager {
private static readonly ILogger Logger = PhantomLogger.Create<UserRoleManager>();
private readonly IDatabaseProvider databaseProvider;
public UserRoleManager(IDatabaseProvider databaseProvider) {
this.databaseProvider = databaseProvider;
}
public async Task<Dictionary<Guid, ImmutableArray<RoleEntity>>> GetAllByUserGuid() {
await using var ctx = databaseProvider.Provide();
return await ctx.UserRoles
.Include(static ur => ur.Role)
.GroupBy(static ur => ur.UserGuid, static ur => ur.Role)
.ToDictionaryAsync(static group => group.Key, static group => group.ToImmutableArray());
}
public async Task<ImmutableArray<RoleEntity>> GetUserRoles(UserEntity user) {
await using var ctx = databaseProvider.Provide();
return await ctx.UserRoles
.Include(static ur => ur.Role)
.Where(ur => ur.UserGuid == user.UserGuid)
.Select(static ur => ur.Role)
.AsAsyncEnumerable()
.ToImmutableArrayAsync();
}
public async Task<ImmutableHashSet<Guid>> GetUserRoleGuids(UserEntity user) {
await using var ctx = databaseProvider.Provide();
return await ctx.UserRoles
.Where(ur => ur.UserGuid == user.UserGuid)
.Select(static ur => ur.RoleGuid)
.AsAsyncEnumerable()
.ToImmutableSetAsync();
}
public async Task<bool> Add(UserEntity user, RoleEntity role) {
try {
await using var ctx = databaseProvider.Provide();
var userRole = await ctx.UserRoles.FindAsync(user.UserGuid, role.RoleGuid);
if (userRole == null) {
userRole = new UserRoleEntity(user.UserGuid, role.RoleGuid);
ctx.UserRoles.Add(userRole);
await ctx.SaveChangesAsync();
}
} catch (Exception e) {
Logger.Error(e, "Could not add user \"{UserName}\" (GUID {UserGuid}) to role \"{RoleName}\" (GUID {RoleGuid}).", user.Name, user.UserGuid, role.Name, role.RoleGuid);
return false;
}
Logger.Information("Added user \"{UserName}\" (GUID {UserGuid}) to role \"{RoleName}\" (GUID {RoleGuid}).", user.Name, user.UserGuid, role.Name, role.RoleGuid);
return true;
}
public async Task<bool> Remove(UserEntity user, RoleEntity role) {
try {
await using var ctx = databaseProvider.Provide();
var userRole = await ctx.UserRoles.FindAsync(user.UserGuid, role.RoleGuid);
if (userRole != null) {
ctx.UserRoles.Remove(userRole);
await ctx.SaveChangesAsync();
}
} catch (Exception e) {
Logger.Error(e, "Could not remove user \"{UserName}\" (GUID {UserGuid}) from role \"{RoleName}\" (GUID {RoleGuid}).", user.Name, user.UserGuid, role.Name, role.RoleGuid);
return false;
}
Logger.Information("Removed user \"{UserName}\" (GUID {UserGuid}) from role \"{RoleName}\" (GUID {RoleGuid}).", user.Name, user.UserGuid, role.Name, role.RoleGuid);
return true;
}
}

View File

@ -16,10 +16,10 @@ public sealed class UserManager {
private const int MaxUserNameLength = 40; private const int MaxUserNameLength = 40;
private readonly ApplicationDbContext db; private readonly IDatabaseProvider databaseProvider;
public UserManager(ApplicationDbContext db) { public UserManager(IDatabaseProvider databaseProvider) {
this.db = db; this.databaseProvider = databaseProvider;
} }
public static Guid? GetAuthenticatedUserId(ClaimsPrincipal user) { public static Guid? GetAuthenticatedUserId(ClaimsPrincipal user) {
@ -35,20 +35,24 @@ public sealed class UserManager {
return Guid.TryParse(claim.Value, out var guid) ? guid : null; return Guid.TryParse(claim.Value, out var guid) ? guid : null;
} }
public Task<ImmutableArray<UserEntity>> GetAll() { public async Task<ImmutableArray<UserEntity>> GetAll() {
return db.Users.AsAsyncEnumerable().ToImmutableArrayAsync(); await using var ctx = databaseProvider.Provide();
return await ctx.Users.AsAsyncEnumerable().ToImmutableArrayAsync();
} }
public Task<Dictionary<Guid, T>> GetAllByGuid<T>(Func<UserEntity, T> valueSelector, CancellationToken cancellationToken = default) { public async Task<Dictionary<Guid, T>> GetAllByGuid<T>(Func<UserEntity, T> valueSelector, CancellationToken cancellationToken = default) {
return db.Users.ToDictionaryAsync(static user => user.UserGuid, valueSelector, cancellationToken); await using var ctx = databaseProvider.Provide();
return await ctx.Users.ToDictionaryAsync(static user => user.UserGuid, valueSelector, cancellationToken);
} }
public Task<UserEntity?> GetByName(string username) { public async Task<UserEntity?> GetByName(string username) {
return db.Users.FirstOrDefaultAsync(user => user.Name == username); await using var ctx = databaseProvider.Provide();
return await ctx.Users.FirstOrDefaultAsync(user => user.Name == username);
} }
public async Task<UserEntity?> GetAuthenticated(string username, string password) { public async Task<UserEntity?> GetAuthenticated(string username, string password) {
var user = await db.Users.FirstOrDefaultAsync(user => user.Name == username); await using var ctx = databaseProvider.Provide();
var user = await ctx.Users.FirstOrDefaultAsync(user => user.Name == username);
if (user == null) { if (user == null) {
return null; return null;
} }
@ -57,7 +61,7 @@ public sealed class UserManager {
case PasswordVerificationResult.SuccessRehashNeeded: case PasswordVerificationResult.SuccessRehashNeeded:
try { try {
UserPasswords.Set(user, password); UserPasswords.Set(user, password);
await db.SaveChangesAsync(); await ctx.SaveChangesAsync();
} catch (Exception e) { } catch (Exception e) {
Logger.Warning(e, "Could not rehash password for \"{Username}\".", user.Name); Logger.Warning(e, "Could not rehash password for \"{Username}\".", user.Name);
} }
@ -87,58 +91,66 @@ public sealed class UserManager {
return Result.Fail<UserEntity, AddUserError>(new AddUserError.PasswordIsInvalid(requirementViolations)); return Result.Fail<UserEntity, AddUserError>(new AddUserError.PasswordIsInvalid(requirementViolations));
} }
UserEntity newUser;
try { try {
if (await db.Users.AnyAsync(user => user.Name == username)) { await using var ctx = databaseProvider.Provide();
if (await ctx.Users.AnyAsync(user => user.Name == username)) {
return Result.Fail<UserEntity, AddUserError>(new AddUserError.NameAlreadyExists()); return Result.Fail<UserEntity, AddUserError>(new AddUserError.NameAlreadyExists());
} }
var guid = Guid.NewGuid(); newUser = new UserEntity(Guid.NewGuid(), username);
var user = new UserEntity(guid, username); UserPasswords.Set(newUser, password);
UserPasswords.Set(user, password);
db.Users.Add(user); ctx.Users.Add(newUser);
await db.SaveChangesAsync(); await ctx.SaveChangesAsync();
Logger.Information("Created user \"{Name}\" (GUID {Guid}).", username, guid);
return Result.Ok<UserEntity, AddUserError>(user);
} catch (Exception e) { } catch (Exception e) {
Logger.Error(e, "Could not create user \"{Name}\".", username); Logger.Error(e, "Could not create user \"{Name}\".", username);
return Result.Fail<UserEntity, AddUserError>(new AddUserError.UnknownError()); return Result.Fail<UserEntity, AddUserError>(new AddUserError.UnknownError());
} }
Logger.Information("Created user \"{Name}\" (GUID {Guid}).", username, newUser.UserGuid);
return Result.Ok<UserEntity, AddUserError>(newUser);
} }
public async Task<Result<SetUserPasswordError>> SetUserPassword(Guid guid, string password) { public async Task<Result<SetUserPasswordError>> SetUserPassword(Guid guid, string password) {
var user = await db.Users.FindAsync(guid); UserEntity foundUser;
if (user == null) {
return Result.Fail<SetUserPasswordError>(new SetUserPasswordError.UserNotFound()); await using (var ctx = databaseProvider.Provide()) {
} var user = await ctx.Users.FindAsync(guid);
if (user == null) {
try { return Result.Fail<SetUserPasswordError>(new SetUserPasswordError.UserNotFound());
var requirementViolations = UserPasswords.CheckRequirements(password);
if (!requirementViolations.IsEmpty) {
return Result.Fail<SetUserPasswordError>(new SetUserPasswordError.PasswordIsInvalid(requirementViolations));
} }
UserPasswords.Set(user, password); foundUser = user;
await db.SaveChangesAsync(); try {
var requirementViolations = UserPasswords.CheckRequirements(password);
if (!requirementViolations.IsEmpty) {
return Result.Fail<SetUserPasswordError>(new SetUserPasswordError.PasswordIsInvalid(requirementViolations));
}
Logger.Information("Changed password for user \"{Name}\" (GUID {Guid}).", user.Name, user.UserGuid); UserPasswords.Set(user, password);
return Result.Ok<SetUserPasswordError>(); await ctx.SaveChangesAsync();
} catch (Exception e) { } catch (Exception e) {
Logger.Error(e, "Could not change password for user \"{Name}\" (GUID {Guid}).", user.Name, user.UserGuid); Logger.Error(e, "Could not change password for user \"{Name}\" (GUID {Guid}).", user.Name, user.UserGuid);
return Result.Fail<SetUserPasswordError>(new SetUserPasswordError.UnknownError()); return Result.Fail<SetUserPasswordError>(new SetUserPasswordError.UnknownError());
}
} }
Logger.Information("Changed password for user \"{Name}\" (GUID {Guid}).", foundUser.Name, foundUser.UserGuid);
return Result.Ok<SetUserPasswordError>();
} }
public async Task<DeleteUserResult> DeleteByGuid(Guid guid) { public async Task<DeleteUserResult> DeleteByGuid(Guid guid) {
var user = await db.Users.FindAsync(guid); await using var ctx = databaseProvider.Provide();
var user = await ctx.Users.FindAsync(guid);
if (user == null) { if (user == null) {
return DeleteUserResult.NotFound; return DeleteUserResult.NotFound;
} }
try { try {
db.Users.Remove(user); ctx.Users.Remove(user);
await db.SaveChangesAsync(); await ctx.SaveChangesAsync();
return DeleteUserResult.Deleted; return DeleteUserResult.Deleted;
} catch (Exception e) { } catch (Exception e) {
Logger.Error(e, "Could not delete user \"{Name}\" (GUID {Guid}).", user.Name, user.UserGuid); Logger.Error(e, "Could not delete user \"{Name}\" (GUID {Guid}).", user.Name, user.UserGuid);

View File

@ -1,76 +0,0 @@
using System.Collections.Immutable;
using Microsoft.EntityFrameworkCore;
using Phantom.Common.Logging;
using Phantom.Controller.Database;
using Phantom.Controller.Database.Entities;
using Phantom.Utils.Collections;
using ILogger = Serilog.ILogger;
namespace Phantom.Controller.Services.Users;
public sealed class UserRoleManager {
private static readonly ILogger Logger = PhantomLogger.Create<UserRoleManager>();
private readonly ApplicationDbContext db;
public UserRoleManager(ApplicationDbContext db) {
this.db = db;
}
public Task<Dictionary<Guid, ImmutableArray<RoleEntity>>> GetAllByUserGuid() {
return db.UserRoles
.Include(static ur => ur.Role)
.GroupBy(static ur => ur.UserGuid, static ur => ur.Role)
.ToDictionaryAsync(static group => group.Key, static group => group.ToImmutableArray());
}
public Task<ImmutableArray<RoleEntity>> GetUserRoles(UserEntity user) {
return db.UserRoles
.Include(static ur => ur.Role)
.Where(ur => ur.UserGuid == user.UserGuid)
.Select(static ur => ur.Role)
.AsAsyncEnumerable()
.ToImmutableArrayAsync();
}
public Task<ImmutableHashSet<Guid>> GetUserRoleGuids(UserEntity user) {
return db.UserRoles
.Where(ur => ur.UserGuid == user.UserGuid)
.Select(static ur => ur.RoleGuid)
.AsAsyncEnumerable()
.ToImmutableSetAsync();
}
public async Task<bool> Add(UserEntity user, RoleEntity role) {
try {
var userRole = await db.UserRoles.FindAsync(user.UserGuid, role.RoleGuid);
if (userRole == null) {
userRole = new UserRoleEntity(user.UserGuid, role.RoleGuid);
db.UserRoles.Add(userRole);
await db.SaveChangesAsync();
}
Logger.Information("Added user \"{UserName}\" (GUID {UserGuid}) to role \"{RoleName}\" (GUID {RoleGuid}).", user.Name, user.UserGuid, role.Name, role.RoleGuid);
return true;
} catch (Exception e) {
Logger.Error(e, "Could not add user \"{UserName}\" (GUID {UserGuid}) to role \"{RoleName}\" (GUID {RoleGuid}).", user.Name, user.UserGuid, role.Name, role.RoleGuid);
return false;
}
}
public async Task<bool> Remove(UserEntity user, RoleEntity role) {
try {
var userRole = await db.UserRoles.FindAsync(user.UserGuid, role.RoleGuid);
if (userRole != null) {
db.UserRoles.Remove(userRole);
await db.SaveChangesAsync();
}
Logger.Information("Removed user \"{UserName}\" (GUID {UserGuid}) from role \"{RoleName}\" (GUID {RoleGuid}).", user.Name, user.UserGuid, role.Name, role.RoleGuid);
return true;
} catch (Exception e) {
Logger.Error(e, "Could not remove user \"{UserName}\" (GUID {UserGuid}) from role \"{RoleName}\" (GUID {RoleGuid}).", user.Name, user.UserGuid, role.Name, role.RoleGuid);
return false;
}
}
}

View File

@ -18,7 +18,6 @@
<ProjectReference Include="..\Phantom.Controller.Minecraft\Phantom.Controller.Minecraft.csproj" /> <ProjectReference Include="..\Phantom.Controller.Minecraft\Phantom.Controller.Minecraft.csproj" />
<ProjectReference Include="..\Phantom.Controller.Rpc\Phantom.Controller.Rpc.csproj" /> <ProjectReference Include="..\Phantom.Controller.Rpc\Phantom.Controller.Rpc.csproj" />
<ProjectReference Include="..\Phantom.Controller.Services\Phantom.Controller.Services.csproj" /> <ProjectReference Include="..\Phantom.Controller.Services\Phantom.Controller.Services.csproj" />
<ProjectReference Include="..\..\Web\Phantom.Web\Phantom.Web.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,24 +1,19 @@
using System.Reflection; using System.Reflection;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Phantom.Common.Logging; using Phantom.Common.Logging;
using Phantom.Controller; using Phantom.Controller;
using Phantom.Controller.Database.Postgres; using Phantom.Controller.Database.Postgres;
using Phantom.Controller.Rpc; using Phantom.Controller.Rpc;
using Phantom.Controller.Services; using Phantom.Controller.Services;
using Phantom.Controller.Services.Rpc;
using Phantom.Utils.Cryptography;
using Phantom.Utils.IO; using Phantom.Utils.IO;
using Phantom.Utils.Rpc; using Phantom.Utils.Rpc;
using Phantom.Utils.Runtime; using Phantom.Utils.Runtime;
using Phantom.Utils.Tasks; using Phantom.Utils.Tasks;
using WebConfiguration = Phantom.Web.Configuration;
using WebLauncher = Phantom.Web.Launcher;
var cancellationTokenSource = new CancellationTokenSource(); var shutdownCancellationTokenSource = new CancellationTokenSource();
var shutdownCancellationToken = shutdownCancellationTokenSource.Token;
PosixSignals.RegisterCancellation(cancellationTokenSource, static () => { PosixSignals.RegisterCancellation(shutdownCancellationTokenSource, static () => {
PhantomLogger.Root.InformationHeading("Stopping Phantom Panel server..."); PhantomLogger.Root.InformationHeading("Stopping Phantom Panel controller...");
}); });
static void CreateFolderOrStop(string path, UnixFileMode chmod) { static void CreateFolderOrStop(string path, UnixFileMode chmod) {
@ -35,48 +30,33 @@ static void CreateFolderOrStop(string path, UnixFileMode chmod) {
try { try {
var fullVersion = AssemblyAttributes.GetFullVersion(Assembly.GetExecutingAssembly()); var fullVersion = AssemblyAttributes.GetFullVersion(Assembly.GetExecutingAssembly());
PhantomLogger.Root.InformationHeading("Initializing Phantom Panel server..."); PhantomLogger.Root.InformationHeading("Initializing Phantom Panel controller...");
PhantomLogger.Root.Information("Server version: {Version}", fullVersion); PhantomLogger.Root.Information("Controller version: {Version}", fullVersion);
var (webServerHost, webServerPort, webBasePath, rpcServerHost, rpcServerPort, sqlConnectionString) = Variables.LoadOrStop(); var (rpcServerHost, rpcServerPort, sqlConnectionString) = Variables.LoadOrStop();
string secretsPath = Path.GetFullPath("./secrets"); string secretsPath = Path.GetFullPath("./secrets");
CreateFolderOrStop(secretsPath, Chmod.URWX_GRX); CreateFolderOrStop(secretsPath, Chmod.URWX_GRX);
string webKeysPath = Path.GetFullPath("./keys");
CreateFolderOrStop(webKeysPath, Chmod.URWX);
var certificateData = await CertificateFiles.CreateOrLoad(secretsPath); var certificateData = await CertificateFiles.CreateOrLoad(secretsPath);
if (certificateData == null) { if (certificateData == null) {
return 1; return 1;
} }
var (certificate, agentToken) = certificateData.Value; var (certificate, agentAuthToken) = certificateData.Value;
var dbContextFactory = new ApplicationDbContextFactory(sqlConnectionString);
var controllerServices = new ControllerServices(dbContextFactory, agentAuthToken, shutdownCancellationToken);
PhantomLogger.Root.InformationHeading("Launching Phantom Panel server..."); PhantomLogger.Root.InformationHeading("Launching Phantom Panel server...");
var taskManager = new TaskManager(PhantomLogger.Create<TaskManager>("Server")); await controllerServices.Initialize();
var rpcConfiguration = new RpcConfiguration(PhantomLogger.Create("Rpc"), PhantomLogger.Create<TaskManager>("Rpc"), rpcServerHost, rpcServerPort, certificate);
var rpcTask = RpcLauncher.Launch(rpcConfiguration, controllerServices.CreateMessageToServerListener, shutdownCancellationToken);
try { try {
var rpcConfiguration = new RpcConfiguration(PhantomLogger.Create("Rpc"), PhantomLogger.Create<TaskManager>("Rpc"), rpcServerHost, rpcServerPort, certificate); await rpcTask.WaitAsync(shutdownCancellationToken);
var webConfiguration = new WebConfiguration(PhantomLogger.Create("Web"), webServerHost, webServerPort, webBasePath, webKeysPath, cancellationTokenSource.Token);
var administratorToken = TokenGenerator.Create(60);
PhantomLogger.Root.Information("Your administrator token is: {AdministratorToken}", administratorToken);
PhantomLogger.Root.Information("For administrator setup, visit: {HttpUrl}{SetupPath}", webConfiguration.HttpUrl, webConfiguration.BasePath + "setup");
var serviceConfiguration = new ServiceConfiguration(fullVersion, TokenGenerator.GetBytesOrThrow(administratorToken), cancellationTokenSource.Token);
var webConfigurator = new WebConfigurator(serviceConfiguration, taskManager, agentToken);
var webApplication = await WebLauncher.CreateApplication(webConfiguration, webConfigurator, options => options.UseNpgsql(sqlConnectionString, static options => {
options.CommandTimeout(10).MigrationsAssembly(typeof(ApplicationDbContextDesignFactory).Assembly.FullName);
}));
await Task.WhenAll(
RpcLauncher.Launch(rpcConfiguration, webApplication.Services.GetRequiredService<MessageToServerListenerFactory>().CreateListener, cancellationTokenSource.Token),
WebLauncher.Launch(webConfiguration, webApplication)
);
} finally { } finally {
cancellationTokenSource.Cancel(); await rpcTask;
await taskManager.Stop();
} }
return 0; return 0;
@ -88,7 +68,8 @@ try {
PhantomLogger.Root.Fatal(e, "Caught exception in entry point."); PhantomLogger.Root.Fatal(e, "Caught exception in entry point.");
return 1; return 1;
} finally { } finally {
cancellationTokenSource.Dispose(); shutdownCancellationTokenSource.Dispose();
PhantomLogger.Root.Information("Bye!"); PhantomLogger.Root.Information("Bye!");
PhantomLogger.Dispose(); PhantomLogger.Dispose();
} }

View File

@ -5,9 +5,6 @@ using Phantom.Utils.Runtime;
namespace Phantom.Controller; namespace Phantom.Controller;
sealed record Variables( sealed record Variables(
string WebServerHost,
ushort WebServerPort,
string WebBasePath,
string RpcServerHost, string RpcServerHost,
ushort RpcServerPort, ushort RpcServerPort,
string SqlConnectionString string SqlConnectionString
@ -22,9 +19,6 @@ sealed record Variables(
}; };
return new Variables( return new Variables(
EnvironmentVariables.GetString("WEB_SERVER_HOST").WithDefault("0.0.0.0"),
EnvironmentVariables.GetPortNumber("WEB_SERVER_PORT").WithDefault(9400),
EnvironmentVariables.GetString("WEB_BASE_PATH").Validate(static value => value.StartsWith('/') && value.EndsWith('/'), "Environment variable must begin and end with '/'").WithDefault("/"),
EnvironmentVariables.GetString("RPC_SERVER_HOST").WithDefault("0.0.0.0"), EnvironmentVariables.GetString("RPC_SERVER_HOST").WithDefault("0.0.0.0"),
EnvironmentVariables.GetPortNumber("RPC_SERVER_PORT").WithDefault(9401), EnvironmentVariables.GetPortNumber("RPC_SERVER_PORT").WithDefault(9401),
connectionStringBuilder.ToString() connectionStringBuilder.ToString()

View File

@ -1,45 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
using Phantom.Common.Data.Agent;
using Phantom.Controller.Minecraft;
using Phantom.Controller.Services;
using Phantom.Controller.Services.Agents;
using Phantom.Controller.Services.Audit;
using Phantom.Controller.Services.Events;
using Phantom.Controller.Services.Instances;
using Phantom.Controller.Services.Rpc;
using Phantom.Utils.Tasks;
using WebLauncher = Phantom.Web.Launcher;
namespace Phantom.Controller;
sealed class WebConfigurator : WebLauncher.IConfigurator {
private readonly ServiceConfiguration serviceConfiguration;
private readonly TaskManager taskManager;
private readonly AgentAuthToken agentToken;
public WebConfigurator(ServiceConfiguration serviceConfiguration, TaskManager taskManager, AgentAuthToken agentToken) {
this.serviceConfiguration = serviceConfiguration;
this.taskManager = taskManager;
this.agentToken = agentToken;
}
public void ConfigureServices(IServiceCollection services) {
services.AddSingleton(serviceConfiguration);
services.AddSingleton(taskManager);
services.AddSingleton(agentToken);
services.AddSingleton<AgentManager>();
services.AddSingleton<AgentJavaRuntimesManager>();
services.AddSingleton<EventLog>();
services.AddSingleton<InstanceManager>();
services.AddSingleton<InstanceLogManager>();
services.AddSingleton<MinecraftVersions>();
services.AddSingleton<MessageToServerListenerFactory>();
services.AddScoped<AuditLog>();
}
public async Task LoadFromDatabase(IServiceProvider serviceProvider) {
await serviceProvider.GetRequiredService<AgentManager>().Initialize();
await serviceProvider.GetRequiredService<InstanceManager>().Initialize();
}
}

View File

@ -5,7 +5,7 @@ services:
image: postgres:14 image: postgres:14
container_name: "phantom-panel-postgres" container_name: "phantom-panel-postgres"
ports: ports:
- "127.0.0.1:9402:5432" - "127.0.0.1:9403:5432"
volumes: volumes:
- postgres:/var/lib/postgresql/data - postgres:/var/lib/postgresql/data
environment: environment:

View File

@ -7,6 +7,7 @@ ARG TARGETARCH
ADD . /app ADD . /app
WORKDIR /app WORKDIR /app
RUN mkdir /data && chmod 777 /data
RUN dotnet restore --arch "$TARGETARCH" RUN dotnet restore --arch "$TARGETARCH"
@ -24,25 +25,31 @@ RUN dotnet publish Agent/Phantom.Agent/Phantom.Agent.csproj \
--output /app/out --output /app/out
# +----------------------+ # +--------------------------+
# | Build Phantom Server | # | Build Phantom Controller |
# +----------------------+ # +--------------------------+
FROM phantom-base-builder AS phantom-server-builder FROM phantom-base-builder AS phantom-controller-builder
RUN dotnet publish Web/Phantom.Web/Phantom.Web.csproj \
/p:DebugType=None \
/p:DebugSymbols=false \
--no-restore \
--arch "$TARGETARCH" \
--configuration Release \
--output /app/out
RUN dotnet publish Controller/Phantom.Controller/Phantom.Controller.csproj \ RUN dotnet publish Controller/Phantom.Controller/Phantom.Controller.csproj \
/p:DebugType=None \ /p:DebugType=None \
/p:DebugSymbols=false \ /p:DebugSymbols=false \
--no-restore \ --no-restore \
--arch "$TARGETARCH" \ --arch "$TARGETARCH" \
--configuration Release \ --configuration Release \
--output /app/out
# +-------------------+
# | Build Phantom Web |
# +-------------------+
FROM phantom-base-builder AS phantom-controller-builder
RUN dotnet publish Web/Phantom.Web/Phantom.Web.csproj \
/p:DebugType=None \
/p:DebugSymbols=false \
--no-restore \
--arch "$TARGETARCH" \
--configuration Release \
--output /app/out --output /app/out
@ -51,7 +58,6 @@ RUN dotnet publish Controller/Phantom.Controller/Phantom.Controller.csproj \
# +------------------------------+ # +------------------------------+
FROM mcr.microsoft.com/dotnet/nightly/runtime:8.0-preview AS phantom-agent FROM mcr.microsoft.com/dotnet/nightly/runtime:8.0-preview AS phantom-agent
RUN mkdir /data && chmod 777 /data
WORKDIR /data WORKDIR /data
COPY --from=eclipse-temurin:8-jre /opt/java/openjdk /opt/java/8 COPY --from=eclipse-temurin:8-jre /opt/java/openjdk /opt/java/8
@ -73,14 +79,25 @@ COPY --from=phantom-agent-builder --chmod=755 /app/out /app
ENTRYPOINT ["dotnet", "/app/Phantom.Agent.dll"] ENTRYPOINT ["dotnet", "/app/Phantom.Agent.dll"]
# +-------------------------------+ # +-----------------------------------+
# | Finalize Phantom Server image | # | Finalize Phantom Controller image |
# +-------------------------------+ # +-----------------------------------+
FROM mcr.microsoft.com/dotnet/nightly/aspnet:8.0-preview AS phantom-server FROM mcr.microsoft.com/dotnet/nightly/runtime:8.0-preview AS phantom-controller
RUN mkdir /data && chmod 777 /data
WORKDIR /data WORKDIR /data
COPY --from=phantom-server-builder --chmod=755 /app/out /app COPY --from=phantom-controller-builder --chmod=755 /app/out /app
ENTRYPOINT ["dotnet", "/app/Phantom.Controller.dll"] ENTRYPOINT ["dotnet", "/app/Phantom.Controller.dll"]
# +----------------------------+
# | Finalize Phantom Web image |
# +----------------------------+
FROM mcr.microsoft.com/dotnet/nightly/aspnet:8.0-preview AS phantom-web
WORKDIR /data
COPY --from=phantom-web-builder --chmod=755 /app/out /app
ENTRYPOINT ["dotnet", "/app/Phantom.Web.dll"]

View File

@ -1,4 +1,4 @@
namespace Phantom.Web.Components.Utils; namespace Phantom.Utils.Tasks;
public sealed class Throttler { public sealed class Throttler {
private readonly TimeSpan interval; private readonly TimeSpan interval;

View File

@ -1,40 +0,0 @@
using System.Security.Claims;
using Phantom.Controller.Database;
using Phantom.Controller.Services.Users;
using Phantom.Web.Identity.Data;
namespace Phantom.Web.Identity.Authorization;
public sealed class PermissionManager {
private readonly DatabaseProvider databaseProvider;
private readonly Dictionary<Guid, IdentityPermissions> userIdsToPermissionIds = new ();
public PermissionManager(DatabaseProvider databaseProvider) {
this.databaseProvider = databaseProvider;
}
private IdentityPermissions FetchPermissionsForUserId(Guid userId) {
using var scope = databaseProvider.CreateScope();
var userPermissions = scope.Ctx.UserPermissions.Where(up => up.UserGuid == userId).Select(static up => up.PermissionId);
var rolePermissions = scope.Ctx.UserRoles.Where(ur => ur.UserGuid == userId).Join(scope.Ctx.RolePermissions, static ur => ur.RoleGuid, static rp => rp.RoleGuid, static (ur, rp) => rp.PermissionId);
return new IdentityPermissions(userPermissions.Union(rolePermissions));
}
private IdentityPermissions GetPermissionsForUserId(Guid userId, bool refreshCache) {
if (!refreshCache && userIdsToPermissionIds.TryGetValue(userId, out var userPermissions)) {
return userPermissions;
}
else {
return userIdsToPermissionIds[userId] = FetchPermissionsForUserId(userId);
}
}
public IdentityPermissions GetPermissions(ClaimsPrincipal user, bool refreshCache = false) {
Guid? userId = UserManager.GetAuthenticatedUserId(user);
return userId == null ? IdentityPermissions.None : GetPermissionsForUserId(userId.Value, refreshCache);
}
public bool CheckPermission(ClaimsPrincipal user, Permission permission, bool refreshCache = false) {
return GetPermissions(user, refreshCache).Check(permission);
}
}

View File

@ -20,8 +20,6 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Common\Phantom.Common.Logging\Phantom.Common.Logging.csproj" /> <ProjectReference Include="..\..\Common\Phantom.Common.Logging\Phantom.Common.Logging.csproj" />
<ProjectReference Include="..\..\Controller\Phantom.Controller.Database\Phantom.Controller.Database.csproj" />
<ProjectReference Include="..\..\Controller\Phantom.Controller.Services\Phantom.Controller.Services.csproj" />
<ProjectReference Include="..\..\Utils\Phantom.Utils\Phantom.Utils.csproj" /> <ProjectReference Include="..\..\Utils\Phantom.Utils\Phantom.Utils.csproj" />
</ItemGroup> </ItemGroup>

View File

@ -1,8 +1,5 @@
using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.DataProtection;
using Microsoft.EntityFrameworkCore;
using Phantom.Controller.Database;
using Phantom.Web.Base; using Phantom.Web.Base;
using Phantom.Web.Components.Utils;
using Phantom.Web.Identity; using Phantom.Web.Identity;
using Phantom.Web.Identity.Interfaces; using Phantom.Web.Identity.Interfaces;
using Serilog; using Serilog;
@ -83,24 +80,6 @@ public static class Launcher {
} }
} }
private static async Task MigrateDatabase(Configuration config, DatabaseProvider databaseProvider) {
var logger = config.Logger;
using var scope = databaseProvider.CreateScope();
var database = scope.Ctx.Database;
logger.Information("Connecting to database...");
var retryConnection = new Throttler(TimeSpan.FromSeconds(10));
while (!await database.CanConnectAsync(config.CancellationToken)) {
logger.Warning("Cannot connect to database, retrying...");
await retryConnection.Wait();
}
logger.Information("Running database migrations...");
await database.MigrateAsync(); // Do not allow cancellation.
}
public interface IConfigurator { public interface IConfigurator {
void ConfigureServices(IServiceCollection services); void ConfigureServices(IServiceCollection services);
Task LoadFromDatabase(IServiceProvider serviceProvider); Task LoadFromDatabase(IServiceProvider serviceProvider);