Skip to content

Commit

Permalink
Add AuthorizeWhenHasAnyOf and AllowUnauthorized examples
Browse files Browse the repository at this point in the history
  • Loading branch information
Wojciech Klusek committed Jan 5, 2024
1 parent a6f0002 commit e782400
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 6 deletions.
148 changes: 145 additions & 3 deletions docs/cqrs/authorization/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<object, string[]>, IHasPermissions
{
private readonly Serilog.ILogger logger = Serilog.Log.ForContext<DefaultPermissionAuthorizer>();

private readonly RoleRegistry registry;

public DefaultPermissionAuthorizer(RoleRegistry registry)
{
this.registry = registry;
}

protected override Task<bool> CheckIfAuthorizedAsync(
ClaimsPrincipal user,
object obj,
string[]? customData)
{
if (!user.HasPermission(registry, customData ?? Array.Empty<string>()))
{
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<Role> All { get; }

public RoleRegistry(IEnumerable<IRoleRegistration> 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<Role> 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<LeanCode.CQRS.Security.IRoleRegistration, AppRoles>();

. . .
}

```

## 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<List<ProjectDTO>>
{
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
{
Expand Down Expand Up @@ -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<CoreDbContext>()
.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. <!-- TODO: add link to Ory Kratos page -->
[query]: ../query/index.md
[command]: ../command/index.md
[operation]: ../operation/index.md
[commands]: ../command/index.md
[queries]: ../query/index.md
[operations]: ../operation/index.md
2 changes: 1 addition & 1 deletion docs/cqrs/command/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion docs/cqrs/operation/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
2 changes: 1 addition & 1 deletion docs/cqrs/query/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Query is just a class that implements the `IQuery<TResult>` 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
Expand Down

0 comments on commit e782400

Please sign in to comment.