From 5d154bc1d85749ad5d5896fe9f464c83852e53a9 Mon Sep 17 00:00:00 2001 From: Francis Pion Date: Mon, 4 Mar 2024 19:26:18 -0500 Subject: [PATCH] Refactored Identity settings. (#55) * Refactored settings configuration. * Refactored the PasswordManager. * Refactored UserManager. --- .../Controllers/SettingsController.cs | 25 ++++++++++ .../appsettings.Development.json | 23 +++++++++ .../Passwords/IPasswordManager.cs | 25 +++++++++- .../Settings/RoleSettingsResolver.cs | 2 +- .../Settings/UserSettingsResolver.cs | 2 +- .../Users/IUserManager.cs | 19 ++++++++ .../Users/UserManager.cs | 28 ++++++++++- .../Passwords/PasswordManager.cs | 47 +++++++++++++++---- .../Settings/RoleSettingsResolverTests.cs | 4 +- .../Settings/UserSettingsResolverTests.cs | 10 ++-- 10 files changed, 164 insertions(+), 21 deletions(-) create mode 100644 src/Logitar.Identity.Demo/Controllers/SettingsController.cs diff --git a/src/Logitar.Identity.Demo/Controllers/SettingsController.cs b/src/Logitar.Identity.Demo/Controllers/SettingsController.cs new file mode 100644 index 0000000..2860fea --- /dev/null +++ b/src/Logitar.Identity.Demo/Controllers/SettingsController.cs @@ -0,0 +1,25 @@ +using Logitar.Identity.Contracts.Settings; +using Logitar.Identity.Domain.Settings; +using Microsoft.AspNetCore.Mvc; + +namespace Logitar.Identity.Demo.Controllers; + +[ApiController] +[Route("settings")] +public class SettingsController : ControllerBase +{ + private readonly IRoleSettingsResolver _roleSettings; + private readonly IUserSettingsResolver _userSettings; + + public SettingsController(IRoleSettingsResolver roleSettings, IUserSettingsResolver userSettings) + { + _roleSettings = roleSettings; + _userSettings = userSettings; + } + + [HttpGet("role")] + public ActionResult GetRoleSettings() => Ok(_roleSettings.Resolve()); + + [HttpGet("user")] + public ActionResult GetUserSettings() => Ok(_userSettings.Resolve()); +} diff --git a/src/Logitar.Identity.Demo/appsettings.Development.json b/src/Logitar.Identity.Demo/appsettings.Development.json index 5a4d486..c9e3b6e 100644 --- a/src/Logitar.Identity.Demo/appsettings.Development.json +++ b/src/Logitar.Identity.Demo/appsettings.Development.json @@ -1,5 +1,28 @@ { "EnableMigrations": true, + "EnableOpenApi": true, + "Identity": { + "Role": { + "UniqueName": { + "AllowedCharacters": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_" + } + }, + "User": { + "UniqueName": { + "AllowedCharacters": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+!" + }, + "Password": { + "RequiredLength": 6, + "RequiredUniqueChars": 1, + "RequireNonAlphanumeric": false, + "RequireLowercase": true, + "RequireUppercase": true, + "RequireDigit": true, + "HashingStrategy": "BCRYPT" + }, + "RequireUniqueEmail": true + } + }, "Logging": { "LogLevel": { "Default": "Information", diff --git a/src/Logitar.Identity.Domain/Passwords/IPasswordManager.cs b/src/Logitar.Identity.Domain/Passwords/IPasswordManager.cs index 098756c..1e404bf 100644 --- a/src/Logitar.Identity.Domain/Passwords/IPasswordManager.cs +++ b/src/Logitar.Identity.Domain/Passwords/IPasswordManager.cs @@ -1,4 +1,6 @@ -namespace Logitar.Identity.Domain.Passwords; +using Logitar.Identity.Contracts.Settings; + +namespace Logitar.Identity.Domain.Passwords; /// /// Defines methods to manage passwords. @@ -12,6 +14,13 @@ public interface IPasswordManager /// The password instance. Password Create(string password); /// + /// Creates a password from the specified string. This method should not be used to create strong user passwords. + /// + /// The password string. + /// The password settings. + /// The password instance. + Password Create(string password, IPasswordSettings? passwordSettings); + /// /// Decodes a password from the encoded string. /// /// The encoded password. @@ -46,10 +55,24 @@ public interface IPasswordManager /// The password string. void Validate(string password); /// + /// Validates the specified password string. + /// + /// The password string. + /// The password settings. + void Validate(string password, IPasswordSettings? passwordSettings); + /// /// Validates the specified password string, then creates a password if it is valid, or throws an exception otherwise. /// /// The password string. /// The password instance. /// The password is too weak. Password ValidateAndCreate(string password); + /// + /// Validates the specified password string, then creates a password if it is valid, or throws an exception otherwise. + /// + /// The password string. + /// The password settings. + /// The password instance. + /// The password is too weak. + Password ValidateAndCreate(string password, IPasswordSettings? passwordSettings); } diff --git a/src/Logitar.Identity.Domain/Settings/RoleSettingsResolver.cs b/src/Logitar.Identity.Domain/Settings/RoleSettingsResolver.cs index f7a393d..f64166c 100644 --- a/src/Logitar.Identity.Domain/Settings/RoleSettingsResolver.cs +++ b/src/Logitar.Identity.Domain/Settings/RoleSettingsResolver.cs @@ -32,7 +32,7 @@ public RoleSettingsResolver(IConfiguration configuration) /// The role settings. public virtual IRoleSettings Resolve() { - RoleSettings ??= Configuration.GetSection("Role").Get() ?? new(); + RoleSettings ??= Configuration.GetSection("Identity").GetSection("Role").Get() ?? new(); return RoleSettings; } } diff --git a/src/Logitar.Identity.Domain/Settings/UserSettingsResolver.cs b/src/Logitar.Identity.Domain/Settings/UserSettingsResolver.cs index e4ff08f..0823ded 100644 --- a/src/Logitar.Identity.Domain/Settings/UserSettingsResolver.cs +++ b/src/Logitar.Identity.Domain/Settings/UserSettingsResolver.cs @@ -32,7 +32,7 @@ public UserSettingsResolver(IConfiguration configuration) /// The user settings. public virtual IUserSettings Resolve() { - UserSettings ??= Configuration.GetSection("User").Get() ?? new(); + UserSettings ??= Configuration.GetSection("Identity").GetSection("User").Get() ?? new(); return UserSettings; } } diff --git a/src/Logitar.Identity.Domain/Users/IUserManager.cs b/src/Logitar.Identity.Domain/Users/IUserManager.cs index d0fefbf..4704529 100644 --- a/src/Logitar.Identity.Domain/Users/IUserManager.cs +++ b/src/Logitar.Identity.Domain/Users/IUserManager.cs @@ -1,4 +1,5 @@ using Logitar.EventSourcing; +using Logitar.Identity.Contracts.Settings; namespace Logitar.Identity.Domain.Users; @@ -15,6 +16,15 @@ public interface IUserManager /// The cancellation token. /// The found users. Task FindAsync(string? tenantId, string id, CancellationToken cancellationToken = default); + /// + /// Tries finding an user by its unique identifier, unique name, or email address if they are unique. + /// + /// The identifier of the tenant in which to search. + /// The identifier of the user to find. + /// The user settings. + /// The cancellation token. + /// The found users. + Task FindAsync(string? tenantId, string id, IUserSettings? userSettings, CancellationToken cancellationToken = default); /// /// Saves the specified user, performing model validation such as unique name and email address unicity. @@ -24,4 +34,13 @@ public interface IUserManager /// The cancellation token. /// The asynchronous operation. Task SaveAsync(UserAggregate user, ActorId actorId = default, CancellationToken cancellationToken = default); + /// + /// Saves the specified user, performing model validation such as unique name and email address unicity. + /// + /// The user to save. + /// The user settings. + /// The actor identifier. + /// The cancellation token. + /// The asynchronous operation. + Task SaveAsync(UserAggregate user, IUserSettings? userSettings, ActorId actorId = default, CancellationToken cancellationToken = default); } diff --git a/src/Logitar.Identity.Domain/Users/UserManager.cs b/src/Logitar.Identity.Domain/Users/UserManager.cs index 46c6528..ccba49a 100644 --- a/src/Logitar.Identity.Domain/Users/UserManager.cs +++ b/src/Logitar.Identity.Domain/Users/UserManager.cs @@ -48,7 +48,19 @@ public UserManager(ISessionRepository sessionRepository, IUserRepository userRep /// The found users. public virtual async Task FindAsync(string? tenantIdValue, string id, CancellationToken cancellationToken) { - IUserSettings userSettings = UserSettingsResolver.Resolve(); + return await FindAsync(tenantIdValue, id, userSettings: null, cancellationToken); + } + /// + /// Tries finding an user by its unique identifier, unique name, or email address if they are unique. + /// + /// The identifier of the tenant in which to search. + /// The identifier of the user to find. + /// The user settings. + /// The cancellation token. + /// The found users. + public virtual async Task FindAsync(string? tenantIdValue, string id, IUserSettings? userSettings, CancellationToken cancellationToken) + { + userSettings ??= UserSettingsResolver.Resolve(); TenantId? tenantId = null; try @@ -116,6 +128,18 @@ public virtual async Task FindAsync(string? tenantIdValue, string id /// The cancellation token. /// The asynchronous operation. public virtual async Task SaveAsync(UserAggregate user, ActorId actorId, CancellationToken cancellationToken) + { + await SaveAsync(user, userSettings: null, actorId, cancellationToken); + } + /// + /// Saves the specified user, performing model validation such as unique name and email address unicity. + /// + /// The user to save. + /// The user settings. + /// The actor identifier. + /// The cancellation token. + /// The asynchronous operation. + public virtual async Task SaveAsync(UserAggregate user, IUserSettings? userSettings, ActorId actorId, CancellationToken cancellationToken) { bool hasBeenDeleted = false; bool hasEmailChanged = false; @@ -147,7 +171,7 @@ public virtual async Task SaveAsync(UserAggregate user, ActorId actorId, Cancell if (hasEmailChanged && user.Email != null) { - IUserSettings userSettings = UserSettingsResolver.Resolve(); + userSettings ??= UserSettingsResolver.Resolve(); if (userSettings.RequireUniqueEmail) { IEnumerable users = await UserRepository.LoadAsync(user.TenantId, user.Email, cancellationToken); diff --git a/src/Logitar.Identity.Infrastructure/Passwords/PasswordManager.cs b/src/Logitar.Identity.Infrastructure/Passwords/PasswordManager.cs index a679298..36b5c8b 100644 --- a/src/Logitar.Identity.Infrastructure/Passwords/PasswordManager.cs +++ b/src/Logitar.Identity.Infrastructure/Passwords/PasswordManager.cs @@ -43,7 +43,17 @@ public PasswordManager(IUserSettingsResolver settingsResolver, IEnumerableThe password instance. public virtual Password Create(string password) { - IPasswordSettings passwordSettings = SettingsResolver.Resolve().Password; + return Create(password, passwordSettings: null); + } + /// + /// Creates a password from the specified string. This method should not be used to create strong user passwords. + /// + /// The password string. + /// The password settings. + /// The password instance. + public virtual Password Create(string password, IPasswordSettings? passwordSettings) + { + passwordSettings ??= SettingsResolver.Resolve().Password; return GetStrategy(passwordSettings.HashingStrategy).Create(password); } @@ -65,7 +75,7 @@ public virtual Password Decode(string password) /// The length of the password, in number of characters. /// The password string. /// The password instance. - public Password Generate(int length, out string password) + public virtual Password Generate(int length, out string password) { password = RandomStringGenerator.GetString(length); return Create(password); @@ -77,7 +87,7 @@ public Password Generate(int length, out string password) /// The length of the password, in number of characters. /// The password string. /// The password instance. - public Password Generate(string characters, int length, out string password) + public virtual Password Generate(string characters, int length, out string password) { password = RandomStringGenerator.GetString(characters, length); return Create(password); @@ -89,7 +99,7 @@ public Password Generate(string characters, int length, out string password) /// The length of the password, in number of bytes. /// The password string. /// The password instance. - public Password GenerateBase64(int length, out string password) + public virtual Password GenerateBase64(int length, out string password) { password = RandomStringGenerator.GetBase64String(length, out _); return Create(password); @@ -99,9 +109,17 @@ public Password GenerateBase64(int length, out string password) /// Validates the specified password string. /// /// The password string. - public void Validate(string password) + public virtual void Validate(string password) + { + Validate(password, passwordSettings: null); + } + /// + /// Validates the specified password string. + /// + /// The password string. + public virtual void Validate(string password, IPasswordSettings? passwordSettings) { - IPasswordSettings passwordSettings = SettingsResolver.Resolve().Password; + passwordSettings ??= SettingsResolver.Resolve().Password; new PasswordValidator(passwordSettings).ValidateAndThrow(password); } @@ -111,10 +129,21 @@ public void Validate(string password) /// The password string. /// The password instance. /// The password is too weak. - public Password ValidateAndCreate(string password) + public virtual Password ValidateAndCreate(string password) { - Validate(password); - return Create(password); + return ValidateAndCreate(password, passwordSettings: null); + } + /// + /// Validates the specified password string, then creates a password if it is valid, or throws an exception otherwise. + /// + /// The password string. + /// The password settings. + /// The password instance. + /// The password is too weak. + public virtual Password ValidateAndCreate(string password, IPasswordSettings? passwordSettings) + { + Validate(password, passwordSettings); + return Create(password, passwordSettings); } /// diff --git a/tests/Logitar.Identity.Domain.UnitTests/Settings/RoleSettingsResolverTests.cs b/tests/Logitar.Identity.Domain.UnitTests/Settings/RoleSettingsResolverTests.cs index 6d3a4fb..f3a8f94 100644 --- a/tests/Logitar.Identity.Domain.UnitTests/Settings/RoleSettingsResolverTests.cs +++ b/tests/Logitar.Identity.Domain.UnitTests/Settings/RoleSettingsResolverTests.cs @@ -8,7 +8,7 @@ public class RoleSettingsResolverTests { private readonly Dictionary _settings = new() { - ["Role:UniqueName:AllowedCharacters"] = "abcdefghijklmnopqrstuvwxyz_" + ["Identity:Role:UniqueName:AllowedCharacters"] = "abcdefghijklmnopqrstuvwxyz_" }; private readonly IConfiguration _configuration; @@ -32,6 +32,6 @@ public void Resolve_it_should_resolve_the_role_settings_correctly() IRoleSettings settings = _resolver.Resolve(); Assert.Equal(1, _resolver.ReadCounter); - Assert.Equal(_settings["Role:UniqueName:AllowedCharacters"], settings.UniqueName.AllowedCharacters); + Assert.Equal(_settings["Identity:Role:UniqueName:AllowedCharacters"], settings.UniqueName.AllowedCharacters); } } diff --git a/tests/Logitar.Identity.Domain.UnitTests/Settings/UserSettingsResolverTests.cs b/tests/Logitar.Identity.Domain.UnitTests/Settings/UserSettingsResolverTests.cs index c3819b0..e84a847 100644 --- a/tests/Logitar.Identity.Domain.UnitTests/Settings/UserSettingsResolverTests.cs +++ b/tests/Logitar.Identity.Domain.UnitTests/Settings/UserSettingsResolverTests.cs @@ -8,9 +8,9 @@ public class UserSettingsResolverTests { private readonly Dictionary _settings = new() { - ["User:Password:HashingStrategy"] = "CUSTOM", - ["User:UniqueName:AllowedCharacters"] = "abcdefghijklmnopqrstuvwxyz0123456789@", - ["User:RequireUniqueEmail"] = "true" + ["Identity:User:Password:HashingStrategy"] = "CUSTOM", + ["Identity:User:UniqueName:AllowedCharacters"] = "abcdefghijklmnopqrstuvwxyz0123456789@", + ["Identity:User:RequireUniqueEmail"] = "true" }; private readonly IConfiguration _configuration; @@ -34,8 +34,8 @@ public void Resolve_it_should_resolve_the_user_settings_correctly() IUserSettings settings = _resolver.Resolve(); Assert.Equal(1, _resolver.ReadCounter); - Assert.Equal(_settings["User:Password:HashingStrategy"], settings.Password.HashingStrategy); - Assert.Equal(_settings["User:UniqueName:AllowedCharacters"], settings.UniqueName.AllowedCharacters); + Assert.Equal(_settings["Identity:User:Password:HashingStrategy"], settings.Password.HashingStrategy); + Assert.Equal(_settings["Identity:User:UniqueName:AllowedCharacters"], settings.UniqueName.AllowedCharacters); Assert.True(settings.RequireUniqueEmail); } }