[ADD] Implementing services/handlers

This commit is contained in:
max 2024-12-25 22:50:04 +01:00
parent 361af34036
commit ebfafa2f29
31 changed files with 360 additions and 44 deletions

View File

@ -1,7 +1,7 @@
namespace DotBased.AspNet.Authority.Attributes; namespace DotBased.AspNet.Authority.Attributes;
/// <summary> /// <summary>
/// Indicates to protect the property before saving to the repository. /// Indicates to protect the property before saving/loading to the repository.
/// </summary> /// </summary>
[AttributeUsage(AttributeTargets.Property)] [AttributeUsage(AttributeTargets.Property)]
public class ProtectAttribute : Attribute public class ProtectAttribute : Attribute

View File

@ -1,20 +1,41 @@
using DotBased.AspNet.Authority.Interfaces; using DotBased.AspNet.Authority.Crypto;
using DotBased.AspNet.Authority.Models.Authority;
using DotBased.AspNet.Authority.Models.Options; using DotBased.AspNet.Authority.Models.Options;
using DotBased.AspNet.Authority.Services;
using DotBased.AspNet.Authority.Validators;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace DotBased.AspNet.Authority; namespace DotBased.AspNet.Authority;
public static class AuthorityProviderExtensions public static class AuthorityProviderExtensions
{ {
public static AuthorityBuilder AddAuthorityProvider<TModel>(this IServiceCollection services, Action<AuthorityOptions> optionsAction) where TModel : class public static AuthorityBuilder AddAuthority(this IServiceCollection services, Action<AuthorityOptions>? optionsAction = null)
=> services.AddAuthority<AuthorityUser, AuthorityGroup, AuthorityRole>(optionsAction);
public static AuthorityBuilder AddAuthority<TUser, TGroup, TRole>(this IServiceCollection services, Action<AuthorityOptions>? optionsAction = null)
where TUser : class where TGroup : class where TRole : class
{ {
services.AddOptions(); if (optionsAction != null)
// Configure required classes, services, etc. {
services.Configure<AuthorityOptions>(optionsAction); services.AddOptions();
services.Configure<AuthorityOptions>(optionsAction);
}
services.TryAddScoped<ICryptographer, Cryptographer>();
services.TryAddScoped<IPasswordHasher, PasswordHasher>();
services.TryAddScoped<IPasswordValidator<TUser>, PasswordOptionsValidator<TUser>>();
services.TryAddScoped<IUserValidator<TUser>, UserValidator<TUser>>();
/*services.TryAddScoped<IEmailVerifier, EmailVerifier>();
services.TryAddScoped<IPhoneNumberVerifier, PhoneNumberVerifier>();
services.TryAddScoped<IUserVerifier, UserVerifier>();*/
services.TryAddScoped<AuthorityManager>();
services.TryAddScoped<AuthorityUserManager<TUser>>();
services.TryAddScoped<AuthorityGroupManager<TGroup>>();
services.TryAddScoped<AuthorityRoleManager<TRole>>();
return new AuthorityBuilder(services); return new AuthorityBuilder(services);
} }
public static AuthorityBuilder AddAuthorityStore<TStore>(this AuthorityBuilder authorityBuilder) where TStore : IAuthorityRepository public static AuthorityBuilder AddAuthorityRepository<TRepository>(this AuthorityBuilder authorityBuilder) where TRepository : class
{ {
return authorityBuilder; return authorityBuilder;
} }

View File

@ -0,0 +1,14 @@
namespace DotBased.AspNet.Authority.Crypto;
public class Cryptographer : ICryptographer
{
public Task<string?> EncryptAsync(string data)
{
throw new NotImplementedException();
}
public Task<string?> DecryptAsync(string data)
{
throw new NotImplementedException();
}
}

View File

@ -0,0 +1,7 @@
namespace DotBased.AspNet.Authority.Crypto;
public interface ICryptographer
{
public Task<string?> EncryptAsync(string data);
public Task<string?> DecryptAsync(string data);
}

View File

@ -0,0 +1,6 @@
namespace DotBased.AspNet.Authority.Crypto;
public interface IPasswordHasher
{
public Task<string> HashPasswordAsync(string password);
}

View File

@ -0,0 +1,9 @@
namespace DotBased.AspNet.Authority.Crypto;
public class PasswordHasher : IPasswordHasher
{
public async Task<string> HashPasswordAsync(string password)
{
throw new NotImplementedException();
}
}

View File

@ -17,7 +17,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Authentication\" />
<Folder Include="Models\Security\" /> <Folder Include="Models\Security\" />
</ItemGroup> </ItemGroup>

View File

@ -1,6 +0,0 @@
namespace DotBased.AspNet.Authority.Interfaces;
public interface IAttributeRepository
{
}

View File

@ -1,6 +0,0 @@
namespace DotBased.AspNet.Authority.Interfaces;
public interface IAuthorityRepository
{
}

View File

@ -1,6 +0,0 @@
namespace DotBased.AspNet.Authority.Interfaces;
public interface IRoleRepository
{
}

View File

@ -6,5 +6,6 @@ public class AuthorityOptions
public LockoutOptions Lockout { get; set; } = new(); public LockoutOptions Lockout { get; set; } = new();
public PasswordOptions Password { get; set; } = new(); public PasswordOptions Password { get; set; } = new();
public ProviderOptions Provider { get; set; } = new(); public ProviderOptions Provider { get; set; } = new();
public RepositoryOptions Repository { get; set; } = new();
public UserOptions User { get; set; } = new(); public UserOptions User { get; set; } = new();
} }

