-
Notifications
You must be signed in to change notification settings - Fork 114
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
docs: Add Resource Authorization Reference Solution (#93)
- Loading branch information
1 parent
39f5370
commit 957dc16
Showing
55 changed files
with
3,629 additions
and
64 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
::: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
73 changes: 73 additions & 0 deletions
73
samples/ResourceAuthorization/Controllers/WorkspaceUsersController.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
64
samples/ResourceAuthorization/Controllers/WorkspacesController.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.