mirror of
https://github.com/chylex/Minecraft-Phantom-Panel.git
synced 2024-11-25 16:42:54 +01:00
Compare commits
3 Commits
0a29b6f21e
...
a6bdc6db12
Author | SHA1 | Date | |
---|---|---|---|
a6bdc6db12 | |||
b2c16279c4 | |||
f1fa90e4d8 |
@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MemoryPack" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
@ -1,28 +1,25 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace Phantom.Controller.Services.Users;
|
||||
namespace Phantom.Common.Data.Web.Users;
|
||||
|
||||
public abstract record AddUserError {
|
||||
private AddUserError() {}
|
||||
|
||||
public sealed record NameIsEmpty : AddUserError;
|
||||
|
||||
public sealed record NameIsTooLong(int MaximumLength) : AddUserError;
|
||||
|
||||
public sealed record NameAlreadyExists : AddUserError;
|
||||
public sealed record NameIsInvalid(UsernameRequirementViolation Violation) : AddUserError;
|
||||
|
||||
public sealed record PasswordIsInvalid(ImmutableArray<PasswordRequirementViolation> Violations) : AddUserError;
|
||||
|
||||
public sealed record NameAlreadyExists : AddUserError;
|
||||
|
||||
public sealed record UnknownError : AddUserError;
|
||||
}
|
||||
|
||||
public static class AddUserErrorExtensions {
|
||||
public static string ToSentences(this AddUserError error, string delimiter) {
|
||||
return error switch {
|
||||
AddUserError.NameIsEmpty => "Name cannot be empty.",
|
||||
AddUserError.NameIsTooLong e => "Name cannot be longer than " + e.MaximumLength + " character(s).",
|
||||
AddUserError.NameAlreadyExists => "Name is already occupied.",
|
||||
AddUserError.NameIsInvalid e => e.Violation.ToSentence(),
|
||||
AddUserError.PasswordIsInvalid e => string.Join(delimiter, e.Violations.Select(static v => v.ToSentence())),
|
||||
AddUserError.NameAlreadyExists => "Username is already occupied.",
|
||||
_ => "Unknown error."
|
||||
};
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
using MemoryPack;
|
||||
|
||||
namespace Phantom.Common.Data.Web.Users;
|
||||
|
||||
[MemoryPackable]
|
||||
[MemoryPackUnion(0, typeof(Success))]
|
||||
[MemoryPackUnion(1, typeof(CreationFailed))]
|
||||
[MemoryPackUnion(2, typeof(UpdatingFailed))]
|
||||
[MemoryPackUnion(3, typeof(AddingToRoleFailed))]
|
||||
public partial interface ICreateOrUpdateAdministratorUserResult {
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record Success(UserInfo User) : ICreateOrUpdateAdministratorUserResult;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record CreationFailed(AddUserError Error) : ICreateOrUpdateAdministratorUserResult;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record UpdatingFailed(SetUserPasswordError Error) : ICreateOrUpdateAdministratorUserResult;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record AddingToRoleFailed : ICreateOrUpdateAdministratorUserResult;
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
namespace Phantom.Controller.Services.Users;
|
||||
namespace Phantom.Common.Data.Web.Users;
|
||||
|
||||
public abstract record PasswordRequirementViolation {
|
||||
private PasswordRequirementViolation() {}
|
@ -1,4 +1,4 @@
|
||||
namespace Phantom.Controller.Services.Users.Permissions;
|
||||
namespace Phantom.Common.Data.Web.Users.Permissions;
|
||||
|
||||
public sealed record Permission(string Id, Permission? Parent) {
|
||||
private static readonly List<Permission> AllPermissions = new ();
|
9
Common/Phantom.Common.Data.Web/Users/RoleInfo.cs
Normal file
9
Common/Phantom.Common.Data.Web/Users/RoleInfo.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using MemoryPack;
|
||||
|
||||
namespace Phantom.Common.Data.Web.Users;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record RoleInfo(
|
||||
[property: MemoryPackOrder(0)] Guid Guid,
|
||||
[property: MemoryPackOrder(1)] string Name
|
||||
);
|
@ -1,6 +1,6 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace Phantom.Controller.Services.Users;
|
||||
namespace Phantom.Common.Data.Web.Users;
|
||||
|
||||
public abstract record SetUserPasswordError {
|
||||
private SetUserPasswordError() {}
|
9
Common/Phantom.Common.Data.Web/Users/UserInfo.cs
Normal file
9
Common/Phantom.Common.Data.Web/Users/UserInfo.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using MemoryPack;
|
||||
|
||||
namespace Phantom.Common.Data.Web.Users;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record UserInfo(
|
||||
[property: MemoryPackOrder(0)] Guid Guid,
|
||||
[property: MemoryPackOrder(1)] string Name
|
||||
);
|
@ -0,0 +1,19 @@
|
||||
namespace Phantom.Common.Data.Web.Users;
|
||||
|
||||
public abstract record UsernameRequirementViolation {
|
||||
private UsernameRequirementViolation() {}
|
||||
|
||||
public sealed record IsEmpty : UsernameRequirementViolation;
|
||||
|
||||
public sealed record TooLong(int MaxLength) : UsernameRequirementViolation;
|
||||
}
|
||||
|
||||
public static class UsernameRequirementViolationExtensions {
|
||||
public static string ToSentence(this UsernameRequirementViolation violation) {
|
||||
return violation switch {
|
||||
UsernameRequirementViolation.IsEmpty => "Username must not be empty.",
|
||||
UsernameRequirementViolation.TooLong v => "Username must not be longer than " + v.MaxLength + " character(s).",
|
||||
_ => "Unknown error."
|
||||
};
|
||||
}
|
||||
}
|
@ -1,8 +1,11 @@
|
||||
using Phantom.Common.Messages.Web.BiDirectional;
|
||||
using Phantom.Common.Data.Web.Users;
|
||||
using Phantom.Common.Messages.Web.BiDirectional;
|
||||
using Phantom.Common.Messages.Web.ToController;
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
|
||||
namespace Phantom.Common.Messages.Web;
|
||||
|
||||
public interface IMessageToControllerListener {
|
||||
Task<ICreateOrUpdateAdministratorUserResult> CreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUser message);
|
||||
Task<NoReply> HandleReply(ReplyMessage message);
|
||||
}
|
||||
|
@ -8,6 +8,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Phantom.Common.Logging\Phantom.Common.Logging.csproj" />
|
||||
<ProjectReference Include="..\Phantom.Common.Data\Phantom.Common.Data.csproj" />
|
||||
<ProjectReference Include="..\Phantom.Common.Data.Web\Phantom.Common.Data.Web.csproj" />
|
||||
<ProjectReference Include="..\..\Utils\Phantom.Utils.Rpc\Phantom.Utils.Rpc.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
@ -0,0 +1,14 @@
|
||||
using MemoryPack;
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
|
||||
namespace Phantom.Common.Messages.Web.ToController;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record CreateOrUpdateAdministratorUser(
|
||||
[property: MemoryPackOrder(0)] string Username,
|
||||
[property: MemoryPackOrder(1)] string Password
|
||||
) : IMessageToController {
|
||||
public Task<NoReply> Accept(IMessageToControllerListener listener) {
|
||||
return listener.CreateOrUpdateAdministratorUser(this);
|
||||
}
|
||||
}
|
@ -1,19 +1,21 @@
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Common.Messages.Web.BiDirectional;
|
||||
using Phantom.Common.Messages.Web.ToController;
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
|
||||
namespace Phantom.Common.Messages.Web;
|
||||
|
||||
public static class WebMessageRegistries {
|
||||
public static MessageRegistry<IMessageToWebListener> ToWeb { get; } = new (PhantomLogger.Create("MessageRegistry", nameof(ToWeb)));
|
||||
public static MessageRegistry<IMessageToControllerListener> ToController { get; } = new (PhantomLogger.Create("MessageRegistry", nameof(ToController)));
|
||||
public static MessageRegistry<IMessageToWebListener> ToWeb { get; } = new (PhantomLogger.Create("MessageRegistry", nameof(ToWeb)));
|
||||
|
||||
public static IMessageDefinitions<IMessageToWebListener, IMessageToControllerListener> Definitions { get; } = new MessageDefinitions();
|
||||
|
||||
static WebMessageRegistries() {
|
||||
ToWeb.Add<ReplyMessage>(127);
|
||||
|
||||
ToController.Add<CreateOrUpdateAdministratorUser>(1);
|
||||
ToController.Add<ReplyMessage>(127);
|
||||
|
||||
ToWeb.Add<ReplyMessage>(127);
|
||||
}
|
||||
|
||||
private sealed class MessageDefinitions : IMessageDefinitions<IMessageToWebListener, IMessageToControllerListener> {
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using Phantom.Common.Data.Web.Users;
|
||||
|
||||
namespace Phantom.Controller.Database.Entities;
|
||||
|
||||
@ -16,4 +17,8 @@ public sealed class UserEntity {
|
||||
Name = name;
|
||||
PasswordHash = null!;
|
||||
}
|
||||
|
||||
public UserInfo ToUserInfo() {
|
||||
return new UserInfo(UserGuid, Name);
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Common\Phantom.Common.Data\Phantom.Common.Data.csproj" />
|
||||
<ProjectReference Include="..\..\Common\Phantom.Common.Data.Web\Phantom.Common.Data.Web.csproj" />
|
||||
<ProjectReference Include="..\..\Common\Phantom.Common.Logging\Phantom.Common.Logging.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Phantom.Controller.Database;
|
||||
using Phantom.Controller.Database.Entities;
|
||||
using Phantom.Controller.Database.Enums;
|
||||
@ -10,13 +9,11 @@ namespace Phantom.Controller.Services.Audit;
|
||||
|
||||
public sealed partial class AuditLog {
|
||||
private readonly IDatabaseProvider databaseProvider;
|
||||
private readonly AuthenticationStateProvider authenticationStateProvider;
|
||||
private readonly TaskManager taskManager;
|
||||
private readonly CancellationToken cancellationToken;
|
||||
|
||||
public AuditLog(IDatabaseProvider databaseProvider, AuthenticationStateProvider authenticationStateProvider, TaskManager taskManager, CancellationToken cancellationToken) {
|
||||
public AuditLog(IDatabaseProvider databaseProvider, TaskManager taskManager, CancellationToken cancellationToken) {
|
||||
this.databaseProvider = databaseProvider;
|
||||
this.authenticationStateProvider = authenticationStateProvider;
|
||||
this.taskManager = taskManager;
|
||||
this.cancellationToken = cancellationToken;
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ using Phantom.Controller.Database;
|
||||
using Phantom.Controller.Minecraft;
|
||||
using Phantom.Controller.Rpc;
|
||||
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;
|
||||
@ -20,6 +21,8 @@ public sealed class ControllerServices {
|
||||
private TaskManager TaskManager { get; }
|
||||
private MinecraftVersions MinecraftVersions { get; }
|
||||
|
||||
private AuditLog AuditLog { get; }
|
||||
|
||||
private AgentManager AgentManager { get; }
|
||||
private AgentJavaRuntimesManager AgentJavaRuntimesManager { get; }
|
||||
private EventLog EventLog { get; }
|
||||
@ -38,13 +41,15 @@ public sealed class ControllerServices {
|
||||
this.TaskManager = new TaskManager(PhantomLogger.Create<TaskManager, ControllerServices>());
|
||||
this.MinecraftVersions = new MinecraftVersions();
|
||||
|
||||
this.AuditLog = new AuditLog(databaseProvider, TaskManager, shutdownCancellationToken);
|
||||
|
||||
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.UserManager = new UserManager(databaseProvider, AuditLog);
|
||||
this.RoleManager = new RoleManager(databaseProvider);
|
||||
this.UserRoleManager = new UserRoleManager(databaseProvider);
|
||||
this.PermissionManager = new PermissionManager(databaseProvider);
|
||||
@ -58,7 +63,7 @@ public sealed class ControllerServices {
|
||||
}
|
||||
|
||||
public WebMessageListener CreateWebMessageListener(RpcClientConnection<IMessageToWebListener> connection) {
|
||||
return new WebMessageListener(connection);
|
||||
return new WebMessageListener(connection, AuditLog, UserManager, RoleManager, UserRoleManager);
|
||||
}
|
||||
|
||||
public async Task Initialize() {
|
||||
|
@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
@ -1,15 +1,64 @@
|
||||
using Phantom.Common.Messages.Web;
|
||||
using Phantom.Common.Messages.Web.BiDirectional;
|
||||
using Phantom.Common.Messages.Web.ToController;
|
||||
using Phantom.Controller.Database.Entities;
|
||||
using Phantom.Controller.Rpc;
|
||||
using Phantom.Controller.Services.Audit;
|
||||
using Phantom.Controller.Services.Users;
|
||||
using Phantom.Controller.Services.Users.Roles;
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
using Phantom.Utils.Tasks;
|
||||
|
||||
namespace Phantom.Controller.Services.Rpc;
|
||||
|
||||
public sealed class WebMessageListener : IMessageToControllerListener {
|
||||
private readonly RpcClientConnection<IMessageToWebListener> connection;
|
||||
private readonly AuditLog auditLog;
|
||||
private readonly UserManager userManager;
|
||||
private readonly RoleManager roleManager;
|
||||
private readonly UserRoleManager userRoleManager;
|
||||
|
||||
internal WebMessageListener(RpcClientConnection<IMessageToWebListener> connection) {
|
||||
internal WebMessageListener(RpcClientConnection<IMessageToWebListener> connection, AuditLog auditLog, UserManager userManager, RoleManager roleManager, UserRoleManager userRoleManager) {
|
||||
this.connection = connection;
|
||||
this.auditLog = auditLog;
|
||||
this.userManager = userManager;
|
||||
this.roleManager = roleManager;
|
||||
this.userRoleManager = userRoleManager;
|
||||
}
|
||||
|
||||
public async Task<ICreateOrUpdateAdministratorUserResult> CreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUser message) {
|
||||
UserEntity administratorUser = null!;
|
||||
|
||||
var existingUser = await userManager.GetByName(message.Username);
|
||||
if (existingUser == null) {
|
||||
var result = await userManager.CreateUser(message.Username, message.Password);
|
||||
switch (result) {
|
||||
case Result<UserEntity, AddUserError>.Ok ok:
|
||||
administratorUser = ok.Value;
|
||||
await auditLog.AddAdministratorUserCreatedEvent(administratorUser);
|
||||
break;
|
||||
|
||||
case Result<UserEntity, AddUserError>.Fail fail:
|
||||
return new ICreateOrUpdateAdministratorUserResult.CreationFailed(fail.Error);
|
||||
}
|
||||
}
|
||||
else {
|
||||
var result = await userManager.SetUserPassword(existingUser.UserGuid, message.Password);
|
||||
if (result is Result<SetUserPasswordError>.Fail fail) {
|
||||
return new ICreateOrUpdateAdministratorUserResult.UpdatingFailed(fail.Error);
|
||||
}
|
||||
else {
|
||||
administratorUser = existingUser;
|
||||
await auditLog.AddAdministratorUserModifiedEvent(administratorUser);
|
||||
}
|
||||
}
|
||||
|
||||
var administratorRole = await roleManager.GetByGuid(Role.Administrator.Guid);
|
||||
if (administratorRole == null || !await userRoleManager.Add(administratorUser, administratorRole)) {
|
||||
return new ICreateOrUpdateAdministratorUserResult.AddingToRoleFailed();
|
||||
}
|
||||
|
||||
return new ICreateOrUpdateAdministratorUserResult.Success(administratorUser.ToUserInfo());
|
||||
}
|
||||
|
||||
public Task<NoReply> HandleReply(ReplyMessage message) {
|
||||
|
@ -5,7 +5,7 @@ using Phantom.Common.Logging;
|
||||
using Phantom.Controller.Database;
|
||||
using Phantom.Controller.Database.Entities;
|
||||
using Phantom.Utils.Collections;
|
||||
using ILogger = Serilog.ILogger;
|
||||
using Serilog;
|
||||
|
||||
namespace Phantom.Controller.Services.Users.Permissions;
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System.Collections.Immutable;
|
||||
using Phantom.Controller.Services.Users.Permissions;
|
||||
|
||||
namespace Phantom.Controller.Services.Users.Roles;
|
||||
|
||||
|
@ -4,7 +4,7 @@ using Phantom.Common.Logging;
|
||||
using Phantom.Controller.Database;
|
||||
using Phantom.Controller.Database.Entities;
|
||||
using Phantom.Utils.Collections;
|
||||
using ILogger = Serilog.ILogger;
|
||||
using Serilog;
|
||||
|
||||
namespace Phantom.Controller.Services.Users.Roles;
|
||||
|
||||
@ -49,11 +49,13 @@ public sealed class UserRoleManager {
|
||||
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();
|
||||
if (userRole != null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
@ -68,10 +70,12 @@ public sealed class UserRoleManager {
|
||||
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();
|
||||
if (userRole == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
|
@ -1,25 +1,26 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Controller.Database;
|
||||
using Phantom.Controller.Database.Entities;
|
||||
using Phantom.Controller.Services.Audit;
|
||||
using Phantom.Controller.Services.Users.Roles;
|
||||
using Phantom.Utils.Collections;
|
||||
using Phantom.Utils.Tasks;
|
||||
using ILogger = Serilog.ILogger;
|
||||
using Serilog;
|
||||
|
||||
namespace Phantom.Controller.Services.Users;
|
||||
|
||||
public sealed class UserManager {
|
||||
sealed class UserManager {
|
||||
private static readonly ILogger Logger = PhantomLogger.Create<UserManager>();
|
||||
|
||||
private const int MaxUserNameLength = 40;
|
||||
|
||||
private readonly IDatabaseProvider databaseProvider;
|
||||
private readonly AuditLog auditLog;
|
||||
|
||||
public UserManager(IDatabaseProvider databaseProvider) {
|
||||
public UserManager(IDatabaseProvider databaseProvider, AuditLog auditLog) {
|
||||
this.databaseProvider = databaseProvider;
|
||||
this.auditLog = auditLog;
|
||||
}
|
||||
|
||||
public static Guid? GetAuthenticatedUserId(ClaimsPrincipal user) {
|
||||
@ -57,10 +58,10 @@ public sealed class UserManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (UserPasswords.Verify(user, password)) {
|
||||
switch (UserValidation.VerifyPassword(user, password)) {
|
||||
case PasswordVerificationResult.SuccessRehashNeeded:
|
||||
try {
|
||||
UserPasswords.Set(user, password);
|
||||
UserValidation.SetPassword(user, password);
|
||||
await ctx.SaveChangesAsync();
|
||||
} catch (Exception e) {
|
||||
Logger.Warning(e, "Could not rehash password for \"{Username}\".", user.Name);
|
||||
@ -78,39 +79,51 @@ public sealed class UserManager {
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
public async Task<Result<UserEntity, AddUserError>> CreateUser(string username, string password) {
|
||||
if (string.IsNullOrWhiteSpace(username)) {
|
||||
return Result.Fail<UserEntity, AddUserError>(new AddUserError.NameIsEmpty());
|
||||
}
|
||||
else if (username.Length > MaxUserNameLength) {
|
||||
return Result.Fail<UserEntity, AddUserError>(new AddUserError.NameIsTooLong(MaxUserNameLength));
|
||||
public async Task<ICreateOrUpdateAdministratorUserResult> CreateAdministratorUser(string username, string password) {
|
||||
await using var editor = new Editor(databaseProvider);
|
||||
|
||||
var createUserResult = await editor.CreateUser(username, password);
|
||||
if (!createUserResult) {
|
||||
return new ICreateOrUpdateAdministratorUserResult.CreationFailed(createUserResult.Error);
|
||||
}
|
||||
|
||||
var requirementViolations = UserPasswords.CheckRequirements(password);
|
||||
if (!requirementViolations.IsEmpty) {
|
||||
return Result.Fail<UserEntity, AddUserError>(new AddUserError.PasswordIsInvalid(requirementViolations));
|
||||
}
|
||||
UserEntity administratorUser = null!;
|
||||
|
||||
UserEntity newUser;
|
||||
try {
|
||||
await using var ctx = databaseProvider.Provide();
|
||||
var existingUser = await userManager.GetByName(message.Username);
|
||||
if (existingUser == null) {
|
||||
var result = await userManager.CreateUser(message.Username, message.Password);
|
||||
switch (result) {
|
||||
case Result<UserEntity, AddUserError>.Ok ok:
|
||||
administratorUser = ok.Value;
|
||||
await auditLog.AddAdministratorUserCreatedEvent(administratorUser);
|
||||
break;
|
||||
|
||||
if (await ctx.Users.AnyAsync(user => user.Name == username)) {
|
||||
return Result.Fail<UserEntity, AddUserError>(new AddUserError.NameAlreadyExists());
|
||||
case Result<UserEntity, AddUserError>.Fail fail:
|
||||
return new ICreateOrUpdateAdministratorUserResult.CreationFailed(fail.Error);
|
||||
}
|
||||
}
|
||||
else {
|
||||
var result = await userManager.SetUserPassword(existingUser.UserGuid, message.Password);
|
||||
if (result is Result<SetUserPasswordError>.Fail fail) {
|
||||
return new ICreateOrUpdateAdministratorUserResult.UpdatingFailed(fail.Error);
|
||||
}
|
||||
else {
|
||||
administratorUser = existingUser;
|
||||
await auditLog.AddAdministratorUserModifiedEvent(administratorUser);
|
||||
}
|
||||
|
||||
newUser = new UserEntity(Guid.NewGuid(), username);
|
||||
UserPasswords.Set(newUser, password);
|
||||
|
||||
ctx.Users.Add(newUser);
|
||||
await ctx.SaveChangesAsync();
|
||||
} catch (Exception e) {
|
||||
Logger.Error(e, "Could not create user \"{Name}\".", username);
|
||||
return Result.Fail<UserEntity, AddUserError>(new AddUserError.UnknownError());
|
||||
}
|
||||
|
||||
Logger.Information("Created user \"{Name}\" (GUID {Guid}).", username, newUser.UserGuid);
|
||||
return Result.Ok<UserEntity, AddUserError>(newUser);
|
||||
var administratorRole = await roleManager.GetByGuid(Role.Administrator.Guid);
|
||||
if (administratorRole == null || !await userRoleManager.Add(administratorUser, administratorRole)) {
|
||||
return new ICreateOrUpdateAdministratorUserResult.AddingToRoleFailed();
|
||||
}
|
||||
|
||||
return new ICreateOrUpdateAdministratorUserResult.Success(administratorUser.ToUserInfo());
|
||||
}
|
||||
|
||||
public async Task<Result<UserEntity, AddUserError>> CreateUser(string username, string password) {
|
||||
await using var editor = new Editor(databaseProvider);
|
||||
return await editor.CreateUser(username, password);
|
||||
}
|
||||
|
||||
public async Task<Result<SetUserPasswordError>> SetUserPassword(Guid guid, string password) {
|
||||
@ -119,21 +132,21 @@ public sealed class UserManager {
|
||||
await using (var ctx = databaseProvider.Provide()) {
|
||||
var user = await ctx.Users.FindAsync(guid);
|
||||
if (user == null) {
|
||||
return Result.Fail<SetUserPasswordError>(new SetUserPasswordError.UserNotFound());
|
||||
return new SetUserPasswordError.UserNotFound();
|
||||
}
|
||||
|
||||
foundUser = user;
|
||||
try {
|
||||
var requirementViolations = UserPasswords.CheckRequirements(password);
|
||||
var requirementViolations = UserValidation.CheckPasswordRequirements(password);
|
||||
if (!requirementViolations.IsEmpty) {
|
||||
return Result.Fail<SetUserPasswordError>(new SetUserPasswordError.PasswordIsInvalid(requirementViolations));
|
||||
return new SetUserPasswordError.PasswordIsInvalid(requirementViolations);
|
||||
}
|
||||
|
||||
UserPasswords.Set(user, password);
|
||||
UserValidation.SetPassword(user, password);
|
||||
await ctx.SaveChangesAsync();
|
||||
} catch (Exception e) {
|
||||
Logger.Error(e, "Could not change password for user \"{Name}\" (GUID {Guid}).", user.Name, user.UserGuid);
|
||||
return Result.Fail<SetUserPasswordError>(new SetUserPasswordError.UnknownError());
|
||||
return new SetUserPasswordError.UnknownError();
|
||||
}
|
||||
}
|
||||
|
||||
@ -142,19 +155,73 @@ public sealed class UserManager {
|
||||
}
|
||||
|
||||
public async Task<DeleteUserResult> DeleteByGuid(Guid guid) {
|
||||
await using var ctx = databaseProvider.Provide();
|
||||
var user = await ctx.Users.FindAsync(guid);
|
||||
if (user == null) {
|
||||
return DeleteUserResult.NotFound;
|
||||
await using var editor = new Editor(databaseProvider);
|
||||
return await editor.DeleteUserByGuid(guid);
|
||||
}
|
||||
|
||||
private sealed class Editor : IAsyncDisposable {
|
||||
public ApplicationDbContext Ctx => cachedContext ??= databaseProvider.Provide();
|
||||
|
||||
private readonly IDatabaseProvider databaseProvider;
|
||||
private ApplicationDbContext? cachedContext;
|
||||
|
||||
public Editor(IDatabaseProvider databaseProvider) {
|
||||
this.databaseProvider = databaseProvider;
|
||||
}
|
||||
|
||||
try {
|
||||
ctx.Users.Remove(user);
|
||||
await ctx.SaveChangesAsync();
|
||||
return DeleteUserResult.Deleted;
|
||||
} catch (Exception e) {
|
||||
Logger.Error(e, "Could not delete user \"{Name}\" (GUID {Guid}).", user.Name, user.UserGuid);
|
||||
return DeleteUserResult.Failed;
|
||||
public ValueTask DisposeAsync() {
|
||||
return cachedContext?.DisposeAsync() ?? ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<UserEntity?> GetByName(string username) {
|
||||
return Ctx.Users.FirstOrDefaultAsync(user => user.Name == username);
|
||||
}
|
||||
|
||||
public async Task<Result<UserEntity, AddUserError>> CreateUser(string username, string password) {
|
||||
var usernameRequirementViolation = UserValidation.CheckUsernameRequirements(username);
|
||||
if (usernameRequirementViolation != null) {
|
||||
return new AddUserError.NameIsInvalid(usernameRequirementViolation);
|
||||
}
|
||||
|
||||
var passwordRequirementViolations = UserValidation.CheckPasswordRequirements(password);
|
||||
if (!passwordRequirementViolations.IsEmpty) {
|
||||
return new AddUserError.PasswordIsInvalid(passwordRequirementViolations);
|
||||
}
|
||||
|
||||
UserEntity newUser;
|
||||
try {
|
||||
if (await Ctx.Users.AnyAsync(user => user.Name == username)) {
|
||||
return new AddUserError.NameAlreadyExists();
|
||||
}
|
||||
|
||||
newUser = new UserEntity(Guid.NewGuid(), username);
|
||||
UserValidation.SetPassword(newUser, password);
|
||||
|
||||
Ctx.Users.Add(newUser);
|
||||
await Ctx.SaveChangesAsync();
|
||||
} catch (Exception e) {
|
||||
Logger.Error(e, "Could not create user \"{Name}\".", username);
|
||||
return new AddUserError.UnknownError();
|
||||
}
|
||||
|
||||
Logger.Information("Created user \"{Name}\" (GUID {Guid}).", username, newUser.UserGuid);
|
||||
return newUser;
|
||||
}
|
||||
|
||||
public async Task<DeleteUserResult> DeleteUserByGuid(Guid guid) {
|
||||
var user = await Ctx.Users.FindAsync(guid);
|
||||
if (user == null) {
|
||||
return DeleteUserResult.NotFound;
|
||||
}
|
||||
|
||||
try {
|
||||
Ctx.Users.Remove(user);
|
||||
await Ctx.SaveChangesAsync();
|
||||
return DeleteUserResult.Deleted;
|
||||
} catch (Exception e) {
|
||||
Logger.Error(e, "Could not delete user \"{Name}\" (GUID {Guid}).", user.Name, user.UserGuid);
|
||||
return DeleteUserResult.Failed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +1,31 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Phantom.Controller.Database.Entities;
|
||||
|
||||
namespace Phantom.Controller.Services.Users;
|
||||
|
||||
static class UserPasswords {
|
||||
static class UserValidation {
|
||||
private static PasswordHasher<UserEntity> Hasher { get; } = new ();
|
||||
|
||||
private const int MinimumLength = 16;
|
||||
private const int MaxUserNameLength = 40;
|
||||
private const int MinimumPasswordLength = 16;
|
||||
|
||||
public static ImmutableArray<PasswordRequirementViolation> CheckRequirements(string password) {
|
||||
public static UsernameRequirementViolation? CheckUsernameRequirements(string username) {
|
||||
if (string.IsNullOrWhiteSpace(username)) {
|
||||
return new UsernameRequirementViolation.IsEmpty();
|
||||
}
|
||||
else if (username.Length > MaxUserNameLength) {
|
||||
return new UsernameRequirementViolation.TooLong(MaxUserNameLength);
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static ImmutableArray<PasswordRequirementViolation> CheckPasswordRequirements(string password) {
|
||||
var violations = ImmutableArray.CreateBuilder<PasswordRequirementViolation>();
|
||||
|
||||
if (password.Length < MinimumLength) {
|
||||
violations.Add(new PasswordRequirementViolation.TooShort(MinimumLength));
|
||||
if (password.Length < MinimumPasswordLength) {
|
||||
violations.Add(new PasswordRequirementViolation.TooShort(MinimumPasswordLength));
|
||||
}
|
||||
|
||||
if (!password.Any(char.IsLower)) {
|
||||
@ -31,11 +43,11 @@ static class UserPasswords {
|
||||
return violations.ToImmutable();
|
||||
}
|
||||
|
||||
public static void Set(UserEntity user, string password) {
|
||||
public static void SetPassword(UserEntity user, string password) {
|
||||
user.PasswordHash = Hasher.HashPassword(user, password);
|
||||
}
|
||||
|
||||
public static PasswordVerificationResult Verify(UserEntity user, string password) {
|
||||
public static PasswordVerificationResult VerifyPassword(UserEntity user, string password) {
|
||||
return Hasher.VerifyHashedPassword(user, user.PasswordHash, password);
|
||||
}
|
||||
}
|
@ -26,6 +26,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Common.Data", "Comm
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Common.Data.Tests", "Common\Phantom.Common.Data.Tests\Phantom.Common.Data.Tests.csproj", "{435D7981-DFDA-46A0-8CD8-CD8C117935D7}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Common.Data.Web", "Common\Phantom.Common.Data.Web\Phantom.Common.Data.Web.csproj", "{BC969D0B-0019-48E0-9FAF-F5CC906AAF09}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Common.Logging", "Common\Phantom.Common.Logging\Phantom.Common.Logging.csproj", "{D7F55010-B3ED-42A5-8D83-E754FFC5F2A2}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Common.Messages.Agent", "Common\Phantom.Common.Messages.Agent\Phantom.Common.Messages.Agent.csproj", "{95B55357-F8F0-48C2-A1C2-5EA997651783}"
|
||||
@ -58,7 +60,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Web.Bootstrap", "We
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Web.Components", "Web\Phantom.Web.Components\Phantom.Web.Components.csproj", "{3F4F9059-F869-42D3-B92C-90D27ADFC42D}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Web.Identity", "Web\Phantom.Web.Identity\Phantom.Web.Identity.csproj", "{A9870842-FE7A-4760-95DC-9D485DDDA31F}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Web.Services", "Web\Phantom.Web.Services\Phantom.Web.Services.csproj", "{7B0EEE34-A586-4629-AC51-16757DE53261}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@ -90,6 +92,10 @@ Global
|
||||
{435D7981-DFDA-46A0-8CD8-CD8C117935D7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{435D7981-DFDA-46A0-8CD8-CD8C117935D7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{435D7981-DFDA-46A0-8CD8-CD8C117935D7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{BC969D0B-0019-48E0-9FAF-F5CC906AAF09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BC969D0B-0019-48E0-9FAF-F5CC906AAF09}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BC969D0B-0019-48E0-9FAF-F5CC906AAF09}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BC969D0B-0019-48E0-9FAF-F5CC906AAF09}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D7F55010-B3ED-42A5-8D83-E754FFC5F2A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D7F55010-B3ED-42A5-8D83-E754FFC5F2A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D7F55010-B3ED-42A5-8D83-E754FFC5F2A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
@ -150,10 +156,10 @@ Global
|
||||
{3F4F9059-F869-42D3-B92C-90D27ADFC42D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3F4F9059-F869-42D3-B92C-90D27ADFC42D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3F4F9059-F869-42D3-B92C-90D27ADFC42D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A9870842-FE7A-4760-95DC-9D485DDDA31F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A9870842-FE7A-4760-95DC-9D485DDDA31F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A9870842-FE7A-4760-95DC-9D485DDDA31F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A9870842-FE7A-4760-95DC-9D485DDDA31F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{7B0EEE34-A586-4629-AC51-16757DE53261}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7B0EEE34-A586-4629-AC51-16757DE53261}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7B0EEE34-A586-4629-AC51-16757DE53261}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7B0EEE34-A586-4629-AC51-16757DE53261}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{418BE1BF-9F63-4B46-B4E4-DF64C3B3DDA7} = {F5878792-64C8-4ECF-A075-66341FF97127}
|
||||
@ -165,6 +171,7 @@ Global
|
||||
{95B55357-F8F0-48C2-A1C2-5EA997651783} = {01CB1A81-8950-471C-BFDF-F135FDDB2C18}
|
||||
{6E798DEB-8921-41A2-8AFB-E4416A9E0704} = {01CB1A81-8950-471C-BFDF-F135FDDB2C18}
|
||||
{435D7981-DFDA-46A0-8CD8-CD8C117935D7} = {D781E00D-8563-4102-A0CD-477A679193B5}
|
||||
{BC969D0B-0019-48E0-9FAF-F5CC906AAF09} = {01CB1A81-8950-471C-BFDF-F135FDDB2C18}
|
||||
{A0F1C595-96B6-4DBF-8C16-6B99223F8F35} = {0AB9471E-6228-4EB7-802E-3102B3952AAD}
|
||||
{E3AD566F-384A-489A-A3BB-EA3BA400C18C} = {0AB9471E-6228-4EB7-802E-3102B3952AAD}
|
||||
{81625B4A-3DB6-48BD-A739-D23DA02107D1} = {0AB9471E-6228-4EB7-802E-3102B3952AAD}
|
||||
@ -178,6 +185,6 @@ Global
|
||||
{7CA2E5FE-E507-4DC6-930C-E18711A9F856} = {92B26F48-235F-4500-BD55-800F06A0BA39}
|
||||
{83FA86DB-34E4-4C2C-832C-90F491CA10C7} = {92B26F48-235F-4500-BD55-800F06A0BA39}
|
||||
{3F4F9059-F869-42D3-B92C-90D27ADFC42D} = {92B26F48-235F-4500-BD55-800F06A0BA39}
|
||||
{A9870842-FE7A-4760-95DC-9D485DDDA31F} = {92B26F48-235F-4500-BD55-800F06A0BA39}
|
||||
{7B0EEE34-A586-4629-AC51-16757DE53261} = {92B26F48-235F-4500-BD55-800F06A0BA39}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
@ -3,14 +3,43 @@
|
||||
public abstract record Result<TValue, TError> {
|
||||
private Result() {}
|
||||
|
||||
public sealed record Ok(TValue Value) : Result<TValue, TError>;
|
||||
public abstract TValue Value { get; init; }
|
||||
public abstract TError Error { get; init; }
|
||||
|
||||
public sealed record Fail(TError Error) : Result<TValue, TError>;
|
||||
public static implicit operator Result<TValue, TError>(TValue value) {
|
||||
return new Ok(value);
|
||||
}
|
||||
|
||||
public static implicit operator Result<TValue, TError>(TError error) {
|
||||
return new Fail(error);
|
||||
}
|
||||
|
||||
public static implicit operator bool(Result<TValue, TError> result) {
|
||||
return result is Ok;
|
||||
}
|
||||
|
||||
public sealed record Ok(TValue Value) : Result<TValue, TError> {
|
||||
public override TError Error {
|
||||
get => throw new InvalidOperationException("Attempted to get error from Ok result.");
|
||||
init {}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record Fail(TError Error) : Result<TValue, TError> {
|
||||
public override TValue Value {
|
||||
get => throw new InvalidOperationException("Attempted to get value from Fail result.");
|
||||
init {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public abstract record Result<TError> {
|
||||
private Result() {}
|
||||
|
||||
public static implicit operator Result<TError>(TError error) {
|
||||
return new Fail(error);
|
||||
}
|
||||
|
||||
public sealed record Ok : Result<TError> {
|
||||
internal static Ok Instance { get; } = new ();
|
||||
}
|
||||
|
@ -1,6 +0,0 @@
|
||||
namespace Phantom.Web.Identity.Interfaces;
|
||||
|
||||
public interface ILoginEvents {
|
||||
void UserLoggedIn(UserEntity user);
|
||||
void UserLoggedOut(Guid userGuid);
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<SupportedPlatform Include="browser" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Web" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Common\Phantom.Common.Logging\Phantom.Common.Logging.csproj" />
|
||||
<ProjectReference Include="..\..\Utils\Phantom.Utils\Phantom.Utils.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
@ -1,7 +1,6 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Utils.Cryptography;
|
||||
using Phantom.Web.Identity.Interfaces;
|
||||
using ILogger = Serilog.ILogger;
|
@ -1,6 +1,5 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Utils.Tasks;
|
||||
using ILogger = Serilog.ILogger;
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Phantom.Web.Services.Authorization;
|
||||
|
||||
namespace Phantom.Web.Identity.Authorization;
|
||||
|
@ -1,5 +1,6 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Phantom.Common.Data.Web.Users.Permissions;
|
||||
|
||||
namespace Phantom.Web.Identity.Authorization;
|
||||
namespace Phantom.Web.Services.Authorization;
|
||||
|
||||
sealed record PermissionBasedPolicyRequirement(Permission Permission) : IAuthorizationRequirement;
|
11
Web/Phantom.Web.Services/Authorization/PermissionManager.cs
Normal file
11
Web/Phantom.Web.Services/Authorization/PermissionManager.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System.Security.Claims;
|
||||
using Phantom.Common.Data.Web.Users.Permissions;
|
||||
|
||||
namespace Phantom.Web.Services.Authorization;
|
||||
|
||||
public class PermissionManager {
|
||||
// TODO
|
||||
public bool CheckPermission(ClaimsPrincipal user, Permission permission, bool refreshCache = false) {
|
||||
return false;
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using Phantom.Web.Identity.Data
|
||||
@using Phantom.Common.Data.Web.Users.Permissions
|
||||
@inject PermissionManager PermissionManager
|
||||
|
||||
<AuthorizeView>
|
17
Web/Phantom.Web.Services/Phantom.Web.Services.csproj
Normal file
17
Web/Phantom.Web.Services/Phantom.Web.Services.csproj
Normal file
@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Common\Phantom.Common.Data\Phantom.Common.Data.csproj" />
|
||||
<ProjectReference Include="..\..\Common\Phantom.Common.Data.Web\Phantom.Common.Data.Web.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="Authorization\PermissionView.razor" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
@ -2,13 +2,16 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Server;
|
||||
using Phantom.Common.Data.Web.Users.Permissions;
|
||||
using Phantom.Web.Identity;
|
||||
using Phantom.Web.Identity.Authentication;
|
||||
using Phantom.Web.Identity.Authorization;
|
||||
using Phantom.Web.Services.Authorization;
|
||||
|
||||
namespace Phantom.Web.Identity;
|
||||
namespace Phantom.Web.Services;
|
||||
|
||||
public static class PhantomIdentityExtensions {
|
||||
public static void AddPhantomIdentity(this IServiceCollection services, CancellationToken cancellationToken) {
|
||||
public static class PhantomWebServices {
|
||||
public static void AddPhantomServices(this IServiceCollection services, CancellationToken cancellationToken) {
|
||||
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(ConfigureIdentityCookie);
|
||||
services.AddAuthorization(ConfigureAuthorization);
|
||||
|
||||
@ -19,7 +22,7 @@ public static class PhantomIdentityExtensions {
|
||||
services.AddScoped<AuthenticationStateProvider, ServerAuthenticationStateProvider>();
|
||||
}
|
||||
|
||||
public static void UsePhantomIdentity(this IApplicationBuilder application) {
|
||||
public static void UsePhantomServices(this IApplicationBuilder application) {
|
||||
application.UseAuthentication();
|
||||
application.UseAuthorization();
|
||||
application.UseWhen(PhantomIdentityMiddleware.AcceptsPath, static app => app.UseMiddleware<PhantomIdentityMiddleware>());
|
5
Web/Phantom.Web.Services/Users/UserManager.cs
Normal file
5
Web/Phantom.Web.Services/Users/UserManager.cs
Normal file
@ -0,0 +1,5 @@
|
||||
namespace Phantom.Web.Services.Users;
|
||||
|
||||
public sealed class UserManager {
|
||||
|
||||
}
|
@ -1,6 +1,4 @@
|
||||
using Phantom.Web.Identity.Interfaces;
|
||||
|
||||
namespace Phantom.Web.Base;
|
||||
namespace Phantom.Web.Base;
|
||||
|
||||
sealed class LoginEvents : ILoginEvents {
|
||||
private readonly AuditLog auditLog;
|
||||
|
@ -1,5 +1,6 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Phantom.Common.Data.Web.Users.Permissions;
|
||||
using Phantom.Common.Logging;
|
||||
using ILogger = Serilog.ILogger;
|
||||
|
||||
|
@ -1,13 +1,13 @@
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Phantom.Utils.Tasks;
|
||||
using Phantom.Web.Base;
|
||||
using Phantom.Web.Identity;
|
||||
using Phantom.Web.Identity.Interfaces;
|
||||
using Phantom.Web.Services;
|
||||
using Serilog;
|
||||
|
||||
namespace Phantom.Web;
|
||||
|
||||
public static class Launcher {
|
||||
static class Launcher {
|
||||
public static WebApplication CreateApplication(Configuration config, ServiceConfiguration serviceConfiguration, TaskManager taskManager) {
|
||||
var assembly = typeof(Launcher).Assembly;
|
||||
var builder = WebApplication.CreateBuilder(new WebApplicationOptions {
|
||||
@ -26,15 +26,13 @@ public static class Launcher {
|
||||
|
||||
builder.Services.AddSingleton(serviceConfiguration);
|
||||
builder.Services.AddSingleton(taskManager);
|
||||
builder.Services.AddPhantomServices(config.CancellationToken);
|
||||
|
||||
builder.Services.AddSingleton<IHostLifetime>(new NullLifetime());
|
||||
builder.Services.AddScoped<INavigation>(Navigation.Create(config.BasePath));
|
||||
|
||||
builder.Services.AddDataProtection().PersistKeysToFileSystem(new DirectoryInfo(config.KeyFolderPath));
|
||||
|
||||
builder.Services.AddPhantomIdentity(config.CancellationToken);
|
||||
builder.Services.AddScoped<ILoginEvents, LoginEvents>();
|
||||
|
||||
builder.Services.AddRazorPages(static options => options.RootDirectory = "/Layout");
|
||||
builder.Services.AddServerSideBlazor();
|
||||
|
||||
@ -53,7 +51,7 @@ public static class Launcher {
|
||||
|
||||
application.UseStaticFiles();
|
||||
application.UseRouting();
|
||||
application.UsePhantomIdentity();
|
||||
application.UsePhantomServices();
|
||||
|
||||
application.MapControllers();
|
||||
application.MapBlazorHub();
|
||||
|
@ -1,4 +1,5 @@
|
||||
@inject ServiceConfiguration Configuration
|
||||
@using Phantom.Common.Data.Web.Users.Permissions
|
||||
@inject ServiceConfiguration Configuration
|
||||
@inject PermissionManager PermissionManager
|
||||
|
||||
<div class="navbar navbar-dark">
|
||||
|
@ -1,14 +1,17 @@
|
||||
@page "/setup"
|
||||
@using Phantom.Common.Data.Web.Users
|
||||
@using Phantom.Utils.Cryptography
|
||||
@using Phantom.Utils.Tasks
|
||||
@using Phantom.Web.Identity.Authentication
|
||||
@using Phantom.Web.Services.Users
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using Phantom.Utils.Cryptography
|
||||
@using System.Security.Cryptography
|
||||
@attribute [AllowAnonymous]
|
||||
@inject ServiceConfiguration ServiceConfiguration
|
||||
@inject PhantomLoginManager LoginManager
|
||||
@inject UserManager UserManager
|
||||
@inject RoleManager RoleManager
|
||||
@inject RoleManager<> RoleManager
|
||||
@inject UserRoleManager UserRoleManager
|
||||
@inject AuditLog AuditLog
|
||||
|
||||
@ -97,7 +100,7 @@
|
||||
}
|
||||
|
||||
switch (await UserManager.CreateUser(form.Username, form.Password)) {
|
||||
case Result<UserEntity, AddUserError>.Ok ok:
|
||||
case Result<UserInfo, AddUserError>.Ok ok:
|
||||
var administratorUser = ok.Value;
|
||||
await AuditLog.AddAdministratorUserCreatedEvent(administratorUser);
|
||||
|
||||
@ -107,15 +110,15 @@
|
||||
|
||||
return Result.Ok<string>();
|
||||
|
||||
case Result<UserEntity, AddUserError>.Fail fail:
|
||||
case Result<UserInfo, AddUserError>.Fail fail:
|
||||
return Result.Fail(fail.Error.ToSentences("\n"));
|
||||
}
|
||||
|
||||
return Result.Fail("Unknown error.");
|
||||
}
|
||||
|
||||
private async Task<Result<string>> UpdateAdministrator(UserEntity existingUser) {
|
||||
switch (await UserManager.SetUserPassword(existingUser.UserGuid, form.Password)) {
|
||||
private async Task<Result<string>> UpdateAdministrator(UserInfo existingUser) {
|
||||
switch (await UserManager.SetUserPassword(existingUser.Guid, form.Password)) {
|
||||
case Result<SetUserPasswordError>.Ok:
|
||||
await AuditLog.AddAdministratorUserModifiedEvent(existingUser);
|
||||
return Result.Ok<string>();
|
||||
|
@ -20,8 +20,10 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Common\Phantom.Common.Logging\Phantom.Common.Logging.csproj" />
|
||||
<ProjectReference Include="..\..\Utils\Phantom.Utils\Phantom.Utils.csproj" />
|
||||
<ProjectReference Include="..\Phantom.Web.Components\Phantom.Web.Components.csproj" />
|
||||
<ProjectReference Include="..\Phantom.Web.Identity\Phantom.Web.Identity.csproj" />
|
||||
<ProjectReference Include="..\Phantom.Web.Services\Phantom.Web.Services.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -1,5 +1,4 @@
|
||||
@using Phantom.Controller.Services.Instances
|
||||
@using Phantom.Controller.Services.Audit
|
||||
@using Phantom.Common.Data.Web.Users.Permissions
|
||||
@using Phantom.Common.Data.Replies
|
||||
@inherits PhantomComponent
|
||||
@inject InstanceManager InstanceManager
|
||||
|
@ -1,8 +1,7 @@
|
||||
@inherits PhantomComponent
|
||||
@using Phantom.Utils.Collections
|
||||
@using Phantom.Utils.Events
|
||||
@using System.Diagnostics
|
||||
@using Phantom.Controller.Services.Instances
|
||||
@using Phantom.Common.Data.Web.Users.Permissions
|
||||
@implements IDisposable
|
||||
@inject IJSRuntime Js;
|
||||
@inject InstanceLogManager InstanceLogManager
|
||||
|
@ -1,7 +1,5 @@
|
||||
@using Phantom.Controller.Services.Users
|
||||
@using Phantom.Common.Data.Web.Users
|
||||
@using Phantom.Utils.Tasks
|
||||
@using Phantom.Controller.Database.Entities
|
||||
@using Phantom.Controller.Services.Audit
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@inherits PhantomComponent
|
||||
@inject IJSRuntime Js;
|
||||
|
@ -1,6 +1,4 @@
|
||||
@using Phantom.Controller.Database.Entities
|
||||
@using Phantom.Controller.Services.Audit
|
||||
@using Phantom.Controller.Services.Users
|
||||
@using Phantom.Common.Data.Web.Users
|
||||
@inherits UserEditDialogBase
|
||||
@inject UserManager UserManager
|
||||
@inject AuditLog AuditLog
|
||||
@ -19,8 +17,8 @@
|
||||
|
||||
@code {
|
||||
|
||||
protected override async Task DoEdit(UserEntity user) {
|
||||
switch (await UserManager.DeleteByGuid(user.UserGuid)) {
|
||||
protected override async Task DoEdit(UserInfo user) {
|
||||
switch (await UserManager.DeleteByGuid(user.Guid)) {
|
||||
case DeleteUserResult.Deleted:
|
||||
await AuditLog.AddUserDeletedEvent(user);
|
||||
await OnEditSuccess();
|
||||
|
@ -1,5 +1,7 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
using Phantom.Common.Data.Web.Users;
|
||||
using Phantom.Common.Data.Web.Users.Permissions;
|
||||
using Phantom.Web.Base;
|
||||
using Phantom.Web.Components.Forms;
|
||||
|
||||
@ -13,14 +15,14 @@ public abstract class UserEditDialogBase : PhantomComponent {
|
||||
public string ModalId { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<UserEntity> UserModified { get; set; }
|
||||
public EventCallback<UserInfo> UserModified { get; set; }
|
||||
|
||||
protected readonly FormButtonSubmit.SubmitModel SubmitModel = new();
|
||||
|
||||
private UserEntity? EditedUser { get; set; } = null;
|
||||
private UserInfo? EditedUser { get; set; } = null;
|
||||
protected string EditedUserName { get; private set; } = string.Empty;
|
||||
|
||||
internal async Task Show(UserEntity user) {
|
||||
internal async Task Show(UserInfo user) {
|
||||
EditedUser = user;
|
||||
EditedUserName = user.Name;
|
||||
await BeforeShown(user);
|
||||
@ -29,7 +31,7 @@ public abstract class UserEditDialogBase : PhantomComponent {
|
||||
await Js.InvokeVoidAsync("showModal", ModalId);
|
||||
}
|
||||
|
||||
protected virtual Task BeforeShown(UserEntity user) {
|
||||
protected virtual Task BeforeShown(UserInfo user) {
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@ -51,7 +53,7 @@ public abstract class UserEditDialogBase : PhantomComponent {
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract Task DoEdit(UserEntity user);
|
||||
protected abstract Task DoEdit(UserInfo user);
|
||||
|
||||
protected async Task OnEditSuccess() {
|
||||
await UserModified.InvokeAsync(EditedUser);
|
||||
|
@ -1,11 +1,5 @@
|
||||
@using Phantom.Controller.Database.Entities
|
||||
@using Phantom.Controller.Services.Audit
|
||||
@using Phantom.Controller.Services.Users
|
||||
@using Phantom.Common.Data.Web.Users
|
||||
@inherits UserEditDialogBase
|
||||
@inject UserManager UserManager
|
||||
@inject RoleManager RoleManager
|
||||
@inject UserRoleManager UserRoleManager
|
||||
@inject AuditLog AuditLog
|
||||
|
||||
<Modal Id="@ModalId" TitleText="Manage User Roles">
|
||||
<Body>
|
||||
@ -29,13 +23,13 @@
|
||||
|
||||
private List<RoleItem> items = new();
|
||||
|
||||
protected override async Task BeforeShown(UserEntity user) {
|
||||
protected override async Task BeforeShown(UserInfo user) {
|
||||
var userRoles = await UserRoleManager.GetUserRoleGuids(user);
|
||||
var allRoles = await RoleManager.GetAll();
|
||||
this.items = allRoles.Select(role => new RoleItem(role, userRoles.Contains(role.RoleGuid))).ToList();
|
||||
}
|
||||
|
||||
protected override async Task DoEdit(UserEntity user) {
|
||||
protected override async Task DoEdit(UserInfo user) {
|
||||
var userRoles = await UserRoleManager.GetUserRoleGuids(user);
|
||||
var addedToRoles = new List<string>();
|
||||
var removedFromRoles = new List<string>();
|
||||
@ -43,7 +37,7 @@
|
||||
|
||||
foreach (var item in items) {
|
||||
var shouldHaveRole = item.Checked;
|
||||
if (shouldHaveRole == userRoles.Contains(item.Role.RoleGuid)) {
|
||||
if (shouldHaveRole == userRoles.Contains(item.Role.Guid)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -70,10 +64,10 @@
|
||||
}
|
||||
|
||||
private sealed class RoleItem {
|
||||
public RoleEntity Role { get; }
|
||||
public RoleInfo Role { get; }
|
||||
public bool Checked { get; set; }
|
||||
|
||||
public RoleItem(RoleEntity role, bool @checked) {
|
||||
public RoleItem(RoleInfo role, bool @checked) {
|
||||
this.Role = role;
|
||||
this.Checked = @checked;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user