View File

@ -0,0 +1,10 @@
namespace DotBased.AspNet.Authority.Models.Options;
public class RepositoryOptions
{
/// <summary>
/// Use data encryption when a property has the <see cref="DotBased.AspNet.Authority.Attributes.ProtectAttribute"/> defined.
/// <value>Default: true</value>
/// </summary>
public bool UseDataProtection { get; set; } = true;
}

View File

@ -0,0 +1,24 @@
namespace DotBased.AspNet.Authority.Models.Validation;
public class ValidationError
{
public ValidationError(string validator, string errorCode, string description)
{
Validator = validator;
ErrorCode = errorCode;
Description = description;
}
/// <summary>
/// The validator name that generated this error.
/// </summary>
public string Validator { get; }
/// <summary>
/// The error code
/// </summary>
public string ErrorCode { get; }
/// <summary>
/// Error description
/// </summary>
public string Description { get; }
}

View File

@ -0,0 +1,21 @@
namespace DotBased.AspNet.Authority.Models.Validation;
public class ValidationResult
{
public ValidationResult(bool success, IEnumerable<ValidationError>? errors = null)
{
if (errors != null)
{
Errors = errors.ToList();
}
Success = success;
}
public bool Success { get; }
public IReadOnlyList<ValidationError> Errors { get; } = [];
public static ValidationResult Failed(IEnumerable<ValidationError> errors) => new(false, errors);
public static ValidationResult Ok() => new(true);
public override string ToString() => Success ? "Success" : $"Failed ({Errors.Count} errors)";
}

View File

@ -1,6 +0,0 @@
namespace DotBased.AspNet.Authority.Repositories;
public class AuthorityRepository // Inherit the repository interfaces?
{
}

View File

@ -0,0 +1,6 @@
namespace DotBased.AspNet.Authority.Repositories;
public interface IAttributeRepository<TAttribute, TId> where TAttribute : class where TId : IEquatable<TId>
{
}

View File

@ -0,0 +1,7 @@
namespace DotBased.AspNet.Authority.Repositories;
public interface IAuthorityRepository
{
public Task<int> GetVersion();
public Task SetVersion(int version);
}

View File

@ -0,0 +1,6 @@
namespace DotBased.AspNet.Authority.Repositories;
public interface IGroupRepository<TGroup, TId> where TGroup : class where TId : IEquatable<TId>
{
}

View File

@ -0,0 +1,6 @@
namespace DotBased.AspNet.Authority.Repositories;
public interface IRoleRepository<TRole, TId> where TRole : class where TId : IEquatable<TId>
{
}

View File

