Skip to content

Commit

Permalink
Refactored Identity settings. (#55)
Browse files Browse the repository at this point in the history
* Refactored settings configuration.

* Refactored the PasswordManager.

* Refactored UserManager.
  • Loading branch information
Utar94 authored Mar 5, 2024
1 parent eb2015e commit 5d154bc
Show file tree
Hide file tree
Showing 10 changed files with 164 additions and 21 deletions.
25 changes: 25 additions & 0 deletions src/Logitar.Identity.Demo/Controllers/SettingsController.cs
Original file line number Diff line number Diff line change
@@ -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<IRoleSettings> GetRoleSettings() => Ok(_roleSettings.Resolve());

[HttpGet("user")]
public ActionResult<IUserSettings> GetUserSettings() => Ok(_userSettings.Resolve());
}
23 changes: 23 additions & 0 deletions src/Logitar.Identity.Demo/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
25 changes: 24 additions & 1 deletion src/Logitar.Identity.Domain/Passwords/IPasswordManager.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace Logitar.Identity.Domain.Passwords;
using Logitar.Identity.Contracts.Settings;

namespace Logitar.Identity.Domain.Passwords;

/// <summary>
/// Defines methods to manage passwords.
Expand All @@ -12,6 +14,13 @@ public interface IPasswordManager
/// <returns>The password instance.</returns>
Password Create(string password);
/// <summary>
/// Creates a password from the specified string. This method should not be used to create strong user passwords.
/// </summary>
/// <param name="password">The password string.</param>
/// <param name="passwordSettings">The password settings.</param>
/// <returns>The password instance.</returns>
Password Create(string password, IPasswordSettings? passwordSettings);
/// <summary>
/// Decodes a password from the encoded string.
/// </summary>
/// <param name="password">The encoded password.</param>
Expand Down Expand Up @@ -46,10 +55,24 @@ public interface IPasswordManager
/// <param name="password">The password string.</param>
void Validate(string password);
/// <summary>
/// Validates the specified password string.
/// </summary>
/// <param name="password">The password string.</param>
/// <param name="passwordSettings">The password settings.</param>
void Validate(string password, IPasswordSettings? passwordSettings);
/// <summary>
/// Validates the specified password string, then creates a password if it is valid, or throws an exception otherwise.
/// </summary>
/// <param name="password">The password string.</param>
/// <returns>The password instance.</returns>
/// <exception cref="FluentValidation.ValidationException">The password is too weak.</exception>
Password ValidateAndCreate(string password);
/// <summary>
/// Validates the specified password string, then creates a password if it is valid, or throws an exception otherwise.
/// </summary>
/// <param name="password">The password string.</param>
/// <param name="passwordSettings">The password settings.</param>
/// <returns>The password instance.</returns>
/// <exception cref="FluentValidation.ValidationException">The password is too weak.</exception>
Password ValidateAndCreate(string password, IPasswordSettings? passwordSettings);
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public RoleSettingsResolver(IConfiguration configuration)
/// <returns>The role settings.</returns>
public virtual IRoleSettings Resolve()
{
RoleSettings ??= Configuration.GetSection("Role").Get<RoleSettings>() ?? new();
RoleSettings ??= Configuration.GetSection("Identity").GetSection("Role").Get<RoleSettings>() ?? new();
return RoleSettings;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public UserSettingsResolver(IConfiguration configuration)
/// <returns>The user settings.</returns>
public virtual IUserSettings Resolve()
{
UserSettings ??= Configuration.GetSection("User").Get<UserSettings>() ?? new();
UserSettings ??= Configuration.GetSection("Identity").GetSection("User").Get<UserSettings>() ?? new();
return UserSettings;
}
}
19 changes: 19 additions & 0 deletions src/Logitar.Identity.Domain/Users/IUserManager.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Logitar.EventSourcing;
using Logitar.Identity.Contracts.Settings;

namespace Logitar.Identity.Domain.Users;

Expand All @@ -15,6 +16,15 @@ public interface IUserManager
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The found users.</returns>
Task<FoundUsers> FindAsync(string? tenantId, string id, CancellationToken cancellationToken = default);
/// <summary>
/// Tries finding an user by its unique identifier, unique name, or email address if they are unique.
/// </summary>
/// <param name="tenantId">The identifier of the tenant in which to search.</param>
/// <param name="id">The identifier of the user to find.</param>
/// <param name="userSettings">The user settings.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The found users.</returns>
Task<FoundUsers> FindAsync(string? tenantId, string id, IUserSettings? userSettings, CancellationToken cancellationToken = default);

