diff --git a/docs/cqrs/authorization/index.md b/docs/cqrs/authorization/index.md index 4698fae99..4109bd99d 100644 --- a/docs/cqrs/authorization/index.md +++ b/docs/cqrs/authorization/index.md @@ -4,13 +4,121 @@ Each command and query has to be authorized or must explicitly opt-out of author If multiple `AuthorizeWhen` attributes are specified, **all** authorization rules must pass. -An authorizer is a class that implements the `ICustomAuthorizer` interface or derives from one of the `CustomAuthorizer` base classes. It has access to both context and command/query. Command/query type doesn't need to be exact, it just has to be coercible to the specified type (`CustomAuthorizer` casts objects to the types internally). Therefore, if you want to use the same authorizer for many commands/queries, you can use base classes or interfaces and implement the authorizer for them. +An authorizer is a class that implements the `ICustomAuthorizer` interface or derives from one of the `CustomAuthorizer` base classes. It has access to both context and [command]/[query]/[operation]. [Command]/[query]/[operation] type doesn't need to be exact, it just has to be coercible to the specified type (`CustomAuthorizer` casts objects to the types internally). Therefore, if you want to use the same authorizer for many [commands]/[queries]/[operations], you can use base classes or interfaces and implement the authorizer for them. -Example authorizer, along with the (not required, but convenient) plumbing: +## AuthorizeWhenHasAnyOf + +The `AuthorizeWhenHasAnyOf` attribute, found in `LeanCode.Contracts.Security`, has default authorization implementation. Upon its application, the `CheckIfAuthorizedAsync` method from the `DefaultPermissionAuthorizer` class is invoked to check whether the user possesses adequate permissions: + +```csharp +public class DefaultPermissionAuthorizer + : CustomAuthorizer, IHasPermissions +{ + private readonly Serilog.ILogger logger = Serilog.Log.ForContext(); + + private readonly RoleRegistry registry; + + public DefaultPermissionAuthorizer(RoleRegistry registry) + { + this.registry = registry; + } + + protected override Task CheckIfAuthorizedAsync( + ClaimsPrincipal user, + object obj, + string[]? customData) + { + if (!user.HasPermission(registry, customData ?? Array.Empty())) + { + logger.Warning( + "User does not have sufficient permissions ({Permissions}) to run {@Object}", + customData, + obj + ); + + return Task.FromResult(false); + } + else + { + return Task.FromResult(true); + } + } +} +``` + +```csharp +public static class ClaimsPrincipalExtensions +{ + public static bool HasPermission( + this ClaimsPrincipal claimsPrincipal, + RoleRegistry registry, + params string[] permissions + ) + { + return registry.All.Any( + role => claimsPrincipal.IsInRole(role.Name) + && permissions.Any(role.Permissions.Contains) + ); + } +} +``` + +```csharp +public sealed class RoleRegistry +{ + public ImmutableList All { get; } + + public RoleRegistry(IEnumerable registrations) + { + All = registrations.SelectMany(r => r.Roles).ToImmutableList(); + } +} +``` + +The `CheckIfAuthorizedAsync` method employs the `RoleRegistry` class to retrieve roles within the system. To integrate roles and ensure proper functionality, a class implementing `IRoleRegistration` must be added to the Dependency Injection (DI) container. The first argument in the `Role` constructor represents the role, and subsequent arguments denote permissions passed as `params`: + +```csharp +internal class AppRoles : IRoleRegistration +{ + public IEnumerable Roles { get; } = new[] + { + new Role("employee", "employee"), + new Role("admin", "admin"), + }; +} +``` + +To register this class in the DI container, include the following code in the `ConfigureServices` method: ```csharp +public override void ConfigureServices(IServiceCollection services) +{ + . . . + + services.AddSingleton(); + + . . . +} + +``` +## AllowUnauthorized +All [query], [command] and [operation] require usage of authorization attribute (which is enforced by Roslyn analyzers). To bypass the authorization requirements, developers can employ the `AllowUnauthorized` attribute as demonstrated below to skip authorization entirely: + +```csharp +[AllowUnauthorized] +public class Projects : IQuery> +{ + public string? NameFilter { get; set; } +} +``` + +## Custom authorizers + +Other than `AuthorizeWhenHasAnyOf` and `AllowUnauthorized` attributes which have default implementations custom authorizers can be defined. Here is an example along with the (not required, but convenient) plumbing: + +```csharp // Object that use `ProjectIsOwned` attribute must implement this interface public interface IProjectRelated { @@ -66,6 +174,40 @@ public class ProjectIsOwnedAuthorizer } ``` -All queries, commands and operations can (and should!) be behind authorization. By default, authorization is run before validation so the object that the command/query/operation is pointing at might not exist. +All [queries], [commands] and [operations] can (and should!) be behind authorization. If pipeline is configured as below, authorization is run before validation so the object that the [command]/[query]/[operation] is pointing at might not exist and we let validation handle this case. + +```csharp + protected override void ConfigureApp(IApplicationBuilder app) + { + . . . + app.UseEndpoints(endpoints => + { + endpoints.MapRemoteCqrs( + "/api", + cqrs => + { + . . . + + cqrs.Commands = c => + c.CQRSTrace() + // Authorization is before validation + .Secure() + .Validate() + .CommitTransaction() + .PublishEvents(); + + . . . + } + ); + }); + } +``` > **Tip:** You can implement your own authorization and use it with LeanCode CoreLibrary authorizers. To see how you can implement authorization using Ory Kratos and LeanCode CoreLibrary see here. + +[query]: ../query/index.md +[command]: ../command/index.md +[operation]: ../operation/index.md +[commands]: ../command/index.md +[queries]: ../query/index.md +[operations]: ../operation/index.md diff --git a/docs/cqrs/command/index.md b/docs/cqrs/command/index.md index c41471bea..1fce18c32 100644 --- a/docs/cqrs/command/index.md +++ b/docs/cqrs/command/index.md @@ -4,7 +4,7 @@ Command is just a class that implements the `ICommand` interface. Commands are ## Contract -Consider the command that updates name of the `Project` (caller of the command is required to have `Employee` role and own project). +Consider the command that updates name of the `Project` (caller of the command is required to have `Employee` role and own project): ```csharp [ProjectIsOwned] diff --git a/docs/cqrs/operation/index.md b/docs/cqrs/operation/index.md index e6e04dd80..d8eb195b3 100644 --- a/docs/cqrs/operation/index.md +++ b/docs/cqrs/operation/index.md @@ -11,7 +11,7 @@ Operations change the state of the system, but also allow to return some result. ## Contract -Consider the operation that creates payment in external service for employee's access to application and returns payment token. +Consider the operation that creates payment in external service for employee's access to application and returns payment token: ```csharp [AuthorizeWhenHasAnyOf(Auth.Roles.Admin)] diff --git a/docs/cqrs/query/index.md b/docs/cqrs/query/index.md index d56ebd3a0..5faa94fc6 100644 --- a/docs/cqrs/query/index.md +++ b/docs/cqrs/query/index.md @@ -4,7 +4,7 @@ Query is just a class that implements the `IQuery` interface (there's ## Contract -Consider the query that finds all projects that match the name filter. It may be called anonymously and returns a list of `ProjectDTO`s (we use a `List` instead of a `IList` or `IReadOnlyList` because of the DTO constraint; `List` is more DTO-ish than any interface). +Consider the query that finds all projects that match the name filter. It may be called anonymously and returns a list of `ProjectDTO`s (we use a `List` instead of a `IList` or `IReadOnlyList` because of the DTO constraint; `List` is more DTO-ish than any interface): ```csharp public class ProjectDTO