From efc82599304e43ac44b3cb781c90ff1771af9f5b Mon Sep 17 00:00:00 2001 From: max Date: Fri, 3 Jan 2025 00:14:12 +0100 Subject: [PATCH] [CHANGE] Implementing managers. repositories --- .../AuthorityProviderExtensions.cs | 2 +- .../AuthorityGroupManager.cs | 2 +- .../AuthorityManager.cs | 10 +- .../AuthorityRoleManager.cs | 2 +- .../Managers/AuthorityUserManager.cs | 97 +++++++++++++++++++ .../Models/AuthorityResult.cs | 38 ++++++++ .../Models/Options/ListOption.cs | 7 ++ .../Models/Options/UserOptions.cs | 3 +- .../Repositories/IAuthorityRepository.cs | 7 -- .../Repositories/IUserRepository.cs | 10 +- .../Repositories/RepositoryManager.cs | 13 --- .../Services/AuthorityUserManager.cs | 50 ---------- .../Validators/IPasswordValidator.cs | 2 +- .../Validators/IUserValidator.cs | 7 +- .../Validators/PasswordEqualsValidator.cs | 6 +- .../Validators/PasswordOptionsValidator.cs | 2 +- .../Validators/UserValidator.cs | 86 +++++++++++++++- 17 files changed, 252 insertions(+), 92 deletions(-) rename DotBased.AspNet.Authority/{Services => Managers}/AuthorityGroupManager.cs (52%) rename DotBased.AspNet.Authority/{Services => Managers}/AuthorityManager.cs (89%) rename DotBased.AspNet.Authority/{Services => Managers}/AuthorityRoleManager.cs (51%) create mode 100644 DotBased.AspNet.Authority/Managers/AuthorityUserManager.cs create mode 100644 DotBased.AspNet.Authority/Models/AuthorityResult.cs create mode 100644 DotBased.AspNet.Authority/Models/Options/ListOption.cs delete mode 100644 DotBased.AspNet.Authority/Repositories/IAuthorityRepository.cs delete mode 100644 DotBased.AspNet.Authority/Repositories/RepositoryManager.cs delete mode 100644 DotBased.AspNet.Authority/Services/AuthorityUserManager.cs diff --git a/DotBased.AspNet.Authority/AuthorityProviderExtensions.cs b/DotBased.AspNet.Authority/AuthorityProviderExtensions.cs index 7789c7b..ab2b8fc 100644 --- a/DotBased.AspNet.Authority/AuthorityProviderExtensions.cs +++ b/DotBased.AspNet.Authority/AuthorityProviderExtensions.cs @@ -1,7 +1,7 @@ using DotBased.AspNet.Authority.Crypto; +using DotBased.AspNet.Authority.Managers; using DotBased.AspNet.Authority.Models.Authority; using DotBased.AspNet.Authority.Models.Options; -using DotBased.AspNet.Authority.Services; using DotBased.AspNet.Authority.Validators; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; diff --git a/DotBased.AspNet.Authority/Services/AuthorityGroupManager.cs b/DotBased.AspNet.Authority/Managers/AuthorityGroupManager.cs similarity index 52% rename from DotBased.AspNet.Authority/Services/AuthorityGroupManager.cs rename to DotBased.AspNet.Authority/Managers/AuthorityGroupManager.cs index b8db5ea..a4dffbe 100644 --- a/DotBased.AspNet.Authority/Services/AuthorityGroupManager.cs +++ b/DotBased.AspNet.Authority/Managers/AuthorityGroupManager.cs @@ -1,4 +1,4 @@ -namespace DotBased.AspNet.Authority.Services; +namespace DotBased.AspNet.Authority.Managers; public class AuthorityGroupManager { diff --git a/DotBased.AspNet.Authority/Services/AuthorityManager.cs b/DotBased.AspNet.Authority/Managers/AuthorityManager.cs similarity index 89% rename from DotBased.AspNet.Authority/Services/AuthorityManager.cs rename to DotBased.AspNet.Authority/Managers/AuthorityManager.cs index c313772..b80e1e3 100644 --- a/DotBased.AspNet.Authority/Services/AuthorityManager.cs +++ b/DotBased.AspNet.Authority/Managers/AuthorityManager.cs @@ -2,24 +2,21 @@ using System.Reflection; using DotBased.AspNet.Authority.Attributes; using DotBased.AspNet.Authority.Crypto; using DotBased.AspNet.Authority.Models.Options; -using DotBased.AspNet.Authority.Repositories; using DotBased.Logging; using Microsoft.Extensions.Options; -namespace DotBased.AspNet.Authority.Services; +namespace DotBased.AspNet.Authority.Managers; public class AuthorityManager { public AuthorityManager( IOptions options, IServiceProvider services, - IAuthorityRepository repository, ICryptographer cryptographer) { _logger = LogService.RegisterLogger(); Options = options.Value ?? new AuthorityOptions(); Services = services; - Repository = repository; Cryptographer = cryptographer; } @@ -27,7 +24,6 @@ public class AuthorityManager public IServiceProvider Services { get; } public AuthorityOptions Options { get; } - public IAuthorityRepository Repository { get; } public ICryptographer Cryptographer { get; } @@ -38,7 +34,7 @@ public class AuthorityManager /// Protect or unprotect the properties with the /// /// The data model - /// True for protection false for unprotection. + /// True for protect false for unprotect. /// The class with the properties to protect. public async Task HandlePropertyProtection(TModel data, bool protection) { @@ -74,7 +70,7 @@ public class AuthorityManager if (cryptString == null) { - _logger.Warning("{Protection} failed for property {PropName}", protection ? "Encyption" : "Decyption", property.Name); + _logger.Warning("{Protection} failed for property {PropName}", protection ? "Encryption" : "Decryption", property.Name); continue; } property.SetValue(data, cryptString); diff --git a/DotBased.AspNet.Authority/Services/AuthorityRoleManager.cs b/DotBased.AspNet.Authority/Managers/AuthorityRoleManager.cs similarity index 51% rename from DotBased.AspNet.Authority/Services/AuthorityRoleManager.cs rename to DotBased.AspNet.Authority/Managers/AuthorityRoleManager.cs index d3b8e7d..c41b6d2 100644 --- a/DotBased.AspNet.Authority/Services/AuthorityRoleManager.cs +++ b/DotBased.AspNet.Authority/Managers/AuthorityRoleManager.cs @@ -1,4 +1,4 @@ -namespace DotBased.AspNet.Authority.Services; +namespace DotBased.AspNet.Authority.Managers; public class AuthorityRoleManager { diff --git a/DotBased.AspNet.Authority/Managers/AuthorityUserManager.cs b/DotBased.AspNet.Authority/Managers/AuthorityUserManager.cs new file mode 100644 index 0000000..b5a9003 --- /dev/null +++ b/DotBased.AspNet.Authority/Managers/AuthorityUserManager.cs @@ -0,0 +1,97 @@ +using DotBased.AspNet.Authority.Crypto; +using DotBased.AspNet.Authority.Models; +using DotBased.AspNet.Authority.Models.Authority; +using DotBased.AspNet.Authority.Models.Validation; +using DotBased.AspNet.Authority.Repositories; +using DotBased.AspNet.Authority.Validators; +using DotBased.Logging; + +namespace DotBased.AspNet.Authority.Managers; + +public class AuthorityUserManager where TUser : class +{ + public AuthorityUserManager( + AuthorityManager manager, + IUserRepository userRepository, + IPasswordHasher passwordHasher, + IEnumerable>? passwordValidators, + IEnumerable>? userValidators) + { + _logger = LogService.RegisterLogger>(); + AuthorityManager = manager; + UserRepository = userRepository; + PasswordHasher = passwordHasher; + if (passwordValidators != null) + PasswordValidators = passwordValidators; + if (userValidators != null) + UserValidators = userValidators; + } + + private readonly ILogger _logger; + public AuthorityManager AuthorityManager { get; } + public IUserRepository UserRepository { get; } + + public IPasswordHasher PasswordHasher { get; } + + public IEnumerable> PasswordValidators { get; } = []; + public IEnumerable> UserValidators { get; } = []; + + public async Task ValidatePasswordAsync(TUser user, string password) + { + List errors = []; + foreach (var validator in PasswordValidators) + { + var validatorResult = await validator.ValidatePasswordAsync(this, user, password); + if (!validatorResult.Success) + { + errors.AddRange(validatorResult.Errors); + } + } + return errors.Count > 0 ? ValidationResult.Failed(errors) : ValidationResult.Ok(); + } + + public async Task ValidateUserAsync(TUser user) + { + List errors = []; + foreach (var userValidator in UserValidators) + { + var validationResult = await userValidator.ValidateUserAsync(this, user); + if (!validationResult.Success) + { + errors.AddRange(validationResult.Errors); + } + } + return errors.Count > 0 ? ValidationResult.Failed(errors) : ValidationResult.Ok(); + } + + public async Task> CreateUserAsync(TUser userModel, string password) + { + if (userModel is not AuthorityUserBase userBase) + { + return AuthorityResult.Error($"Given user is not of base type {nameof(AuthorityUserBase)}!"); + } + + var userValidation = await ValidateUserAsync(userModel); + var passwordValidation = await ValidatePasswordAsync(userModel, password); + if (!userValidation.Success || !passwordValidation.Success) + { + List errors = []; + errors.AddRange(userValidation.Errors); + errors.AddRange(passwordValidation.Errors); + return AuthorityResult.Failed(errors, ResultFailReason.Validation); + } + + var version = AuthorityManager.GenerateVersion(); + userBase.Version = version; + var securityVersion = AuthorityManager.GenerateVersion(); + userBase.SecurityVersion = securityVersion; + var hashedPassword = await PasswordHasher.HashPasswordAsync(password); + userBase.PasswordHash = hashedPassword; + + var userCreationResult = await UserRepository.CreateUserAsync(userModel); + + return userCreationResult != null + ? AuthorityResult.Ok(userCreationResult) + : AuthorityResult.Error("Failed to create user in repository!"); + } +} \ No newline at end of file diff --git a/DotBased.AspNet.Authority/Models/AuthorityResult.cs b/DotBased.AspNet.Authority/Models/AuthorityResult.cs new file mode 100644 index 0000000..f6c6d35 --- /dev/null +++ b/DotBased.AspNet.Authority/Models/AuthorityResult.cs @@ -0,0 +1,38 @@ +using DotBased.AspNet.Authority.Models.Validation; + +namespace DotBased.AspNet.Authority.Models; + +public class AuthorityResult +{ + public AuthorityResult(bool success, string errorMessage = "", TResultValue? value = default, ResultFailReason reason = ResultFailReason.None, List? errors = null) + { + Success = success; + ErrorMessage = errorMessage; + Value = value; + Reason = reason; + ValidationErrors = errors; + } + + public bool Success { get; } + public string ErrorMessage { get; } + public TResultValue? Value { get; } + public ResultFailReason Reason { get; } + public List? ValidationErrors { get; } + + + public static AuthorityResult Ok(TResultValue? value) => new AuthorityResult(true, value:value); + + public static AuthorityResult Error(string errorMessage, ResultFailReason reason = ResultFailReason.Error) => + new AuthorityResult(false, errorMessage, reason:reason); + + public static AuthorityResult Failed(List errors, ResultFailReason reason = ResultFailReason.None) + => new AuthorityResult(false, errors:errors, reason:reason); +} + +public enum ResultFailReason +{ + None, + Unknown, + Validation, + Error +} \ No newline at end of file diff --git a/DotBased.AspNet.Authority/Models/Options/ListOption.cs b/DotBased.AspNet.Authority/Models/Options/ListOption.cs new file mode 100644 index 0000000..3c43bf2 --- /dev/null +++ b/DotBased.AspNet.Authority/Models/Options/ListOption.cs @@ -0,0 +1,7 @@ +namespace DotBased.AspNet.Authority.Models.Options; + +public enum ListOption +{ + Blacklist, + Whitelist +} \ No newline at end of file diff --git a/DotBased.AspNet.Authority/Models/Options/UserOptions.cs b/DotBased.AspNet.Authority/Models/Options/UserOptions.cs index 54011e7..c483bab 100644 --- a/DotBased.AspNet.Authority/Models/Options/UserOptions.cs +++ b/DotBased.AspNet.Authority/Models/Options/UserOptions.cs @@ -4,7 +4,8 @@ public class UserOptions { public bool EnableRegister { get; set; } public bool RequireUniqueEmail { get; set; } - public string AllowedCharacters { get; set; } = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@"; + public string UserNameCharacters { get; set; } = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@"; + public ListOption UserNameCharacterListType { get; set; } = ListOption.Whitelist; public List UserNameBlackList { get; set; } = ["admin", "administrator", "dev", "developer"]; public StringComparer UserNameBlackListComparer { get; set; } = StringComparer.OrdinalIgnoreCase; diff --git a/DotBased.AspNet.Authority/Repositories/IAuthorityRepository.cs b/DotBased.AspNet.Authority/Repositories/IAuthorityRepository.cs deleted file mode 100644 index da76f55..0000000 --- a/DotBased.AspNet.Authority/Repositories/IAuthorityRepository.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace DotBased.AspNet.Authority.Repositories; - -public interface IAuthorityRepository -{ - public Task GetVersion(); - public Task SetVersion(long version); -} \ No newline at end of file diff --git a/DotBased.AspNet.Authority/Repositories/IUserRepository.cs b/DotBased.AspNet.Authority/Repositories/IUserRepository.cs index 0aa9893..8385b26 100644 --- a/DotBased.AspNet.Authority/Repositories/IUserRepository.cs +++ b/DotBased.AspNet.Authority/Repositories/IUserRepository.cs @@ -4,6 +4,12 @@ public interface IUserRepository where TUser : class { public Task GetUserByIdAsync(string id); public Task GetUserIdAsync(TUser user); - public Task SetVersion(TUser user, long version); - public Task SetSecurityVersion(TUser user, long version); + public Task GetUserByEmailAsync(string email); + public Task SetVersionAsync(TUser user, long version); + public Task GetVersionAsync(TUser user); + public Task SetSecurityVersionAsync(TUser user, long version); + public Task GetSecurityVersionAsync(TUser user); + public Task CreateUserAsync(TUser user); + public Task UpdateUserAsync(TUser user); + public Task DeleteUserAsync(TUser user); } \ No newline at end of file diff --git a/DotBased.AspNet.Authority/Repositories/RepositoryManager.cs b/DotBased.AspNet.Authority/Repositories/RepositoryManager.cs deleted file mode 100644 index 952a7b2..0000000 --- a/DotBased.AspNet.Authority/Repositories/RepositoryManager.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace DotBased.AspNet.Authority.Repositories; - -public class RepositoryManager where TUser : class where TGroup : class -{ - public RepositoryManager(IUserRepository userRepository, IGroupRepository groupRepository) - { - UserRepository = userRepository; - GroupRepository = groupRepository; - } - - public IUserRepository UserRepository { get; set; } - public IGroupRepository GroupRepository { get; set; } -} \ No newline at end of file diff --git a/DotBased.AspNet.Authority/Services/AuthorityUserManager.cs b/DotBased.AspNet.Authority/Services/AuthorityUserManager.cs deleted file mode 100644 index a874760..0000000 --- a/DotBased.AspNet.Authority/Services/AuthorityUserManager.cs +++ /dev/null @@ -1,50 +0,0 @@ -using DotBased.AspNet.Authority.Crypto; -using DotBased.AspNet.Authority.Models.Validation; -using DotBased.AspNet.Authority.Repositories; -using DotBased.AspNet.Authority.Validators; -using DotBased.Logging; - -namespace DotBased.AspNet.Authority.Services; - -public class AuthorityUserManager where TUser : class -{ - public AuthorityUserManager( - AuthorityManager manager, - IUserRepository userRepository, - IPasswordHasher passwordHasher, - IEnumerable>? passwordValidators, - IEnumerable>? userValidators) - { - _logger = LogService.RegisterLogger>(); - AuthorityManager = manager; - UserRepository = userRepository; - PasswordHasher = passwordHasher; - if (passwordValidators != null) - PasswordValidators = passwordValidators; - if (userValidators != null) - UserValidators = userValidators; - } - - private readonly ILogger _logger; - public AuthorityManager AuthorityManager { get; } - public IUserRepository UserRepository { get; } - - public IPasswordHasher PasswordHasher { get; } - - public IEnumerable> PasswordValidators { get; } = []; - public IEnumerable> UserValidators { get; } = []; - - public async Task ValidatePasswordAsync(TUser user, string password) - { - List errors = []; - foreach (var validator in PasswordValidators) - { - var validatorResult = await validator.ValidatePasswordAsync(this, user, password); - if (!validatorResult.Success) - { - errors.AddRange(validatorResult.Errors); - } - } - return errors.Count > 0 ? ValidationResult.Failed(errors) : ValidationResult.Ok(); - } -} \ No newline at end of file diff --git a/DotBased.AspNet.Authority/Validators/IPasswordValidator.cs b/DotBased.AspNet.Authority/Validators/IPasswordValidator.cs index 035038d..1f6304f 100644 --- a/DotBased.AspNet.Authority/Validators/IPasswordValidator.cs +++ b/DotBased.AspNet.Authority/Validators/IPasswordValidator.cs @@ -1,5 +1,5 @@ +using DotBased.AspNet.Authority.Managers; using DotBased.AspNet.Authority.Models.Validation; -using DotBased.AspNet.Authority.Services; namespace DotBased.AspNet.Authority.Validators; diff --git a/DotBased.AspNet.Authority/Validators/IUserValidator.cs b/DotBased.AspNet.Authority/Validators/IUserValidator.cs index 96511d6..a2b3f69 100644 --- a/DotBased.AspNet.Authority/Validators/IUserValidator.cs +++ b/DotBased.AspNet.Authority/Validators/IUserValidator.cs @@ -1,6 +1,9 @@ +using DotBased.AspNet.Authority.Managers; +using DotBased.AspNet.Authority.Models.Validation; + namespace DotBased.AspNet.Authority.Validators; -public interface IUserValidator +public interface IUserValidator where TUser : class { - + public Task ValidateUserAsync(AuthorityUserManager manager, TUser user); } \ No newline at end of file diff --git a/DotBased.AspNet.Authority/Validators/PasswordEqualsValidator.cs b/DotBased.AspNet.Authority/Validators/PasswordEqualsValidator.cs index 1d5071a..b91de4a 100644 --- a/DotBased.AspNet.Authority/Validators/PasswordEqualsValidator.cs +++ b/DotBased.AspNet.Authority/Validators/PasswordEqualsValidator.cs @@ -1,6 +1,6 @@ +using DotBased.AspNet.Authority.Managers; using DotBased.AspNet.Authority.Models.Authority; using DotBased.AspNet.Authority.Models.Validation; -using DotBased.AspNet.Authority.Services; namespace DotBased.AspNet.Authority.Validators; @@ -10,9 +10,9 @@ public class PasswordEqualsValidator : IPasswordValidator where TU private const string ValidationBase = "Authority.Validation.Password"; public async Task ValidatePasswordAsync(AuthorityUserManager userManager, TUser user, string password) { - if (user == null || user is not AuthorityUserBase authorityUser) + if (user is not AuthorityUserBase authorityUser) { - throw new ArgumentException("Invalid user given!", nameof(user)); + throw new ArgumentException($"User is not type of {nameof(AuthorityUserBase)}!", nameof(user)); } List errors = []; diff --git a/DotBased.AspNet.Authority/Validators/PasswordOptionsValidator.cs b/DotBased.AspNet.Authority/Validators/PasswordOptionsValidator.cs index 6bfcad0..65de9b2 100644 --- a/DotBased.AspNet.Authority/Validators/PasswordOptionsValidator.cs +++ b/DotBased.AspNet.Authority/Validators/PasswordOptionsValidator.cs @@ -1,5 +1,5 @@ +using DotBased.AspNet.Authority.Managers; using DotBased.AspNet.Authority.Models.Validation; -using DotBased.AspNet.Authority.Services; using DotBased.Extensions; namespace DotBased.AspNet.Authority.Validators; diff --git a/DotBased.AspNet.Authority/Validators/UserValidator.cs b/DotBased.AspNet.Authority/Validators/UserValidator.cs index 219bccb..e23183b 100644 --- a/DotBased.AspNet.Authority/Validators/UserValidator.cs +++ b/DotBased.AspNet.Authority/Validators/UserValidator.cs @@ -1,6 +1,88 @@ +using DotBased.AspNet.Authority.Managers; +using DotBased.AspNet.Authority.Models.Authority; +using DotBased.AspNet.Authority.Models.Options; +using DotBased.AspNet.Authority.Models.Validation; + namespace DotBased.AspNet.Authority.Validators; -public class UserValidator : IUserValidator +public class UserValidator : IUserValidator where TUser : class { - + private const string ValidatorId = "Authority.Validator.User"; + private const string ValidationBase = "Authority.Validation.User"; + + public async Task ValidateUserAsync(AuthorityUserManager manager, TUser user) + { + List errors = []; + + var userOptions = manager.AuthorityManager.Options.User; + + if (user is not AuthorityUserBase userBase) + { + errors.Add(new ValidationError(ValidatorId, $"{ValidationBase}.NotAuthorityUser", + $"Given user model is not an type of {nameof(AuthorityUserBase)}")); + return ValidationResult.Failed(errors); + } + + if (userOptions.RequireUniqueEmail) + { + if (string.IsNullOrWhiteSpace(userBase.EmailAddress)) + { + errors.Add(new ValidationError(ValidatorId, $"{ValidationBase}.NoEmail", + $"Option {nameof(UserOptions.RequireUniqueEmail)} is set to true but given user does not have an email address!")); + } + else + { + var userEmailResult = await manager.UserRepository.GetUserByEmailAsync(userBase.EmailAddress); + if (userEmailResult != null) + { + errors.Add(new ValidationError(ValidatorId, $"{ValidationBase}.EmailExists", + "Given email has already registered an account!")); + } + } + } + + if (!string.IsNullOrWhiteSpace(userBase.UserName)) + { + if (userOptions.UserNameBlackList.Count != 0 && userOptions.UserNameBlackList.Contains(userBase.UserName, userOptions.UserNameBlackListComparer)) + { + errors.Add(new ValidationError(ValidatorId, $"{ValidationBase}.Blacklisted", "Given username is not allowed (blacklisted)")); + } + + if (!string.IsNullOrWhiteSpace(userOptions.UserNameCharacters)) + { + List chars = []; + if (userOptions.UserNameCharacterListType == ListOption.Whitelist) + { + chars.AddRange(userBase.UserName.Where(userNameChar => !userOptions.UserNameCharacters.Contains(userNameChar))); + } + if (userOptions.UserNameCharacterListType == ListOption.Blacklist) + { + chars.AddRange(userBase.UserName.Where(userNameChar => userOptions.UserNameCharacters.Contains(userNameChar))); + } + + if (chars.Count <= 0) return errors.Count > 0 ? ValidationResult.Failed(errors) : ValidationResult.Ok(); + var errorCode = ""; + var description = ""; + switch (userOptions.UserNameCharacterListType) + { + case ListOption.Whitelist: + errorCode = "CharactersNotOnWhitelist"; + description = $"Found characters in username that were not on the whitelist! Chars: [{string.Join(',', chars)}]"; + break; + case ListOption.Blacklist: + errorCode = "CharactersOnBlacklist"; + description = $"Found characters in username that are on the blacklist! Chars: [{string.Join(',', chars)}]"; + break; + } + + errors.Add(new ValidationError(ValidatorId, $"{ValidationBase}.UserName.{errorCode}", description)); + } + } + else + { + errors.Add(new ValidationError(ValidatorId, $"{ValidationBase}.InvalidUserName", "No username given!")); + } + + return errors.Count > 0 ? ValidationResult.Failed(errors) : ValidationResult.Ok(); + } } \ No newline at end of file