@ -1,4 +1,4 @@
namespace DotBased.AspNet.Authority.Interfaces; namespace DotBased.AspNet.Authority.Repositories;
public interface IUserRepository<TUser, TId> where TUser : class where TId : IEquatable<TId> public interface IUserRepository<TUser, TId> where TUser : class where TId : IEquatable<TId>
{ {

View File

@ -0,0 +1,6 @@
namespace DotBased.AspNet.Authority.Services;
public class AuthorityGroupManager<TGroup>
{
}

View File

@ -1,6 +1,100 @@
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.Services;
public class AuthorityManager<TData> public class AuthorityManager
{ {
public AuthorityManager(
IOptions<AuthorityOptions> options,
IServiceProvider services,
IAuthorityRepository repository,
ICryptographer cryptographer)
{
_logger = LogService.RegisterLogger<AuthorityManager>();
Options = options.Value ?? new AuthorityOptions();
Services = services;
Repository = repository;
Cryptographer = cryptographer;
}
private readonly ILogger _logger;
public IServiceProvider Services { get; }
public AuthorityOptions Options { get; }
public IAuthorityRepository Repository { get; }
public ICryptographer Cryptographer { get; }
public long GenerateVersion() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); public long GenerateVersion() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
/// <summary>
/// Protect or unprotect the properties with the <see cref="ProtectAttribute"/>
/// </summary>
/// <param name="data">The data model</param>
/// <param name="protection">True for protection false for unprotection.</param>
/// <typeparam name="TModel">The class with the properties to protect.</typeparam>
public async Task HandlePropertyProtection<TModel>(TModel data, bool protection)
{
var props = GetProtectedPropertiesValues<TModel>(data);
if (Cryptographer == null)
{
_logger.Warning("No cryptographer specified! Cannot encrypt/decrypt properties.");
return;
}
if (props.Count == 0)
{
return;
}
var handledProperties = 0;
foreach (var property in props)
{
if (property.PropertyType != typeof(string))
{
_logger.Warning("Property({PropName}) with type: {PropType} detected, encrypting only supports strings! Skipping property!", property.Name, property.PropertyType);
continue;
}
string? cryptString;
if (protection)
{
cryptString = await Cryptographer.EncryptAsync(property.GetValue(data)?.ToString() ?? string.Empty);
}
else
{
cryptString = await Cryptographer.DecryptAsync(property.GetValue(data)?.ToString() ?? string.Empty);
}
if (cryptString == null)
{
_logger.Warning("{Protection} failed for property {PropName}", protection ? "Encyption" : "Decyption", property.Name);
continue;
}
property.SetValue(data, cryptString);
handledProperties++;
}
_logger.Debug("{HandledPropCount}/{TotalPropCount} protection properties handled!", handledProperties, props.Count);
}
public bool IsPropertieProtected<TModel>(string propertieName)
{
var protectedProperties = GetProtectedProperties<TModel>();
var propertieFound = protectedProperties.Where(propInfo => propInfo.Name == propertieName);
return propertieFound.Any();
}
public List<PropertyInfo> GetProtectedPropertiesValues<TModel>(TModel model)
{
var protectedProperties = GetProtectedProperties<TModel>();
return protectedProperties.Count != 0 ? protectedProperties : [];
}
public List<PropertyInfo> GetProtectedProperties<TModel>()
=> typeof(TModel).GetProperties().Where(p => Attribute.IsDefined(p, typeof(ProtectAttribute))).ToList();
} }

View File

@ -0,0 +1,6 @@
namespace DotBased.AspNet.Authority.Services;
public class AuthorityRoleManager<TRole>
{
}

View File

@ -0,0 +1,28 @@
using DotBased.AspNet.Authority.Validators;
using DotBased.Logging;
namespace DotBased.AspNet.Authority.Services;
public class AuthorityUserManager<TUser>
{
public AuthorityUserManager(
AuthorityManager manager,
IEnumerable<IPasswordValidator<TUser>>? passwordValidators,
IEnumerable<IUserValidator<TUser>>? userValidators)
{
_logger = LogService.RegisterLogger<AuthorityUserManager<TUser>>();
AuthorityManager = manager;
if (passwordValidators != null)
PasswordValidators = passwordValidators;
if (userValidators != null)
UserValidators = userValidators;
}
private readonly ILogger _logger;
public AuthorityManager AuthorityManager { get; }
public IEnumerable<IPasswordValidator<TUser>> PasswordValidators { get; } = [];
public IEnumerable<IUserValidator<TUser>> UserValidators { get; } = [];
}

View File

@ -1,6 +1,9 @@
using DotBased.AspNet.Authority.Models.Validation;
using DotBased.AspNet.Authority.Services;
namespace DotBased.AspNet.Authority.Validators; namespace DotBased.AspNet.Authority.Validators;
public interface IPasswordValidator<TUser> public interface IPasswordValidator<TUser>
{ {
public Task<ValidationResult> ValidatePasswordAsync(AuthorityUserManager<TUser> userManager, string password);
} }

View File

@ -1,6 +1,6 @@
namespace DotBased.AspNet.Authority.Validators; namespace DotBased.AspNet.Authority.Validators;
public interface IUserValidator public interface IUserValidator<TUser>
{ {
} }

View File

@ -0,0 +1,66 @@
using DotBased.AspNet.Authority.Models.Validation;
using DotBased.AspNet.Authority.Services;
using DotBased.Extensions;
namespace DotBased.AspNet.Authority.Validators;
/// <summary>
/// Validates the password against the options that is configured.
/// </summary>
/// <typeparam name="TUser">The user model used.</typeparam>
public class PasswordOptionsValidator<TUser> : IPasswordValidator<TUser>
{
private const string ValidatorId = "Authority.Validator.Password.Options";
private const string ValidationBase = "Authority.Validation.Password";
public async Task<ValidationResult> ValidatePasswordAsync(AuthorityUserManager<TUser> userManager, string password)
{
if (userManager == null)
{
throw new ArgumentNullException(nameof(userManager), "User manager is not provided!");
}
var passwordOptions = userManager.AuthorityManager.Options.Password;
var errors = new List<ValidationError>();
if (password.IsNullOrEmpty() || password.Length < passwordOptions.RequiredLength)
{
errors.Add(new ValidationError(ValidatorId, $"{ValidationBase}.Required.Length", $"Password needs to have a minimum length of {passwordOptions.RequiredLength}"));
}
if (passwordOptions.RequireDigit && !ContainsDigit(password))
{
errors.Add(new ValidationError(ValidatorId, $"{ValidationBase}.Required.Digit", "Password must contain a digit!"));
}
if (passwordOptions.RequireNonAlphanumeric && ContainsNonAlphanumeric(password))
{
errors.Add(new ValidationError(ValidatorId, $"{ValidationBase}.Required.NonAlphanumeric", "Password must contain a non alphanumeric character."));
}
if (passwordOptions.RequireLowercase && password.Any(char.IsLower))
{
errors.Add(new ValidationError(ValidatorId, $"{ValidationBase}.Required.Lowercase", "Password must contains at least one lowercase character."));
}
if (passwordOptions.RequireUppercase && password.Any(char.IsUpper))
{
errors.Add(new ValidationError(ValidatorId, $"{ValidationBase}.Required.Uppercase", "Password must contains at least one uppercase character."));
}
if (passwordOptions.PasswordBlackList.Count != 0 && passwordOptions.PasswordBlackList.Contains(password, passwordOptions.PasswordBlackListComparer))
{
errors.Add(new ValidationError(ValidatorId, $"{ValidationBase}.Blacklisted", "Given password is not allowed (blacklisted)"));
}
if (passwordOptions.MinimalUniqueChars > 0 && password.Distinct().Count() < passwordOptions.MinimalUniqueChars)
{
errors.Add(new ValidationError(ValidatorId, $"{ValidationBase}.UniqueChars", $"Password must contain at least {passwordOptions.MinimalUniqueChars} unique chars."));
}
return await Task.FromResult(errors.Count > 0 ? ValidationResult.Failed(errors) : ValidationResult.Ok());
}
private bool ContainsDigit(string strVal) => strVal.Any(char.IsDigit);
private bool ContainsNonAlphanumeric(string strVal) => !strVal.Any(char.IsLetterOrDigit);
}

View File

@ -1,6 +0,0 @@
namespace DotBased.AspNet.Authority.Validators;
public class PasswordValidator
{
}

View File

@ -1,6 +1,6 @@
namespace DotBased.AspNet.Authority.Validators; namespace DotBased.AspNet.Authority.Validators;
public class UserValidator public class UserValidator<TUser> : IUserValidator<TUser>
{ {
} }

View File

@ -1,6 +1,6 @@
namespace DotBased.AspNet.Authority.Verifiers; namespace DotBased.AspNet.Authority.Verifiers;
public class IUserVerifier public interface IUserVerifier
{ {
} }

View File

@ -1,3 +1,4 @@
using DotBased.AspNet.Authority;
using DotBased.Logging; using DotBased.Logging;
using DotBased.Logging.MEL; using DotBased.Logging.MEL;
using DotBased.Logging.Serilog; using DotBased.Logging.Serilog;
@ -19,6 +20,11 @@ LogService.AddLogAdapter(new BasedSerilogAdapter(serilogLogger));
builder.Logging.ClearProviders(); builder.Logging.ClearProviders();
builder.Logging.AddDotBasedLoggerProvider(LogService.Options); builder.Logging.AddDotBasedLoggerProvider(LogService.Options);
builder.Services.AddAuthority(options =>
{
});
/*builder.Services.AddAuthentication(options => /*builder.Services.AddAuthentication(options =>
{ {
options.DefaultScheme = BasedAuthenticationDefaults.BasedAuthenticationScheme; options.DefaultScheme = BasedAuthenticationDefaults.BasedAuthenticationScheme;