/// <summary>
/// Saves the specified user, performing model validation such as unique name and email address unicity.
Expand All @@ -24,4 +34,13 @@ public interface IUserManager
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The asynchronous operation.</returns>
Task SaveAsync(UserAggregate user, ActorId actorId = default, CancellationToken cancellationToken = default);
/// <summary>
/// Saves the specified user, performing model validation such as unique name and email address unicity.
/// </summary>
/// <param name="user">The user to save.</param>
/// <param name="userSettings">The user settings.</param>
/// <param name="actorId">The actor identifier.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The asynchronous operation.</returns>
Task SaveAsync(UserAggregate user, IUserSettings? userSettings, ActorId actorId = default, CancellationToken cancellationToken = default);
}
28 changes: 26 additions & 2 deletions src/Logitar.Identity.Domain/Users/UserManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,19 @@ public UserManager(ISessionRepository sessionRepository, IUserRepository userRep
/// <returns>The found users.</returns>
public virtual async Task<FoundUsers> FindAsync(string? tenantIdValue, string id, CancellationToken cancellationToken)
{
IUserSettings userSettings = UserSettingsResolver.Resolve();
return await FindAsync(tenantIdValue, id, userSettings: null, cancellationToken);
}
/// <summary>
/// Tries finding an user by its unique identifier, unique name, or email address if they are unique.
/// </summary>
/// <param name="tenantIdValue">The identifier of the tenant in which to search.</param>
/// <param name="id">The identifier of the user to find.</param>
/// <param name="userSettings">The user settings.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The found users.</returns>
public virtual async Task<FoundUsers> FindAsync(string? tenantIdValue, string id, IUserSettings? userSettings, CancellationToken cancellationToken)
{
userSettings ??= UserSettingsResolver.Resolve();

TenantId? tenantId = null;
try
Expand Down Expand Up @@ -116,6 +128,18 @@ public virtual async Task<FoundUsers> FindAsync(string? tenantIdValue, string id
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The asynchronous operation.</returns>
public virtual async Task SaveAsync(UserAggregate user, ActorId actorId, CancellationToken cancellationToken)
{
await SaveAsync(user, userSettings: null, actorId, cancellationToken);
}
/// <summary>
/// Saves the specified user, performing model validation such as unique name and email address unicity.
/// </summary>
/// <param name="user">The user to save.</param>
/// <param name="userSettings">The user settings.</param>
/// <param name="actorId">The actor identifier.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The asynchronous operation.</returns>
public virtual async Task SaveAsync(UserAggregate user, IUserSettings? userSettings, ActorId actorId, CancellationToken cancellationToken)
{
bool hasBeenDeleted = false;
bool hasEmailChanged = false;
Expand Down Expand Up @@ -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<UserAggregate> users = await UserRepository.LoadAsync(user.TenantId, user.Email, cancellationToken);
Expand Down
47 changes: 38 additions & 9 deletions src/Logitar.Identity.Infrastructure/Passwords/PasswordManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,17 @@ public PasswordManager(IUserSettingsResolver settingsResolver, IEnumerable<IPass
/// <returns>The password instance.</returns>
public virtual Password Create(string password)
{
IPasswordSettings passwordSettings = SettingsResolver.Resolve().Password;
return Create(password, passwordSettings: null);
}
/// <summary>
/// Creates a password from the specified string. This method should not be used to create strong user passwords.
/// </summary>
/// <param name="password">The password string.</param>
/// <param name="passwordSettings">The password settings.</param>
/// <returns>The password instance.</returns>
public virtual Password Create(string password, IPasswordSettings? passwordSettings)
{
passwordSettings ??= SettingsResolver.Resolve().Password;
return GetStrategy(passwordSettings.HashingStrategy).Create(password);
}

Expand All @@ -65,7 +75,7 @@ public virtual Password Decode(string password)
/// <param name="length">The length of the password, in number of characters.</param>
/// <param name="password">The password string.</param>
/// <returns>The password instance.</returns>
public Password Generate(int length, out string password)
public virtual Password Generate(int length, out string password)
{
password = RandomStringGenerator.GetString(length);
return Create(password);
Expand All @@ -77,7 +87,7 @@ public Password Generate(int length, out string password)
/// <param name="length">The length of the password, in number of characters.</param>
/// <param name="password">The password string.</param>
/// <returns>The password instance.</returns>
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);
Expand All @@ -89,7 +99,7 @@ public Password Generate(string characters, int length, out string password)
/// <param name="length">The length of the password, in number of bytes.</param>
/// <param name="password">The password string.</param>
/// <returns>The password instance.</returns>
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);
Expand All @@ -99,9 +109,17 @@ public Password GenerateBase64(int length, out string password)
/// Validates the specified password string.
/// </summary>
/// <param name="password">The password string.</param>
public void Validate(string password)
public virtual void Validate(string password)
{
Validate(password, passwordSettings: null);
}
/// <summary>
/// Validates the specified password string.
/// </summary>
/// <param name="password">The password string.</param>
public virtual void Validate(string password, IPasswordSettings? passwordSettings)
{
IPasswordSettings passwordSettings = SettingsResolver.Resolve().Password;
passwordSettings ??= SettingsResolver.Resolve().Password;
new PasswordValidator(passwordSettings).ValidateAndThrow(password);
}

Expand All @@ -111,10 +129,21 @@ public void Validate(string password)
/// <param name="password">The password string.</param>
/// <returns>The password instance.</returns>
/// <exception cref="ValidationException">The password is too weak.</exception>
public Password ValidateAndCreate(string password)
public virtual Password ValidateAndCreate(string password)
{
Validate(password);
return Create(password);
return ValidateAndCreate(password, passwordSettings: null);
}
/// <summary>
/// Validates the specified password string, then creates a password if it is valid, or throws an exception otherwise.
/// </summary>
/// <param name="password">The password string.</param>
/// <param name="passwordSettings">The password settings.</param>
/// <returns>The password instance.</returns>
/// <exception cref="ValidationException">The password is too weak.</exception>
public virtual Password ValidateAndCreate(string password, IPasswordSettings? passwordSettings)
{
Validate(password, passwordSettings);
return Create(password, passwordSettings);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public class RoleSettingsResolverTests
{
private readonly Dictionary<string, string?> _settings = new()
{
["Role:UniqueName:AllowedCharacters"] = "abcdefghijklmnopqrstuvwxyz_"
["Identity:Role:UniqueName:AllowedCharacters"] = "abcdefghijklmnopqrstuvwxyz_"
};

private readonly IConfiguration _configuration;
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ public class UserSettingsResolverTests
{
private readonly Dictionary<string, string?> _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;
Expand All @@ -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);
}
}

0 comments on commit 5d154bc

Please sign in to comment.