mirror of
https://github.com/chylex/Minecraft-Phantom-Panel.git
synced 2024-11-25 16:42:54 +01:00
Compare commits
1 Commits
340b236282
...
55d8c6bcfc
Author | SHA1 | Date | |
---|---|---|---|
55d8c6bcfc |
@ -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
1
.workdir/Controller/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
25
Controller/Phantom.Controller.Database/DatabaseMigrator.cs
Normal file
25
Controller/Phantom.Controller.Database/DatabaseMigrator.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,5 @@
|
|||||||
|
namespace Phantom.Controller.Database;
|
||||||
|
|
||||||
|
public interface IDatabaseProvider {
|
||||||
|
ApplicationDbContext Provide();
|
||||||
|
}
|
@ -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>
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
65
Controller/Phantom.Controller.Services/ControllerServices.cs
Normal file
65
Controller/Phantom.Controller.Services/ControllerServices.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
@ -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) {
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 ();
|
@ -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 ();
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,
|
@ -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 ();
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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>
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -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:
|
||||||
|
65
Dockerfile
65
Dockerfile
@ -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"]
|
||||||
|
@ -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;
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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>
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
Loading…
Reference in New Issue
Block a user