diff --git a/TodoSample/Core/Application.Contracts/TodoItems/Commands/CreateTodoItemCommand.cs b/TodoSample/Core/Application.Contracts/TodoItems/Commands/CreateTodoItemCommand.cs index 58febff..41af562 100644 --- a/TodoSample/Core/Application.Contracts/TodoItems/Commands/CreateTodoItemCommand.cs +++ b/TodoSample/Core/Application.Contracts/TodoItems/Commands/CreateTodoItemCommand.cs @@ -1,4 +1,5 @@  +using Honamic.Framework.Applications.Authorizes; using Honamic.Framework.Applications.Results; using Honamic.Framework.Commands; @@ -8,6 +9,8 @@ public record CreateTodoItemCommand(string title, string content, List t +//[DynamicAuthorize] +[Authorize("admin")] public record CreateTodoItem2Command(string title, string content, List tags) : ICommand>; diff --git a/TodoSample/Facade/TodoItems/TodoItemFacade.cs b/TodoSample/Facade/TodoItems/TodoItemFacade.cs index 61d0c9c..5c6ad5f 100644 --- a/TodoSample/Facade/TodoItems/TodoItemFacade.cs +++ b/TodoSample/Facade/TodoItems/TodoItemFacade.cs @@ -1,4 +1,5 @@ -using Honamic.Framework.Applications.Results; +using Honamic.Framework.Applications.Authorizes; +using Honamic.Framework.Applications.Results; using Honamic.Framework.Commands; using Honamic.Framework.Events; using Honamic.Framework.Facade; @@ -8,7 +9,7 @@ namespace Honamic.Todo.Facade.TodoItems; -[FacadeDynamicAuthorize] +[DynamicAuthorize] internal class TodoItemFacade : BaseFacade, ITodoItemFacade { private readonly ICommandBus _commandBus; diff --git a/TodoSample/Facade/TodoItems/TodoItemQueryFacade.cs b/TodoSample/Facade/TodoItems/TodoItemQueryFacade.cs index 304250c..4db18fa 100644 --- a/TodoSample/Facade/TodoItems/TodoItemQueryFacade.cs +++ b/TodoSample/Facade/TodoItems/TodoItemQueryFacade.cs @@ -6,10 +6,11 @@ using Honamic.Todo.Query.Domain.TodoItems.Queries; using Honamic.Todo.Query.Domain.TodoItems; using Honamic.Framework.Applications.Results; +using Honamic.Framework.Applications.Authorizes; namespace Honamic.Todo.Facade.TodoItems; -[FacadeDynamicAuthorize] +[DynamicAuthorize] [DisplayName("نمایش انجام دادنی ها")] public class TodoItemQueryFacade : BaseFacade, ITodoItemQueryFacade { diff --git a/src/Core/Applications.Abstractions/Authorizes/AllowAnonymousAttribute.cs b/src/Core/Applications.Abstractions/Authorizes/AllowAnonymousAttribute.cs new file mode 100644 index 0000000..3433b09 --- /dev/null +++ b/src/Core/Applications.Abstractions/Authorizes/AllowAnonymousAttribute.cs @@ -0,0 +1,7 @@ +namespace Honamic.Framework.Applications.Authorizes; + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +public class AllowAnonymousAttribute : Attribute +{ + +} diff --git a/src/Core/Applications.Abstractions/Authorizes/AuthorizeAttribute.cs b/src/Core/Applications.Abstractions/Authorizes/AuthorizeAttribute.cs new file mode 100644 index 0000000..c9380df --- /dev/null +++ b/src/Core/Applications.Abstractions/Authorizes/AuthorizeAttribute.cs @@ -0,0 +1,17 @@ +namespace Honamic.Framework.Applications.Authorizes; + +[AttributeUsage(AttributeTargets.All, AllowMultiple = false)] +public class AuthorizeAttribute : Attribute +{ + public AuthorizeAttribute() + { + Permissions = null; + } + + public AuthorizeAttribute(params string[] permissions) + { + Permissions = permissions; + } + + public string[]? Permissions { get; } +} \ No newline at end of file diff --git a/src/Core/Applications.Abstractions/Authorizes/DynamicAuthorizeAttribute.cs b/src/Core/Applications.Abstractions/Authorizes/DynamicAuthorizeAttribute.cs new file mode 100644 index 0000000..3a20a2a --- /dev/null +++ b/src/Core/Applications.Abstractions/Authorizes/DynamicAuthorizeAttribute.cs @@ -0,0 +1,7 @@ +namespace Honamic.Framework.Applications.Authorizes; + +[AttributeUsage(AttributeTargets.All, AllowMultiple = false)] +public class DynamicAuthorizeAttribute : Attribute +{ + +} diff --git a/src/Core/Applications.Abstractions/Authorizes/IAuthorization.cs b/src/Core/Applications.Abstractions/Authorizes/IAuthorization.cs new file mode 100644 index 0000000..89dfff8 --- /dev/null +++ b/src/Core/Applications.Abstractions/Authorizes/IAuthorization.cs @@ -0,0 +1,11 @@ +namespace Honamic.Framework.Applications.Authorizes; + +public interface IAuthorization +{ + [Obsolete("Use HaveAccessAsync instead. This method will be removed in future versions.")] + bool HaveAccess(string permission); + + Task HaveAccessAsync(string permission); + + bool IsAuthenticated(); +} diff --git a/src/Core/Applications.Abstractions/Exceptions/UnauthorizedException.cs b/src/Core/Applications.Abstractions/Exceptions/UnauthorizedException.cs index 9ab570d..0d7a502 100644 --- a/src/Core/Applications.Abstractions/Exceptions/UnauthorizedException.cs +++ b/src/Core/Applications.Abstractions/Exceptions/UnauthorizedException.cs @@ -6,7 +6,7 @@ public UnauthorizedException(string permissionName) { PermissionName = permissionName; } - public override string Message => "the request does not have valid authentication credentials for the operation"; + public override string Message => "You do not have permission to perform this operation"; public string PermissionName { get; } } diff --git a/src/Core/Applications/CommandHandlerDecorators/AuthorizeCommandHandlerDecorator.cs b/src/Core/Applications/CommandHandlerDecorators/AuthorizeCommandHandlerDecorator.cs new file mode 100644 index 0000000..43d218d --- /dev/null +++ b/src/Core/Applications/CommandHandlerDecorators/AuthorizeCommandHandlerDecorator.cs @@ -0,0 +1,106 @@ +using Honamic.Framework.Applications.Authorizes; +using Honamic.Framework.Applications.Exceptions; +using Honamic.Framework.Commands; +using System.Reflection; + +namespace Honamic.Framework.Applications.CommandHandlerDecorators; + +public class AuthorizeCommandHandlerDecorator : ICommandHandler + where TCommand : ICommand +{ + private readonly ICommandHandler _commandHandler; + private readonly IAuthorization _authorization; + + public AuthorizeCommandHandlerDecorator(ICommandHandler commandHandler, IAuthorization authorization) + { + _commandHandler = commandHandler; + _authorization = authorization; + } + + public async Task HandleAsync(TCommand command, CancellationToken cancellationToken) + { + await _authorization.AuthorizeCommandAttributes(typeof(TCommand)); + + await _commandHandler.HandleAsync(command, cancellationToken); + } +} + +public class AuthorizeCommandHandlerDecorator : ICommandHandler + where TCommand : ICommand +{ + private readonly ICommandHandler _commandHandler; + private readonly IAuthorization _authorization; + + public AuthorizeCommandHandlerDecorator(ICommandHandler commandHandler, IAuthorization authorization) + { + _commandHandler = commandHandler; + _authorization = authorization; + } + + public async Task HandleAsync(TCommand command, CancellationToken cancellationToken) + { + await _authorization.AuthorizeCommandAttributes(typeof(TCommand)); + + return await _commandHandler.HandleAsync(command, cancellationToken); + } +} + +internal static class AuthorizeCommandHandlerDecoratorHelper +{ + + public static async Task AuthorizeCommandAttributes(this IAuthorization authorization, Type type) + { + await authorization.AuthorizeWithAttributes(type); + + await authorization.AuthorizeWithDynamicPermissions(type); + } + + public static async Task AuthorizeWithDynamicPermissions(this IAuthorization authorization, Type type) + { + var dynamicAuthorizeAttribute = type.GetCustomAttribute(); + + if (dynamicAuthorizeAttribute is not null) + { + if (!authorization.IsAuthenticated()) + { + throw new UnauthenticatedException(); + } + + string dynamicPermission = CalculatePermissionName(type); + + if (!await authorization.HaveAccessAsync(dynamicPermission)) + { + throw new UnauthorizedException(dynamicPermission); + } + } + } + + public static async Task AuthorizeWithAttributes(this IAuthorization authorization, Type type) + { + var authorizeAttribute = type.GetCustomAttribute(); + + if (authorizeAttribute is not null) + { + if (!authorization.IsAuthenticated()) + { + throw new UnauthenticatedException(); + } + + if (authorizeAttribute.Permissions?.Length > 0) + { + foreach (var permission in authorizeAttribute.Permissions) + { + if (!await authorization.HaveAccessAsync(permission)) + { + throw new UnauthorizedException(permission); + } + } + } + } + } + + private static string CalculatePermissionName(Type type) + { + return type.Name; + } +} \ No newline at end of file diff --git a/src/Core/Applications/CommandHandlerDecorators/ExceptionCommandHandlerDecorator.cs b/src/Core/Applications/CommandHandlerDecorators/ExceptionCommandHandlerDecorator.cs new file mode 100644 index 0000000..314e6bc --- /dev/null +++ b/src/Core/Applications/CommandHandlerDecorators/ExceptionCommandHandlerDecorator.cs @@ -0,0 +1,107 @@ +using Honamic.Framework.Applications.Exceptions; +using Honamic.Framework.Applications.Results; +using Honamic.Framework.Commands; +using Honamic.Framework.Domain; + +namespace Honamic.Framework.Applications.CommandHandlerDecorators; + +public class ExceptionCommandHandlerDecorator : ICommandHandler + where TCommand : ICommand +{ + private readonly ICommandHandler _commandHandler; + + public ExceptionCommandHandlerDecorator(ICommandHandler commandHandler) + { + _commandHandler = commandHandler; + } + + public async Task HandleAsync(TCommand command, CancellationToken cancellationToken) + { + TResponse result; + try + { + result = await _commandHandler.HandleAsync(command, cancellationToken); + } + catch (Exception ex) + { + if (IsResultOriented(typeof(TResponse))) + { + result = CreateResultWithError(typeof(TResponse), ex); + return result; + } + + throw; + } + + return result; + } + + private TResponse CreateResultWithError(Type type, Exception ex) + { + var resultObject = CreateResult(type); + + if (resultObject is Result result) + { + switch (ex) + { + case UnauthenticatedException: + result.SetStatusAsUnauthenticated(); + result.AppendError(ex.Message); + break; + case UnauthorizedException: + result.SetStatusAsUnauthorized(); + result.AppendError(ex.Message); + break; + case BusinessException businessException: + result.Status = ResultStatus.ValidationError; + var code = businessException.GetCode(); + var message = businessException.GetMessage(); + result.AppendError(message, null, code); + break; + default: + result.SetStatusAsUnhandledExceptionWithSorryError(); + result.AppendError(ex.ToString(), "Exception"); + break; + } + return resultObject; + } + + // If we can't cast to Result, we have a serious error + throw new ArgumentException($"Expected a Result type but got {type.FullName}"); + } + + private TResponse CreateResult(Type type) + { + // For non-generic Result + if (type == typeof(Result)) + { + return (TResponse)(object)new Result(); + } + + // For Result + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Result<>)) + { + var genericArgType = type.GenericTypeArguments[0]; + var resultType = typeof(Result<>).MakeGenericType(genericArgType); + return (TResponse)Activator.CreateInstance(resultType); + } + + return default; + } + + private bool IsResultOriented(Type type) + { + if (type == typeof(Result)) + { + return true; + } + + if (type.IsGenericType + && type.GetGenericTypeDefinition() == typeof(Result<>)) + { + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/src/Core/Applications/CommandHandlerDecorators/ResultOrientedCommandHandlerDecorator.cs b/src/Core/Applications/CommandHandlerDecorators/ResultOrientedCommandHandlerDecorator.cs index 05a514c..e69de29 100644 --- a/src/Core/Applications/CommandHandlerDecorators/ResultOrientedCommandHandlerDecorator.cs +++ b/src/Core/Applications/CommandHandlerDecorators/ResultOrientedCommandHandlerDecorator.cs @@ -1,107 +0,0 @@ -using Honamic.Framework.Applications.Exceptions; -using Honamic.Framework.Applications.Results; -using Honamic.Framework.Commands; -using Honamic.Framework.Domain; - -namespace Honamic.Framework.Applications.CommandHandlerDecorators; - -public class ResultOrientedCommandHandlerDecorator : ICommandHandler - where TCommand : ICommand -{ - private readonly ICommandHandler _commandHandler; - - public ResultOrientedCommandHandlerDecorator(ICommandHandler commandHandler) - { - _commandHandler = commandHandler; - } - - public async Task HandleAsync(TCommand command, CancellationToken cancellationToken) - { - TResponse result; - try - { - result = await _commandHandler.HandleAsync(command, cancellationToken); - } - catch (Exception ex) - { - if (IsResultOriented(typeof(TResponse))) - { - result = CreateResultWithError(typeof(TResponse), ex); - return result; - } - - throw; - } - - return result; - } - - private TResponse CreateResultWithError(Type type, Exception ex) - { - var resultObject = CreateResult(type); - - if (resultObject is Result result) - { - switch (ex) - { - case UnauthenticatedException: - result.SetStatusAsUnauthenticated(); - result.AppendError(ex.Message); - break; - case UnauthorizedException: - result.SetStatusAsUnauthorized(); - result.AppendError(ex.Message); - break; - case BusinessException businessException: - result.Status = ResultStatus.ValidationError; - var code = businessException.GetCode(); - var message = businessException.GetMessage(); - result.AppendError(message, null, code); - break; - default: - result.SetStatusAsUnhandledExceptionWithSorryError(); - result.AppendError(ex.ToString(), "Exception"); - break; - } - return resultObject; - } - - // If we can't cast to Result, we have a serious error - throw new ArgumentException($"Expected a Result type but got {type.FullName}"); - } - - private TResponse CreateResult(Type type) - { - // For non-generic Result - if (type == typeof(Result)) - { - return (TResponse)(object)new Result(); - } - - // For Result - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Result<>)) - { - var genericArgType = type.GenericTypeArguments[0]; - var resultType = typeof(Result<>).MakeGenericType(genericArgType); - return (TResponse)Activator.CreateInstance(resultType); - } - - return default; - } - - private bool IsResultOriented(Type type) - { - if (type == typeof(Result)) - { - return true; - } - - if (type.IsGenericType - && type.GetGenericTypeDefinition() == typeof(Result<>)) - { - return true; - } - - return false; - } -} \ No newline at end of file diff --git a/src/Core/Applications/Extensions/ServiceCollectionExtensions.cs b/src/Core/Applications/Extensions/ServiceCollectionExtensions.cs index f1309d6..6681a32 100644 --- a/src/Core/Applications/Extensions/ServiceCollectionExtensions.cs +++ b/src/Core/Applications/Extensions/ServiceCollectionExtensions.cs @@ -26,6 +26,7 @@ public static void AddCommandHandler(this IServiceCol where TCommandHandler : class, ICommandHandler { services.AddTransient, TCommandHandler>(); + services.Decorate, AuthorizeCommandHandlerDecorator>(); services.Decorate, TransactionalCommandHandlerDecorator>(); // Note: No ResultOriented decorator for non-response commands } @@ -35,8 +36,9 @@ public static void AddCommandHandler(this where TCommandHandler : class, ICommandHandler { services.AddTransient, TCommandHandler>(); + services.Decorate, AuthorizeCommandHandlerDecorator>(); services.Decorate, TransactionalCommandHandlerDecorator>(); - services.Decorate, ResultOrientedCommandHandlerDecorator>(); + services.Decorate, ExceptionCommandHandlerDecorator>(); } public static void AddEventHandler(this IServiceCollection services) diff --git a/src/Facade/Abstractions/AuthorizeAttribute.cs b/src/Facade/Abstractions/AuthorizeAttribute.cs deleted file mode 100644 index e2b4ba1..0000000 --- a/src/Facade/Abstractions/AuthorizeAttribute.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace Honamic.Framework.Facade; - -[AttributeUsage(AttributeTargets.All, AllowMultiple = false)] -public class FacadeAuthorizeAttribute : Attribute -{ - -} - -[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] -public class FacadeAllowAnonymousAttribute : Attribute -{ - -} - -[AttributeUsage(AttributeTargets.All, AllowMultiple = false)] -public class FacadeDynamicAuthorizeAttribute : FacadeAuthorizeAttribute -{ - -} - -public class FacadeStaticAuthorizeAttribute : Attribute -{ - - public FacadeStaticAuthorizeAttribute(string permission) - { - Permission = permission; - } - - public string Permission { get; } -} - - - diff --git a/src/Facade/Abstractions/IFacadeAuthorization.cs b/src/Facade/Abstractions/IFacadeAuthorization.cs deleted file mode 100644 index 11b5246..0000000 --- a/src/Facade/Abstractions/IFacadeAuthorization.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Honamic.Framework.Facade; - -public interface IFacadeAuthorization -{ - bool HaveAccess(string permission); - - bool IsAuthenticated(); -} diff --git a/src/Facade/Default/DisableAuthorization.cs b/src/Facade/Default/DisableAuthorization.cs new file mode 100644 index 0000000..d6782ce --- /dev/null +++ b/src/Facade/Default/DisableAuthorization.cs @@ -0,0 +1,21 @@ +using Honamic.Framework.Applications.Authorizes; + +namespace Honamic.Framework.Facade; + +internal class DisableAuthorization : IAuthorization +{ + public bool HaveAccess(string permission) + { + return true; + } + + public Task HaveAccessAsync(string permission) + { + return Task.FromResult(true); + } + + public bool IsAuthenticated() + { + return true; + } +} diff --git a/src/Facade/Default/DisableFacadeAuthorization.cs b/src/Facade/Default/DisableFacadeAuthorization.cs deleted file mode 100644 index 4e0afdb..0000000 --- a/src/Facade/Default/DisableFacadeAuthorization.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Honamic.Framework.Facade; - -internal class DisableFacadeAuthorization : IFacadeAuthorization -{ - public bool HaveAccess(string permission) - { - return true; - } - - public bool IsAuthenticated() - { - return true; - } -} diff --git a/src/Facade/Default/Extensions/FacadeServiceCollectionExtensions.cs b/src/Facade/Default/Extensions/FacadeServiceCollectionExtensions.cs index 96f9958..4441897 100644 --- a/src/Facade/Default/Extensions/FacadeServiceCollectionExtensions.cs +++ b/src/Facade/Default/Extensions/FacadeServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ using Castle.DynamicProxy; +using Honamic.Framework.Applications.Authorizes; using Honamic.Framework.Facade.Discovery; using Honamic.Framework.Facade.Interceptors; using Microsoft.Extensions.Configuration; @@ -20,7 +21,7 @@ public static void AddDefaultFrameworkFacadeServices(this IServiceCollection ser public static void AddDisableFacadeAuthorizationServices(this IServiceCollection services) { - services.AddScoped(); + services.AddScoped(); } public static void AddFacadeScoped(this IServiceCollection services) diff --git a/src/Facade/Default/Interceptors/AuthorizeInterceptor.cs b/src/Facade/Default/Interceptors/AuthorizeInterceptor.cs index 3615657..e9a5b79 100644 --- a/src/Facade/Default/Interceptors/AuthorizeInterceptor.cs +++ b/src/Facade/Default/Interceptors/AuthorizeInterceptor.cs @@ -1,4 +1,5 @@ using Castle.DynamicProxy; +using Honamic.Framework.Applications.Authorizes; using Honamic.Framework.Applications.Exceptions; using Microsoft.Extensions.Logging; using System.Reflection; @@ -8,10 +9,10 @@ namespace Honamic.Framework.Facade.Interceptors; internal class AuthorizeInterceptor : IInterceptor { private readonly ILogger _logger; - private readonly IFacadeAuthorization _facadeAuthorization; + private readonly IAuthorization _facadeAuthorization; public AuthorizeInterceptor(ILogger logger, - IFacadeAuthorization facadeAuthorization) + IAuthorization facadeAuthorization) { _logger = logger; _facadeAuthorization = facadeAuthorization; @@ -28,10 +29,10 @@ public void Intercept(IInvocation invocation) private void Authorize(IInvocation invocation) { var classDynamicAuthorize = invocation.TargetType - .GetCustomAttribute(); + .GetCustomAttribute(); var methodDynamicAuthorize = invocation.MethodInvocationTarget - .GetCustomAttribute(); + .GetCustomAttribute(); if (classDynamicAuthorize is null && methodDynamicAuthorize is null) @@ -43,7 +44,7 @@ private void Authorize(IInvocation invocation) if (methodDynamicAuthorize is null) { var AllowAnonymous = invocation.MethodInvocationTarget - .GetCustomAttribute(); + .GetCustomAttribute(); if (AllowAnonymous is not null) { return; @@ -66,10 +67,10 @@ private void Authorize(IInvocation invocation) private void Authentication(IInvocation invocation) { var classAuthorize = invocation.TargetType - .GetCustomAttribute(); + .GetCustomAttribute(); var methodAuthorize = invocation.Method - .GetCustomAttribute(); + .GetCustomAttribute(); if (classAuthorize is null && methodAuthorize is null) @@ -80,7 +81,7 @@ private void Authentication(IInvocation invocation) if (methodAuthorize is null) { var AllowAnonymous = invocation.Method - .GetCustomAttribute(); + .GetCustomAttribute(); if (AllowAnonymous is not null) { return;