Skip to content

Commit

Permalink
docs: Add Resource Authorization Reference Solution (#93)
Browse files Browse the repository at this point in the history
  • Loading branch information
NikiforovAll authored May 9, 2024
1 parent 39f5370 commit 957dc16
Show file tree
Hide file tree
Showing 55 changed files with 3,629 additions and 64 deletions.
15 changes: 15 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,21 @@ dotnet_diagnostic.IDE0036.severity = error
dotnet_diagnostic.IDE0059.severity = error
# IDE0016 Use throw expression
dotnet_diagnostic.IDE0016.severity = suggestion
# CA1056: URI properties should not be strings
dotnet_diagnostic.CA1056.severity = suggestion

dotnet_diagnostic.CA1812.severity = none
dotnet_diagnostic.CA2007.severity = none
dotnet_diagnostic.CA1307.severity = none
dotnet_diagnostic.CA2234.severity = none
dotnet_diagnostic.CA1054.severity = none
dotnet_diagnostic.CA1032.severity = none
dotnet_diagnostic.CA1724.severity = none
dotnet_diagnostic.CA1055.severity = none
dotnet_diagnostic.CA1510.severity = suggestion
dotnet_diagnostic.CA2227.severity = suggestion
dotnet_diagnostic.CA1819.severity = suggestion
dotnet_diagnostic.CA1019.severity = none

##########################################
# File Header (Uncomment to support file headers)
Expand Down
7 changes: 7 additions & 0 deletions KeycloakAuthorizationServicesDotNet.sln
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Keycloak.AuthServices.Sdk.K
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestWebApiWithControllers", "tests\TestWebApiWithControllers\TestWebApiWithControllers.csproj", "{BF2DCACD-E7C4-4B92-909F-CC535B70F94D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResourceAuthorization", "samples\ResourceAuthorization\ResourceAuthorization.csproj", "{B060EE8C-C76D-48A4-B209-4646070A7E0D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -154,6 +156,10 @@ Global
{BF2DCACD-E7C4-4B92-909F-CC535B70F94D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BF2DCACD-E7C4-4B92-909F-CC535B70F94D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BF2DCACD-E7C4-4B92-909F-CC535B70F94D}.Release|Any CPU.Build.0 = Release|Any CPU
{B060EE8C-C76D-48A4-B209-4646070A7E0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B060EE8C-C76D-48A4-B209-4646070A7E0D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B060EE8C-C76D-48A4-B209-4646070A7E0D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B060EE8C-C76D-48A4-B209-4646070A7E0D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -179,6 +185,7 @@ Global
{331F4EF5-9CFE-4060-B903-69CCE9062BFD} = {96857509-627A-4FD2-AC82-34387619A7B1}
{8C43A1C1-0069-4B21-ADDE-5268EB214820} = {F9D5C5B8-9933-4AE0-ADAC-6B8C15F7552A}
{BF2DCACD-E7C4-4B92-909F-CC535B70F94D} = {96857509-627A-4FD2-AC82-34387619A7B1}
{B060EE8C-C76D-48A4-B209-4646070A7E0D} = {AEBE10B1-96B1-4060-B8C1-1F9BFA7A586C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E1907BFD-C144-4B48-AA40-972F499D4E08}
Expand Down
7 changes: 4 additions & 3 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,11 @@ export default withMermaid({
text: 'Examples',
collapsed: false,
items: [
{ text: 'Auth Web API Getting Started', link: '/examples/auth-getting-started' },
{ text: 'Web API Getting Started', link: '/examples/auth-getting-started' },
{ text: 'Authorization', link: '/examples/authorization-getting-started' },
{ text: 'Auth Clean Architecture', link: '/examples/auth-clean-arch' },
{ text: 'Auth Web API + Blazor', link: '/examples/web-api-blazor' }
{ text: 'Resource Authorization ✨', link: '/examples/resource-authorization' },
{ text: 'Clean Architecture', link: '/examples/auth-clean-arch' },
{ text: 'Web API + Blazor', link: '/examples/web-api-blazor' }
]
}
]
Expand Down
3 changes: 3 additions & 0 deletions docs/authorization/protected-resource-builder-mvc.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

This article demonstrates how to add **Protected Resource Builder** to an MVC project. Here is how to add resource authorization:

> [!TIP]
> 💡See [Resource Authorization](/examples/resource-authorization) Reference Solution to see a real world example of how to use Protected Resource Builder.
## Register Protected Resource Builder for MVC

```csharp:line-numbers
Expand Down
3 changes: 3 additions & 0 deletions docs/authorization/protected-resource-builder.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

Using *Policies* is a standard and common approach. However, as the number of resources grows, organizing and managing these policies can become a challenge. To address this issue, we suggest using the **Protected Resource Builder** approach. This builder provides a convenient way to authorize resources, making it easier to manage and maintain authorization rules.

> [!TIP]
> 💡See [Resource Authorization](/examples/resource-authorization) Reference Solution to see a real world example of how to use Protected Resource Builder.
::: info
In most cases, we don't really need to build policies when working with *Authorization Server*, the authorization responsibility is delegated.
:::
Expand Down
87 changes: 87 additions & 0 deletions docs/examples/resource-authorization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Resource Authorization

Resource authorization is a concept in software development that involves controlling access to specific resources or functionalities within an application. It ensures that only authorized users can perform certain actions or access certain data.

For example, assume we have project management software. A user can be an *Owner* for one project and don't have access at all to another project. The authorization logic is centered around resources and permissions and not about user roles necessarily.

You can create a **hierarchical structure** for authorization that allows for efficient management and fine-grained control over access to resources. This is particularly useful in complex applications where resources and users need to be organized in a meaningful way.

> [!TIP]
> 💡 Keycloak supports User-Managed Access ([**UMA**](https://en.wikipedia.org/wiki/User-Managed_Access)).
> [!TIP]
> 💡The other benefit of using Keycloak as *Authorization Server* - you can change authorization rules at runtime without the need of redeploying your code.
## Workspaces API Overview

The Workspaces API includes endpoints for creating, listing, reading, and deleting workspaces, as well as managing users within those workspaces.

> [!INFO]
> Workspaces: This term is used in the context of collaborative applications. A workspace is a shared environment where a group of users can access and manipulate a set of resources. For example, in a project management application, each project could be considered a workspace. Access to the project (and its associated tasks, files, etc.) can be controlled at the workspace level.
## Authorization Use Cases

Here are use cases implemented in this example:

1. Global access to "Workspaces" functionality. Only users with roles - "Admin" or "Reader" can access API.
2. Admins can manage workspaces - get, create, delete, add/remove users
3. Only workspace members get see a workspace details
4. Workspace members get see each other.
5. Everyone, including anonymous users, can get a details about "public" workspace, but anonymous users can't see workspace's members.

### Endpoints

#### `/workspaces`

- **GET**: Lists all workspaces. Returns an array of strings.
- **POST**: Creates a new workspace. Requires a JSON body specifying workspace details.

#### `/workspaces/{id}`

- **GET**: Retrieves details of a specific workspace identified by `{id}`.
- **DELETE**: Deletes a workspace identified by `{id}`.

#### `/my/workspaces`

- **GET**: Lists all workspaces associated with the authenticated user.

#### `/workspaces/{id}/users`

- **GET**: Lists all users within a specific workspace identified by `{id}`.
- **POST**: Adds a user to a workspace. Requires a JSON body with user details.
- **DELETE**: Removes a user from a workspace identified by `{id}` and a user `email` specified in the query.

### Data Models

#### Workspace

- **Type**: Object
- **Properties**:
- `name`: String
- `membersCount`: Integer (nullable)

#### User

- **Type**: Object
- **Properties**:
- `email`: String

## Code

Setup DI:

<<< @/../samples/ResourceAuthorization/Program.cs

Protected Resource are configured based on `ProtectedResourceAttribute`, attributes are applied based on hierarchy.

<<< @/../samples/ResourceAuthorization/Controllers/WorkspacesController.cs

The idea is to define Keycloak Protected Resources during the creation of a domain object (i.e.: workspace). Protected Resources are the base for enforcement of the authorization rules.

<<< @/../samples/ResourceAuthorization/WorkspaceService.cs

See sample source code: [keycloak-authorization-services-dotnet/tree/main/samples/ResourceAuthorization](https://github.com/NikiforovAll/keycloak-authorization-services-dotnet/tree/main/samples/ResourceAuthorization)

:::details Configuration of Authorization Server
<<< @/../samples/ResourceAuthorization/KeycloakConfiguration/clients/test-client-auth-rules.json
:::
2 changes: 2 additions & 0 deletions samples/Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<Project>
<ItemGroup>
<PackageVersion Include="Duende.AccessTokenManagement" Version="2.1.2" />
<PackageVersion Include="Keycloak.AuthServices.Authentication" Version="1.7.0" />
<PackageVersion Include="MediatR" Version="12.2.0" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.29" />
Expand All @@ -9,6 +10,7 @@
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.29" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.11" />
<PackageVersion Include="NSwag.AspNetCore" Version="14.0.7" />
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageVersion Include="Serilog.Extensions.Hosting" Version="8.0.0" />
<PackageVersion Include="Serilog.Sinks.SpectreConsole" Version="0.3.3" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
namespace ResourceAuthorization.Controllers;

using Keycloak.AuthServices.Authorization;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using ResourceAuthorization.Models;

[ApiController]
[Route("workspaces/{id}/users")]
[OpenApiTag("Users", Description = "Manage Users.")]
public class WorkspaceUsersController(WorkspaceService workspaceService) : ControllerBase
{
[HttpGet("/my/workspaces", Name = nameof(GetMyWorkspacesAsync))]
[OpenApiOperation("[workspace:list]", "")]
[ProtectedResource("workspaces", "workspace:list")]
public async Task<ActionResult<IEnumerable<string>>> GetMyWorkspacesAsync()
{
var workspaces = await workspaceService.ListMyWorkspacesAsync();

return this.Ok(workspaces.Select(w => w.Name));
}

[HttpGet("/workspaces/public/users", Name = nameof(ListUsersForPublicWorkspaceAsync))]
[OpenApiIgnore]
[IgnoreProtectedResource]
public Task<IActionResult> ListUsersForPublicWorkspaceAsync() => this.ListUsersAsync("public");

[HttpGet("/workspaces/{id:regex((?!public))}/users", Name = nameof(ListUsersAsync))]
[OpenApiOperation("[workspace:list-users]", "")]
[ProtectedResource("workspaces__{id}", "workspace:list-users")]
public async Task<IActionResult> ListUsersAsync(string id)
{
if (string.IsNullOrWhiteSpace(id))
{
return this.BadRequest();
}

var users = await workspaceService.ListMembersAsync(id);

return this.Ok(users);
}

[HttpPost("", Name = nameof(AddUserAsync))]
[OpenApiOperation("[workspace:add-user]", "")]
[ProtectedResource("workspaces__{id}", "workspace:add-user")]
public async Task<IActionResult> AddUserAsync(string id, User user)
{
if (string.IsNullOrWhiteSpace(id))
{
return this.BadRequest();
}

await workspaceService.AddMember(id, user);

return this.Created();
}

[HttpDelete("", Name = nameof(RemoveUserAsync))]
[OpenApiOperation("[workspace:remove-user]", "")]
[ProtectedResource("workspaces__{id}", "workspace:remove-user")]
public async Task<IActionResult> RemoveUserAsync(string id, string email)
{
if (string.IsNullOrWhiteSpace(email) || string.IsNullOrWhiteSpace(id))
{
return this.BadRequest();
}

var user = new User(email);
await workspaceService.RemoveMember(id, user);

return this.NoContent();
}
}
64 changes: 64 additions & 0 deletions samples/ResourceAuthorization/Controllers/WorkspacesController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
namespace ResourceAuthorization.Controllers;

using System.Collections.Generic;
using System.Threading.Tasks;
using Keycloak.AuthServices.Authorization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using ResourceAuthorization.Models;

#region WorkspaceAPI
[ApiController]
[Route("workspaces")]
[OpenApiTag("Workspaces", Description = "Manage workspaces.")]
[ProtectedResource("workspaces")]
public class WorkspacesController(WorkspaceService workspaceService) : ControllerBase
{
[HttpGet(Name = nameof(GetWorkspacesAsync))]
[OpenApiOperation("[workspace:list]", "")]
[ProtectedResource("workspaces", "workspace:list")]
public async Task<ActionResult<IEnumerable<string>>> GetWorkspacesAsync()
{
var workspaces = await workspaceService.ListWorkspacesAsync();

return this.Ok(workspaces.Select(w => w.Name));
}

[HttpGet("public")]
[OpenApiIgnore]
[AllowAnonymous]
public async Task<IActionResult> GetPublicWorkspaceAsync() =>
await this.GetWorkspaceAsync("public");

[HttpGet("{id}", Name = nameof(GetWorkspaceAsync))]
[OpenApiOperation("[workspace:read]", "")]
[ProtectedResource("workspaces__{id}", "workspace:read")]
public async Task<IActionResult> GetWorkspaceAsync(string id)
{
var workspace = await workspaceService.GetWorkspaceAsync(id);

return this.Ok(workspace);
}

[HttpPost("", Name = nameof(CreateWorkspaceAsync))]
[OpenApiOperation("[workspace:create]", "")]
[ProtectedResource("workspaces", "workspace:create")]
public async Task<IActionResult> CreateWorkspaceAsync(Workspace workspace)
{
await workspaceService.CreateWorkspaceAsync(workspace);

return this.Created();
}

[HttpDelete("{id}", Name = nameof(DeleteWorkspaceAsync))]
[OpenApiOperation("[workspace:delete]", "")]
[ProtectedResource("workspaces__{id}", "workspace:delete")]
public async Task<IActionResult> DeleteWorkspaceAsync(string id)
{
await workspaceService.DeleteWorkspaceAsync(id);

return this.NoContent();
}
}
#endregion WorkspaceAPI
Loading

0 comments on commit 957dc16

Please sign in to comment.