diff --git a/src/modules/Elsa.Identity/Contracts/IRoleManager.cs b/src/modules/Elsa.Identity/Contracts/IRoleManager.cs
new file mode 100644
index 000000000..51f509068
--- /dev/null
+++ b/src/modules/Elsa.Identity/Contracts/IRoleManager.cs
@@ -0,0 +1,20 @@
+using Elsa.Identity.Entities;
+using Elsa.Identity.Models;
+
+namespace Elsa.Identity.Contracts;
+
+///
+/// Manages role operations such as creation.
+///
+public interface IRoleManager
+{
+ ///
+ /// Creates a new role with the specified details.
+ ///
+ /// The role name.
+ /// The permissions to assign to the role. If null, defaults to an empty list.
+ /// The optional role ID. If null, will be generated from the name.
+ /// The cancellation token.
+ /// A result containing the created role.
+ Task CreateRoleAsync(string name, ICollection? permissions = null, string? id = null, CancellationToken cancellationToken = default);
+}
diff --git a/src/modules/Elsa.Identity/Contracts/IUserManager.cs b/src/modules/Elsa.Identity/Contracts/IUserManager.cs
new file mode 100644
index 000000000..db6d1b21b
--- /dev/null
+++ b/src/modules/Elsa.Identity/Contracts/IUserManager.cs
@@ -0,0 +1,20 @@
+using Elsa.Identity.Entities;
+using Elsa.Identity.Models;
+
+namespace Elsa.Identity.Contracts;
+
+///
+/// Manages user operations such as creation and validation.
+///
+public interface IUserManager
+{
+ ///
+ /// Creates a new user with the specified details.
+ ///
+ /// The user name (typically email).
+ /// The user's password. If null or empty, a password will be generated.
+ /// The roles to assign to the user. If null, defaults to an empty list.
+ /// The cancellation token.
+ /// A result containing the created user and the plain-text password.
+ Task CreateUserAsync(string name, string? password = null, ICollection? roles = null, CancellationToken cancellationToken = default);
+}
diff --git a/src/modules/Elsa.Identity/Endpoints/Roles/Create/Endpoint.cs b/src/modules/Elsa.Identity/Endpoints/Roles/Create/Endpoint.cs
index 0b4d68f97..5405aef12 100644
--- a/src/modules/Elsa.Identity/Endpoints/Roles/Create/Endpoint.cs
+++ b/src/modules/Elsa.Identity/Endpoints/Roles/Create/Endpoint.cs
@@ -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.
///
[PublicAPI]
-internal class Create : ElsaEndpoint
+internal class Create(IRoleManager roleManager) : ElsaEndpoint
{
- 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;
- }
-
///
public override void Configure()
{
@@ -44,21 +21,16 @@ internal class Create : ElsaEndpoint
///
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()
- };
-
- 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);
}
diff --git a/src/modules/Elsa.Identity/Endpoints/Users/Create/Endpoint.cs b/src/modules/Elsa.Identity/Endpoints/Users/Create/Endpoint.cs
index 52d216620..15ad92dee 100644
--- a/src/modules/Elsa.Identity/Endpoints/Users/Create/Endpoint.cs
+++ b/src/modules/Elsa.Identity/Endpoints/Users/Create/Endpoint.cs
@@ -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 SecurityRoot policy.
///
[PublicAPI]
-internal class Create : ElsaEndpoint
+internal class Create(IUserManager userManager) : ElsaEndpoint
{
- 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;
- }
-
///
public override void Configure()
{
@@ -43,29 +21,20 @@ internal class Create : ElsaEndpoint
///
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(),
- 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);
}
diff --git a/src/modules/Elsa.Identity/Features/IdentityFeature.cs b/src/modules/Elsa.Identity/Features/IdentityFeature.cs
index 9fab9f58e..e5119971c 100644
--- a/src/modules/Elsa.Identity/Features/IdentityFeature.cs
+++ b/src/modules/Elsa.Identity/Features/IdentityFeature.cs
@@ -189,6 +189,8 @@ public class IdentityFeature : FeatureBase
.AddScoped(UserProvider)
.AddScoped(ApplicationProvider)
.AddScoped(RoleProvider)
+ .AddScoped()
+ .AddScoped()
.AddScoped()
.AddScoped()
.AddScoped()
diff --git a/src/modules/Elsa.Identity/Models/CreateRoleResult.cs b/src/modules/Elsa.Identity/Models/CreateRoleResult.cs
new file mode 100644
index 000000000..48a16b7ea
--- /dev/null
+++ b/src/modules/Elsa.Identity/Models/CreateRoleResult.cs
@@ -0,0 +1,9 @@
+using Elsa.Identity.Entities;
+
+namespace Elsa.Identity.Models;
+
+///
+/// Result of a role creation operation.
+///
+/// The created role entity.
+public record CreateRoleResult(Role Role);
diff --git a/src/modules/Elsa.Identity/Models/CreateUserResult.cs b/src/modules/Elsa.Identity/Models/CreateUserResult.cs
new file mode 100644
index 000000000..3a7159102
--- /dev/null
+++ b/src/modules/Elsa.Identity/Models/CreateUserResult.cs
@@ -0,0 +1,10 @@
+using Elsa.Identity.Entities;
+
+namespace Elsa.Identity.Models;
+
+///
+/// Result of a user creation operation.
+///
+/// The created user entity.
+/// The plain-text password (either provided or generated).
+public record CreateUserResult(User User, string Password);
diff --git a/src/modules/Elsa.Identity/Services/RoleManager.cs b/src/modules/Elsa.Identity/Services/RoleManager.cs
new file mode 100644
index 000000000..da9c431be
--- /dev/null
+++ b/src/modules/Elsa.Identity/Services/RoleManager.cs
@@ -0,0 +1,40 @@
+using Elsa.Identity.Contracts;
+using Elsa.Identity.Entities;
+using Elsa.Identity.Models;
+using Humanizer;
+
+namespace Elsa.Identity.Services;
+
+///
+/// Default implementation of .
+///
+public class RoleManager : IRoleManager
+{
+ private readonly IRoleStore _roleStore;
+
+ public RoleManager(IRoleStore roleStore)
+ {
+ _roleStore = roleStore;
+ }
+
+ ///
+ public async Task CreateRoleAsync(
+ string name,
+ ICollection? 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()
+ };
+
+ await _roleStore.SaveAsync(role, cancellationToken);
+
+ return new CreateRoleResult(role);
+ }
+}
diff --git a/src/modules/Elsa.Identity/Services/UserManager.cs b/src/modules/Elsa.Identity/Services/UserManager.cs
new file mode 100644
index 000000000..cfd2829da
--- /dev/null
+++ b/src/modules/Elsa.Identity/Services/UserManager.cs
@@ -0,0 +1,54 @@
+using Elsa.Identity.Contracts;
+using Elsa.Identity.Entities;
+using Elsa.Identity.Models;
+using Elsa.Workflows;
+
+namespace Elsa.Identity.Services;
+
+///
+/// Default implementation of .
+///
+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;
+ }
+
+ ///
+ public async Task CreateUserAsync(
+ string name,
+ string? password = null,
+ ICollection? 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(),
+ HashedPassword = hashedPassword.EncodeSecret(),
+ HashedPasswordSalt = hashedPassword.EncodeSalt()
+ };
+
+ await _userStore.SaveAsync(user, cancellationToken);
+
+ return new CreateUserResult(user, plainTextPassword);
+ }
+}
diff --git a/src/modules/Elsa.Identity/ShellFeatures/DefaultAuthenticationFeature.cs b/src/modules/Elsa.Identity/ShellFeatures/DefaultAuthenticationFeature.cs
index c31783057..2d2912dd6 100644
--- a/src/modules/Elsa.Identity/ShellFeatures/DefaultAuthenticationFeature.cs
+++ b/src/modules/Elsa.Identity/ShellFeatures/DefaultAuthenticationFeature.cs
@@ -28,11 +28,6 @@ public class DefaultAuthenticationFeature : IShellFeature
///
public Type ApiKeyProviderType { get; set; } = typeof(DefaultApiKeyProvider);
- ///
- /// Gets or sets whether to require localhost for the security root policy.
- ///
- public bool RequireLocalHost { get; set; } = true;
-
public void ConfigureServices(IServiceCollection services)
{
services.ConfigureOptions();
@@ -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());
});
}
}
diff --git a/src/modules/Elsa.Identity/ShellFeatures/IdentityFeature.cs b/src/modules/Elsa.Identity/ShellFeatures/IdentityFeature.cs
index a4b46679d..702e645c0 100644
--- a/src/modules/Elsa.Identity/ShellFeatures/IdentityFeature.cs
+++ b/src/modules/Elsa.Identity/ShellFeatures/IdentityFeature.cs
@@ -24,36 +24,6 @@ namespace Elsa.Identity.ShellFeatures;
[UsedImplicitly]
public class IdentityFeature : IFastEndpointsShellFeature
{
- ///
- /// A delegate that creates an instance of an implementation of .
- ///
- public Func UserStore { get; set; } = sp => sp.GetRequiredService();
-
- ///
- /// A delegate that creates an instance of an implementation of .
- ///
- public Func ApplicationStore { get; set; } = sp => sp.GetRequiredService();
-
- ///
- /// A delegate that creates an instance of an implementation of .
- ///
- public Func RoleStore { get; set; } = sp => sp.GetRequiredService();
-
- ///
- /// A delegate that creates an instance of an implementation of .
- ///
- public Func UserProvider { get; set; } = sp => sp.GetRequiredService();
-
- ///
- /// A delegate that creates an instance of an implementation of .
- ///
- public Func ApplicationProvider { get; set; } = sp => sp.GetRequiredService();
-
- ///
- /// A delegate that creates an instance of an implementation of .
- ///
- public Func RoleProvider { get; set; } = sp => sp.GetRequiredService();
-
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()
+ .AddScoped()
.AddScoped()
.AddScoped()
.AddScoped()
@@ -115,5 +81,14 @@ public class IdentityFeature : IFastEndpointsShellFeature
.AddScoped()
.AddHttpContextAccessor()
;
+
+ // Overridable services.
+ services
+ .AddScoped()
+ .AddScoped()
+ .AddScoped()
+ .AddScoped()
+ .AddScoped()
+ .AddScoped();
}
-}
+}
\ No newline at end of file