Introduce role and user management services (#7297)
* Introduce role and user management services - Add `CreateRoleResult` and `CreateUserResult` models for managing creation results. - Implement `IRoleManager` and `IUserManager` interfaces and their service classes. - Configure dependency injection for role and user managers. - Refactor endpoints to use new manager interfaces. * Remove IRoleStore dependency from Create endpoint
This commit is contained in:
parent
c99a594eb3
commit
1b09538a13
20
src/modules/Elsa.Identity/Contracts/IRoleManager.cs
Normal file
20
src/modules/Elsa.Identity/Contracts/IRoleManager.cs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
using Elsa.Identity.Entities;
|
||||
using Elsa.Identity.Models;
|
||||
|
||||
namespace Elsa.Identity.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Manages role operations such as creation.
|
||||
/// </summary>
|
||||
public interface IRoleManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new role with the specified details.
|
||||
/// </summary>
|
||||
/// <param name="name">The role name.</param>
|
||||
/// <param name="permissions">The permissions to assign to the role. If null, defaults to an empty list.</param>
|
||||
/// <param name="id">The optional role ID. If null, will be generated from the name.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A result containing the created role.</returns>
|
||||
Task<CreateRoleResult> CreateRoleAsync(string name, ICollection<string>? permissions = null, string? id = null, CancellationToken cancellationToken = default);
|
||||
}
|
||||
20
src/modules/Elsa.Identity/Contracts/IUserManager.cs
Normal file
20
src/modules/Elsa.Identity/Contracts/IUserManager.cs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
using Elsa.Identity.Entities;
|
||||
using Elsa.Identity.Models;
|
||||
|
||||
namespace Elsa.Identity.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Manages user operations such as creation and validation.
|
||||
/// </summary>
|
||||
public interface IUserManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new user with the specified details.
|
||||
/// </summary>
|
||||
/// <param name="name">The user name (typically email).</param>
|
||||
/// <param name="password">The user's password. If null or empty, a password will be generated.</param>
|
||||
/// <param name="roles">The roles to assign to the user. If null, defaults to an empty list.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A result containing the created user and the plain-text password.</returns>
|
||||
Task<CreateUserResult> CreateUserAsync(string name, string? password = null, ICollection<string>? roles = null, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
|
@ -1,8 +1,5 @@
|
|||
using Elsa.Abstractions;
|
||||
using Elsa.Abstractions;
|
||||
using Elsa.Identity.Contracts;
|
||||
using Elsa.Identity.Entities;
|
||||
using Elsa.Workflows;
|
||||
using Humanizer;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Elsa.Identity.Endpoints.Roles.Create;
|
||||
|
|
@ -11,28 +8,8 @@ namespace Elsa.Identity.Endpoints.Roles.Create;
|
|||
/// An endpoint that creates a new role.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
internal class Create : ElsaEndpoint<Request, Response>
|
||||
internal class Create(IRoleManager roleManager) : ElsaEndpoint<Request, Response>
|
||||
{
|
||||
private readonly IIdentityGenerator _identityGenerator;
|
||||
private readonly ISecretGenerator _secretGenerator;
|
||||
private readonly ISecretHasher _secretHasher;
|
||||
private readonly IUserStore _userStore;
|
||||
private readonly IRoleStore _roleStore;
|
||||
|
||||
public Create(
|
||||
IIdentityGenerator identityGenerator,
|
||||
ISecretGenerator secretGenerator,
|
||||
ISecretHasher secretHasher,
|
||||
IUserStore userStore,
|
||||
IRoleStore roleStore)
|
||||
{
|
||||
_identityGenerator = identityGenerator;
|
||||
_secretGenerator = secretGenerator;
|
||||
_secretHasher = secretHasher;
|
||||
_userStore = userStore;
|
||||
_roleStore = roleStore;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Configure()
|
||||
{
|
||||
|
|
@ -44,21 +21,16 @@ internal class Create : ElsaEndpoint<Request, Response>
|
|||
/// <inheritdoc />
|
||||
public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
|
||||
{
|
||||
var id = request.Id ?? request.Name.Kebaberize();
|
||||
|
||||
var role = new Role
|
||||
{
|
||||
Id = id,
|
||||
Name = request.Name,
|
||||
Permissions = request.Permissions ?? new List<string>()
|
||||
};
|
||||
|
||||
await _roleStore.SaveAsync(role, cancellationToken);
|
||||
var result = await roleManager.CreateRoleAsync(
|
||||
request.Name,
|
||||
request.Permissions,
|
||||
request.Id,
|
||||
cancellationToken);
|
||||
|
||||
var response = new Response(
|
||||
id,
|
||||
role.Name,
|
||||
role.Permissions);
|
||||
result.Role.Id,
|
||||
result.Role.Name,
|
||||
result.Role.Permissions);
|
||||
|
||||
await Send.OkAsync(response, cancellationToken);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
using Elsa.Abstractions;
|
||||
using Elsa.Abstractions;
|
||||
using Elsa.Identity.Contracts;
|
||||
using Elsa.Identity.Entities;
|
||||
using Elsa.Workflows;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Elsa.Identity.Endpoints.Users.Create;
|
||||
|
|
@ -10,28 +8,8 @@ namespace Elsa.Identity.Endpoints.Users.Create;
|
|||
/// An endpoint that creates a new user. Requires the <code>SecurityRoot</code> policy.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
internal class Create : ElsaEndpoint<Request, Response>
|
||||
internal class Create(IUserManager userManager) : ElsaEndpoint<Request, Response>
|
||||
{
|
||||
private readonly IIdentityGenerator _identityGenerator;
|
||||
private readonly ISecretGenerator _secretGenerator;
|
||||
private readonly ISecretHasher _secretHasher;
|
||||
private readonly IUserStore _userStore;
|
||||
private readonly IRoleStore _roleStore;
|
||||
|
||||
public Create(
|
||||
IIdentityGenerator identityGenerator,
|
||||
ISecretGenerator secretGenerator,
|
||||
ISecretHasher secretHasher,
|
||||
IUserStore userStore,
|
||||
IRoleStore roleStore)
|
||||
{
|
||||
_identityGenerator = identityGenerator;
|
||||
_secretGenerator = secretGenerator;
|
||||
_secretHasher = secretHasher;
|
||||
_userStore = userStore;
|
||||
_roleStore = roleStore;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Configure()
|
||||
{
|
||||
|
|
@ -43,29 +21,20 @@ internal class Create : ElsaEndpoint<Request, Response>
|
|||
/// <inheritdoc />
|
||||
public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
|
||||
{
|
||||
var id = _identityGenerator.GenerateId();
|
||||
var password = string.IsNullOrWhiteSpace(request.Password) ? _secretGenerator.Generate() : request.Password.Trim();
|
||||
var hashedPassword = _secretHasher.HashSecret(password);
|
||||
|
||||
var user = new User
|
||||
{
|
||||
Id = id,
|
||||
Name = request.Name,
|
||||
Roles = request.Roles ?? new List<string>(),
|
||||
HashedPassword = hashedPassword.EncodeSecret(),
|
||||
HashedPasswordSalt = hashedPassword.EncodeSalt()
|
||||
};
|
||||
|
||||
await _userStore.SaveAsync(user, cancellationToken);
|
||||
var result = await userManager.CreateUserAsync(
|
||||
request.Name,
|
||||
request.Password,
|
||||
request.Roles,
|
||||
cancellationToken);
|
||||
|
||||
var response = new Response(
|
||||
id,
|
||||
user.Name,
|
||||
password,
|
||||
user.Roles,
|
||||
user.TenantId,
|
||||
hashedPassword.EncodeSecret(),
|
||||
hashedPassword.EncodeSalt());
|
||||
result.User.Id,
|
||||
result.User.Name,
|
||||
result.Password,
|
||||
result.User.Roles,
|
||||
result.User.TenantId,
|
||||
result.User.HashedPassword,
|
||||
result.User.HashedPasswordSalt);
|
||||
|
||||
await Send.OkAsync(response, cancellationToken);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -189,6 +189,8 @@ public class IdentityFeature : FeatureBase
|
|||
.AddScoped(UserProvider)
|
||||
.AddScoped(ApplicationProvider)
|
||||
.AddScoped(RoleProvider)
|
||||
.AddScoped<IUserManager, UserManager>()
|
||||
.AddScoped<IRoleManager, RoleManager>()
|
||||
.AddScoped<ISecretHasher, DefaultSecretHasher>()
|
||||
.AddScoped<IAccessTokenIssuer, DefaultAccessTokenIssuer>()
|
||||
.AddScoped<IUserCredentialsValidator, DefaultUserCredentialsValidator>()
|
||||
|
|
|
|||
9
src/modules/Elsa.Identity/Models/CreateRoleResult.cs
Normal file
9
src/modules/Elsa.Identity/Models/CreateRoleResult.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
using Elsa.Identity.Entities;
|
||||
|
||||
namespace Elsa.Identity.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Result of a role creation operation.
|
||||
/// </summary>
|
||||
/// <param name="Role">The created role entity.</param>
|
||||
public record CreateRoleResult(Role Role);
|
||||
10
src/modules/Elsa.Identity/Models/CreateUserResult.cs
Normal file
10
src/modules/Elsa.Identity/Models/CreateUserResult.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
using Elsa.Identity.Entities;
|
||||
|
||||
namespace Elsa.Identity.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Result of a user creation operation.
|
||||
/// </summary>
|
||||
/// <param name="User">The created user entity.</param>
|
||||
/// <param name="Password">The plain-text password (either provided or generated).</param>
|
||||
public record CreateUserResult(User User, string Password);
|
||||
40
src/modules/Elsa.Identity/Services/RoleManager.cs
Normal file
40
src/modules/Elsa.Identity/Services/RoleManager.cs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
using Elsa.Identity.Contracts;
|
||||
using Elsa.Identity.Entities;
|
||||
using Elsa.Identity.Models;
|
||||
using Humanizer;
|
||||
|
||||
namespace Elsa.Identity.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of <see cref="IRoleManager"/>.
|
||||
/// </summary>
|
||||
public class RoleManager : IRoleManager
|
||||
{
|
||||
private readonly IRoleStore _roleStore;
|
||||
|
||||
public RoleManager(IRoleStore roleStore)
|
||||
{
|
||||
_roleStore = roleStore;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<CreateRoleResult> CreateRoleAsync(
|
||||
string name,
|
||||
ICollection<string>? permissions = null,
|
||||
string? id = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var roleId = id ?? name.Kebaberize();
|
||||
|
||||
var role = new Role
|
||||
{
|
||||
Id = roleId,
|
||||
Name = name,
|
||||
Permissions = permissions ?? new List<string>()
|
||||
};
|
||||
|
||||
await _roleStore.SaveAsync(role, cancellationToken);
|
||||
|
||||
return new CreateRoleResult(role);
|
||||
}
|
||||
}
|
||||
54
src/modules/Elsa.Identity/Services/UserManager.cs
Normal file
54
src/modules/Elsa.Identity/Services/UserManager.cs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
using Elsa.Identity.Contracts;
|
||||
using Elsa.Identity.Entities;
|
||||
using Elsa.Identity.Models;
|
||||
using Elsa.Workflows;
|
||||
|
||||
namespace Elsa.Identity.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of <see cref="IUserManager"/>.
|
||||
/// </summary>
|
||||
public class UserManager : IUserManager
|
||||
{
|
||||
private readonly IIdentityGenerator _identityGenerator;
|
||||
private readonly ISecretGenerator _secretGenerator;
|
||||
private readonly ISecretHasher _secretHasher;
|
||||
private readonly IUserStore _userStore;
|
||||
|
||||
public UserManager(
|
||||
IIdentityGenerator identityGenerator,
|
||||
ISecretGenerator secretGenerator,
|
||||
ISecretHasher secretHasher,
|
||||
IUserStore userStore)
|
||||
{
|
||||
_identityGenerator = identityGenerator;
|
||||
_secretGenerator = secretGenerator;
|
||||
_secretHasher = secretHasher;
|
||||
_userStore = userStore;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<CreateUserResult> CreateUserAsync(
|
||||
string name,
|
||||
string? password = null,
|
||||
ICollection<string>? roles = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var id = _identityGenerator.GenerateId();
|
||||
var plainTextPassword = string.IsNullOrWhiteSpace(password) ? _secretGenerator.Generate() : password.Trim();
|
||||
var hashedPassword = _secretHasher.HashSecret(plainTextPassword);
|
||||
|
||||
var user = new User
|
||||
{
|
||||
Id = id,
|
||||
Name = name,
|
||||
Roles = roles ?? new List<string>(),
|
||||
HashedPassword = hashedPassword.EncodeSecret(),
|
||||
HashedPasswordSalt = hashedPassword.EncodeSalt()
|
||||
};
|
||||
|
||||
await _userStore.SaveAsync(user, cancellationToken);
|
||||
|
||||
return new CreateUserResult(user, plainTextPassword);
|
||||
}
|
||||
}
|
||||
|
|
@ -28,11 +28,6 @@ public class DefaultAuthenticationFeature : IShellFeature
|
|||
/// </summary>
|
||||
public Type ApiKeyProviderType { get; set; } = typeof(DefaultApiKeyProvider);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether to require localhost for the security root policy.
|
||||
/// </summary>
|
||||
public bool RequireLocalHost { get; set; } = true;
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.ConfigureOptions<ConfigureJwtBearerOptions>();
|
||||
|
|
@ -64,10 +59,7 @@ public class DefaultAuthenticationFeature : IShellFeature
|
|||
|
||||
services.AddAuthorization(options =>
|
||||
{
|
||||
if (RequireLocalHost)
|
||||
options.AddPolicy(IdentityPolicyNames.SecurityRoot, policy => policy.AddRequirements(new LocalHostPermissionRequirement()));
|
||||
else
|
||||
options.AddPolicy(IdentityPolicyNames.SecurityRoot, policy => policy.RequireAuthenticatedUser());
|
||||
options.AddPolicy(IdentityPolicyNames.SecurityRoot, policy => policy.RequireAuthenticatedUser());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,36 +24,6 @@ namespace Elsa.Identity.ShellFeatures;
|
|||
[UsedImplicitly]
|
||||
public class IdentityFeature : IFastEndpointsShellFeature
|
||||
{
|
||||
/// <summary>
|
||||
/// A delegate that creates an instance of an implementation of <see cref="IUserStore"/>.
|
||||
/// </summary>
|
||||
public Func<IServiceProvider, IUserStore> UserStore { get; set; } = sp => sp.GetRequiredService<MemoryUserStore>();
|
||||
|
||||
/// <summary>
|
||||
/// A delegate that creates an instance of an implementation of <see cref="IApplicationStore"/>.
|
||||
/// </summary>
|
||||
public Func<IServiceProvider, IApplicationStore> ApplicationStore { get; set; } = sp => sp.GetRequiredService<MemoryApplicationStore>();
|
||||
|
||||
/// <summary>
|
||||
/// A delegate that creates an instance of an implementation of <see cref="IRoleStore"/>.
|
||||
/// </summary>
|
||||
public Func<IServiceProvider, IRoleStore> RoleStore { get; set; } = sp => sp.GetRequiredService<MemoryRoleStore>();
|
||||
|
||||
/// <summary>
|
||||
/// A delegate that creates an instance of an implementation of <see cref="IUserProvider"/>.
|
||||
/// </summary>
|
||||
public Func<IServiceProvider, IUserProvider> UserProvider { get; set; } = sp => sp.GetRequiredService<AdminUserProvider>();
|
||||
|
||||
/// <summary>
|
||||
/// A delegate that creates an instance of an implementation of <see cref="IApplicationProvider"/>.
|
||||
/// </summary>
|
||||
public Func<IServiceProvider, IApplicationProvider> ApplicationProvider { get; set; } = sp => sp.GetRequiredService<StoreBasedApplicationProvider>();
|
||||
|
||||
/// <summary>
|
||||
/// A delegate that creates an instance of an implementation of <see cref="IRoleProvider"/>.
|
||||
/// </summary>
|
||||
public Func<IServiceProvider, IRoleProvider> RoleProvider { get; set; } = sp => sp.GetRequiredService<AdminRoleProvider>();
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
// Configure options - Note: SigningKey must be configured by the application for security
|
||||
|
|
@ -97,12 +67,8 @@ public class IdentityFeature : IFastEndpointsShellFeature
|
|||
|
||||
// Services.
|
||||
services
|
||||
.AddScoped(UserStore)
|
||||
.AddScoped(ApplicationStore)
|
||||
.AddScoped(RoleStore)
|
||||
.AddScoped(UserProvider)
|
||||
.AddScoped(ApplicationProvider)
|
||||
.AddScoped(RoleProvider)
|
||||
.AddScoped<IUserManager, UserManager>()
|
||||
.AddScoped<IRoleManager, RoleManager>()
|
||||
.AddScoped<ISecretHasher, DefaultSecretHasher>()
|
||||
.AddScoped<IAccessTokenIssuer, DefaultAccessTokenIssuer>()
|
||||
.AddScoped<IUserCredentialsValidator, DefaultUserCredentialsValidator>()
|
||||
|
|
@ -115,5 +81,14 @@ public class IdentityFeature : IFastEndpointsShellFeature
|
|||
.AddScoped<DefaultApiKeyGeneratorAndParser>()
|
||||
.AddHttpContextAccessor()
|
||||
;
|
||||
|
||||
// Overridable services.
|
||||
services
|
||||
.AddScoped<IUserStore, MemoryUserStore>()
|
||||
.AddScoped<IApplicationStore, MemoryApplicationStore>()
|
||||
.AddScoped<IRoleStore, MemoryRoleStore>()
|
||||
.AddScoped<IUserProvider, StoreBasedUserProvider>()
|
||||
.AddScoped<IApplicationProvider, StoreBasedApplicationProvider>()
|
||||
.AddScoped<IRoleProvider, StoreBasedRoleProvider>();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue