From 56a8ba5cb6548bd030a2ff810360e66dcecf14c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Tue, 1 Jul 2025 20:34:46 +0200 Subject: [PATCH 1/5] Add GenericDelete action --- .../Endpoints/ApiKeys/DeleteApiKeyEndpoint.cs | 2 +- .../Endpoints/Documents/DeleteDocumentEndpoint.cs | 2 +- src/Turnierplan.App/Endpoints/Images/DeleteImageEndpoint.cs | 2 +- .../Endpoints/Organizations/DeleteOrganizationEndpoint.cs | 2 +- .../Endpoints/Tournaments/DeleteTournamentEndpoint.cs | 2 +- src/Turnierplan.App/Endpoints/Venues/DeleteVenueEndpoint.cs | 2 +- src/Turnierplan.App/Security/Actions.cs | 5 +++++ 7 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Turnierplan.App/Endpoints/ApiKeys/DeleteApiKeyEndpoint.cs b/src/Turnierplan.App/Endpoints/ApiKeys/DeleteApiKeyEndpoint.cs index 7d31d642..b1d30526 100644 --- a/src/Turnierplan.App/Endpoints/ApiKeys/DeleteApiKeyEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/ApiKeys/DeleteApiKeyEndpoint.cs @@ -26,7 +26,7 @@ private static async Task Handle( return Results.NotFound(); } - if (!accessValidator.IsActionAllowed(apiKey.Organization, Actions.GenericWrite)) + if (!accessValidator.IsActionAllowed(apiKey.Organization, Actions.GenericDelete)) { return Results.Forbid(); } diff --git a/src/Turnierplan.App/Endpoints/Documents/DeleteDocumentEndpoint.cs b/src/Turnierplan.App/Endpoints/Documents/DeleteDocumentEndpoint.cs index 9b6d384a..a83c2598 100644 --- a/src/Turnierplan.App/Endpoints/Documents/DeleteDocumentEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/Documents/DeleteDocumentEndpoint.cs @@ -26,7 +26,7 @@ private static async Task Handle( return Results.NotFound(); } - if (!accessValidator.IsActionAllowed(document.Tournament, Actions.GenericWrite)) + if (!accessValidator.IsActionAllowed(document.Tournament, Actions.GenericDelete)) { return Results.Forbid(); } diff --git a/src/Turnierplan.App/Endpoints/Images/DeleteImageEndpoint.cs b/src/Turnierplan.App/Endpoints/Images/DeleteImageEndpoint.cs index 7db4165a..ddc1a5dc 100644 --- a/src/Turnierplan.App/Endpoints/Images/DeleteImageEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/Images/DeleteImageEndpoint.cs @@ -28,7 +28,7 @@ private static async Task Handle( return Results.NotFound(); } - if (!accessValidator.IsActionAllowed(image, Actions.GenericWrite)) + if (!accessValidator.IsActionAllowed(image, Actions.GenericDelete)) { return Results.Forbid(); } diff --git a/src/Turnierplan.App/Endpoints/Organizations/DeleteOrganizationEndpoint.cs b/src/Turnierplan.App/Endpoints/Organizations/DeleteOrganizationEndpoint.cs index e558a23f..786bdafe 100644 --- a/src/Turnierplan.App/Endpoints/Organizations/DeleteOrganizationEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/Organizations/DeleteOrganizationEndpoint.cs @@ -28,7 +28,7 @@ private static async Task Handle( return Results.NotFound(); } - if (!accessValidator.IsActionAllowed(organization, Actions.GenericWrite)) + if (!accessValidator.IsActionAllowed(organization, Actions.GenericDelete)) { return Results.Forbid(); } diff --git a/src/Turnierplan.App/Endpoints/Tournaments/DeleteTournamentEndpoint.cs b/src/Turnierplan.App/Endpoints/Tournaments/DeleteTournamentEndpoint.cs index 1ea6e3e5..5ba9fc99 100644 --- a/src/Turnierplan.App/Endpoints/Tournaments/DeleteTournamentEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/Tournaments/DeleteTournamentEndpoint.cs @@ -28,7 +28,7 @@ private static async Task Handle( return Results.NotFound(); } - if (!accessValidator.IsActionAllowed(tournament, Actions.GenericWrite)) + if (!accessValidator.IsActionAllowed(tournament, Actions.GenericDelete)) { return Results.Forbid(); } diff --git a/src/Turnierplan.App/Endpoints/Venues/DeleteVenueEndpoint.cs b/src/Turnierplan.App/Endpoints/Venues/DeleteVenueEndpoint.cs index 01266e0e..c53b390c 100644 --- a/src/Turnierplan.App/Endpoints/Venues/DeleteVenueEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/Venues/DeleteVenueEndpoint.cs @@ -26,7 +26,7 @@ private static async Task Handle( return Results.NotFound(); } - if (!accessValidator.IsActionAllowed(venue, Actions.GenericWrite)) + if (!accessValidator.IsActionAllowed(venue, Actions.GenericDelete)) { return Results.Forbid(); } diff --git a/src/Turnierplan.App/Security/Actions.cs b/src/Turnierplan.App/Security/Actions.cs index 0603851e..defae370 100644 --- a/src/Turnierplan.App/Security/Actions.cs +++ b/src/Turnierplan.App/Security/Actions.cs @@ -9,6 +9,11 @@ internal static class Actions /// public static readonly Action ReadOrWriteRoleAssignments = new(Role.Owner); + /// + /// Any action that deletes some entity. + /// + public static readonly Action GenericDelete = new(Role.Owner); + /// /// Any action that modifies some entity. /// From 9efebdf43dbdcf677788e3d618dbe7e89ac5b815 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Tue, 1 Jul 2025 20:39:51 +0200 Subject: [PATCH 2/5] Fix wrong permission check for api keys --- src/Turnierplan.App/Endpoints/ApiKeys/DeleteApiKeyEndpoint.cs | 2 +- src/Turnierplan.App/Endpoints/ApiKeys/GetApiKeyUsageEndpoint.cs | 2 +- .../Endpoints/ApiKeys/SetApiKeyStatusEndpoint.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Turnierplan.App/Endpoints/ApiKeys/DeleteApiKeyEndpoint.cs b/src/Turnierplan.App/Endpoints/ApiKeys/DeleteApiKeyEndpoint.cs index b1d30526..deb6fb08 100644 --- a/src/Turnierplan.App/Endpoints/ApiKeys/DeleteApiKeyEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/ApiKeys/DeleteApiKeyEndpoint.cs @@ -26,7 +26,7 @@ private static async Task Handle( return Results.NotFound(); } - if (!accessValidator.IsActionAllowed(apiKey.Organization, Actions.GenericDelete)) + if (!accessValidator.IsActionAllowed(apiKey, Actions.GenericDelete)) { return Results.Forbid(); } diff --git a/src/Turnierplan.App/Endpoints/ApiKeys/GetApiKeyUsageEndpoint.cs b/src/Turnierplan.App/Endpoints/ApiKeys/GetApiKeyUsageEndpoint.cs index a892789b..d8db8b39 100644 --- a/src/Turnierplan.App/Endpoints/ApiKeys/GetApiKeyUsageEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/ApiKeys/GetApiKeyUsageEndpoint.cs @@ -32,7 +32,7 @@ private static async Task Handle( return Results.NotFound(); } - if (!accessValidator.IsActionAllowed(apiKey.Organization, Actions.GenericRead)) + if (!accessValidator.IsActionAllowed(apiKey, Actions.GenericRead)) { return Results.Forbid(); } diff --git a/src/Turnierplan.App/Endpoints/ApiKeys/SetApiKeyStatusEndpoint.cs b/src/Turnierplan.App/Endpoints/ApiKeys/SetApiKeyStatusEndpoint.cs index 1f9cb117..02666f1f 100644 --- a/src/Turnierplan.App/Endpoints/ApiKeys/SetApiKeyStatusEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/ApiKeys/SetApiKeyStatusEndpoint.cs @@ -27,7 +27,7 @@ private static async Task Handle( return Results.NotFound(); } - if (!accessValidator.IsActionAllowed(apiKey.Organization, Actions.GenericWrite)) + if (!accessValidator.IsActionAllowed(apiKey, Actions.GenericWrite)) { return Results.Forbid(); } From 5f35315041c147fca62296433a3dd9dd2d53e7d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Fri, 4 Jul 2025 14:14:20 +0200 Subject: [PATCH 3/5] Add Owner role assignments when creating stuff --- .../Endpoints/ApiKeys/CreateApiKeyEndpoint.cs | 11 +++++++++++ .../Endpoints/Images/UploadImageEndpoint.cs | 16 ++++++++++++++-- .../Tournaments/CreateTournamentEndpoint.cs | 14 ++++++++++++++ .../Tournaments/SetTournamentFolderEndpoint.cs | 13 +++++++++++++ .../Endpoints/Venues/CreateVenueEndpoint.cs | 13 +++++++++++++ 5 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/Turnierplan.App/Endpoints/ApiKeys/CreateApiKeyEndpoint.cs b/src/Turnierplan.App/Endpoints/ApiKeys/CreateApiKeyEndpoint.cs index 6a51ce69..c182f1ea 100644 --- a/src/Turnierplan.App/Endpoints/ApiKeys/CreateApiKeyEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/ApiKeys/CreateApiKeyEndpoint.cs @@ -10,6 +10,7 @@ using Turnierplan.Core.Organization; using Turnierplan.Core.PublicId; using Turnierplan.Core.RoleAssignment; +using Turnierplan.Core.User; using Turnierplan.Dal; using Turnierplan.Dal.Extensions; @@ -25,6 +26,8 @@ internal sealed class CreateApiKeyEndpoint : EndpointBase private static async Task Handle( [FromBody] CreateApiKeyEndpointRequest request, + HttpContext httpContext, + IUserRepository userRepository, IOrganizationRepository organizationRepository, IAccessValidator accessValidator, IPasswordHasher secretHasher, @@ -38,6 +41,13 @@ private static async Task Handle( return result; } + var user = await userRepository.GetByIdAsync(httpContext.GetCurrentUserIdOrThrow()).ConfigureAwait(false); + + if (user is null) + { + return Results.Unauthorized(); + } + var organization = await organizationRepository.GetByPublicIdAsync(request.OrganizationId).ConfigureAwait(false); if (organization is null) @@ -53,6 +63,7 @@ private static async Task Handle( var expiryDate = DateTime.UtcNow.AddDays(request.Validity); var apiKey = new ApiKey(organization, request.Name.Trim(), request.Description.Trim(), expiryDate); + apiKey.AddRoleAssignment(Role.Owner, user.AsPrincipal()); apiKey.AssignNewSecret(plainText => secretHasher.HashPassword(apiKey, plainText), out var secret); await apiKeyRepository.CreateAsync(apiKey).ConfigureAwait(false); diff --git a/src/Turnierplan.App/Endpoints/Images/UploadImageEndpoint.cs b/src/Turnierplan.App/Endpoints/Images/UploadImageEndpoint.cs index a96a4961..59275049 100644 --- a/src/Turnierplan.App/Endpoints/Images/UploadImageEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/Images/UploadImageEndpoint.cs @@ -5,9 +5,12 @@ using Turnierplan.App.Mapping; using Turnierplan.App.Models; using Turnierplan.App.Security; +using Turnierplan.Core.Extensions; using Turnierplan.Core.Image; using Turnierplan.Core.Organization; using Turnierplan.Core.PublicId; +using Turnierplan.Core.RoleAssignment; +using Turnierplan.Core.User; using Turnierplan.Dal; using Turnierplan.ImageStorage; @@ -25,8 +28,9 @@ internal sealed class UploadImageEndpoint : EndpointBase protected override Delegate Handler => Handle; private static async Task Handle( - // Note the usage of [FromForm] instead of [FromBody] - [FromForm] UploadImageEndpointRequest request, + [FromForm] UploadImageEndpointRequest request, // note that we use [FromForm] instead of [FromBody] + HttpContext httpContext, + IUserRepository userRepository, IOrganizationRepository organizationRepository, IAccessValidator accessValidator, IImageStorage imageStorage, @@ -44,6 +48,13 @@ private static async Task Handle( return Results.BadRequest("Invalid organization ID provided."); } + var user = await userRepository.GetByIdAsync(httpContext.GetCurrentUserIdOrThrow()).ConfigureAwait(false); + + if (user is null) + { + return Results.Unauthorized(); + } + var organization = await organizationRepository.GetByPublicIdAsync(organizationId.Value).ConfigureAwait(false); if (organization is null) @@ -93,6 +104,7 @@ private static async Task Handle( memoryStream.Seek(0, SeekOrigin.Begin); var image = new Image(organization, request.ImageName, request.ImageType, "webp", memoryStream.Length, (ushort)imageData.Width, (ushort)imageData.Height); + image.AddRoleAssignment(Role.Owner, user.AsPrincipal()); // Dispose here because Image() ctor accesses width and height of imageData imageData.Dispose(); diff --git a/src/Turnierplan.App/Endpoints/Tournaments/CreateTournamentEndpoint.cs b/src/Turnierplan.App/Endpoints/Tournaments/CreateTournamentEndpoint.cs index 8eba841d..4aa62d09 100644 --- a/src/Turnierplan.App/Endpoints/Tournaments/CreateTournamentEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/Tournaments/CreateTournamentEndpoint.cs @@ -4,10 +4,13 @@ using Turnierplan.App.Mapping; using Turnierplan.App.Models; using Turnierplan.App.Security; +using Turnierplan.Core.Extensions; using Turnierplan.Core.Folder; using Turnierplan.Core.Organization; using Turnierplan.Core.PublicId; +using Turnierplan.Core.RoleAssignment; using Turnierplan.Core.Tournament; +using Turnierplan.Core.User; using Turnierplan.Dal; namespace Turnierplan.App.Endpoints.Tournaments; @@ -22,6 +25,8 @@ internal sealed class CreateTournamentEndpoint : EndpointBase private static async Task Handle( [FromBody] CreateTournamentEndpointRequest request, + HttpContext httpContext, + IUserRepository userRepository, IOrganizationRepository organizationRepository, IAccessValidator accessValidator, IFolderRepository folderRepository, @@ -34,6 +39,13 @@ private static async Task Handle( return result; } + var user = await userRepository.GetByIdAsync(httpContext.GetCurrentUserIdOrThrow()).ConfigureAwait(false); + + if (user is null) + { + return Results.Unauthorized(); + } + var queryDetails = request.FolderId is not null ? IOrganizationRepository.Include.Folders : IOrganizationRepository.Include.None; var organization = await organizationRepository.GetByPublicIdAsync(request.OrganizationId, queryDetails).ConfigureAwait(false); @@ -61,11 +73,13 @@ private static async Task Handle( else if (request.FolderName is not null) { folder = new Folder(organization, request.FolderName); + folder.AddRoleAssignment(Role.Owner, user.AsPrincipal()); await folderRepository.CreateAsync(folder).ConfigureAwait(false); } var tournament = new Tournament(organization, request.Name.Trim(), request.Visibility); + tournament.AddRoleAssignment(Role.Owner, user.AsPrincipal()); tournament.SetFolder(folder); await tournamentRepository.CreateAsync(tournament).ConfigureAwait(false); diff --git a/src/Turnierplan.App/Endpoints/Tournaments/SetTournamentFolderEndpoint.cs b/src/Turnierplan.App/Endpoints/Tournaments/SetTournamentFolderEndpoint.cs index 8fe20736..b8b21ea0 100644 --- a/src/Turnierplan.App/Endpoints/Tournaments/SetTournamentFolderEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/Tournaments/SetTournamentFolderEndpoint.cs @@ -2,9 +2,12 @@ using Microsoft.AspNetCore.Mvc; using Turnierplan.App.Extensions; using Turnierplan.App.Security; +using Turnierplan.Core.Extensions; using Turnierplan.Core.Folder; using Turnierplan.Core.PublicId; +using Turnierplan.Core.RoleAssignment; using Turnierplan.Core.Tournament; +using Turnierplan.Core.User; using Turnierplan.Dal; namespace Turnierplan.App.Endpoints.Tournaments; @@ -20,6 +23,8 @@ internal sealed class SetTournamentFolderEndpoint : EndpointBase private static async Task Handle( [FromRoute] PublicId id, [FromBody] SetTournamentFolderEndpointRequest request, + HttpContext httpContext, + IUserRepository userRepository, ITournamentRepository tournamentRepository, IFolderRepository folderRepository, IAccessValidator accessValidator, @@ -55,7 +60,15 @@ private static async Task Handle( { // Assign tournament to a newly created folder + var user = await userRepository.GetByIdAsync(httpContext.GetCurrentUserIdOrThrow()).ConfigureAwait(false); + + if (user is null) + { + return Results.Unauthorized(); + } + var folder = new Folder(tournament.Organization, request.FolderName); + folder.AddRoleAssignment(Role.Owner, user.AsPrincipal()); tournament.SetFolder(folder); diff --git a/src/Turnierplan.App/Endpoints/Venues/CreateVenueEndpoint.cs b/src/Turnierplan.App/Endpoints/Venues/CreateVenueEndpoint.cs index 80f01aea..73b5e442 100644 --- a/src/Turnierplan.App/Endpoints/Venues/CreateVenueEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/Venues/CreateVenueEndpoint.cs @@ -4,8 +4,11 @@ using Turnierplan.App.Mapping; using Turnierplan.App.Models; using Turnierplan.App.Security; +using Turnierplan.Core.Extensions; using Turnierplan.Core.Organization; using Turnierplan.Core.PublicId; +using Turnierplan.Core.RoleAssignment; +using Turnierplan.Core.User; using Turnierplan.Core.Venue; using Turnierplan.Dal; @@ -21,6 +24,8 @@ internal sealed class CreateVenueEndpoint : EndpointBase private static async Task Handle( [FromBody] CreateVenueEndpointRequest request, + HttpContext httpContext, + IUserRepository userRepository, IOrganizationRepository organizationRepository, IAccessValidator accessValidator, IVenueRepository venueRepository, @@ -32,6 +37,13 @@ private static async Task Handle( return result; } + var user = await userRepository.GetByIdAsync(httpContext.GetCurrentUserIdOrThrow()).ConfigureAwait(false); + + if (user is null) + { + return Results.Unauthorized(); + } + var organization = await organizationRepository.GetByPublicIdAsync(request.OrganizationId).ConfigureAwait(false); if (organization is null) @@ -45,6 +57,7 @@ private static async Task Handle( } var venue = new Venue(organization, request.Name.Trim(), string.Empty); + venue.AddRoleAssignment(Role.Owner, user.AsPrincipal()); await venueRepository.CreateAsync(venue).ConfigureAwait(false); await venueRepository.UnitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); From 66952a917c6020eda5e2a642cfd76dd19020d781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sun, 6 Jul 2025 09:14:08 +0200 Subject: [PATCH 4/5] Revert "Add Owner role assignments when creating stuff" This reverts commit 5f35315041c147fca62296433a3dd9dd2d53e7d9. --- .../Endpoints/ApiKeys/CreateApiKeyEndpoint.cs | 11 ----------- .../Endpoints/Images/UploadImageEndpoint.cs | 16 ++-------------- .../Tournaments/CreateTournamentEndpoint.cs | 14 -------------- .../Tournaments/SetTournamentFolderEndpoint.cs | 13 ------------- .../Endpoints/Venues/CreateVenueEndpoint.cs | 13 ------------- 5 files changed, 2 insertions(+), 65 deletions(-) diff --git a/src/Turnierplan.App/Endpoints/ApiKeys/CreateApiKeyEndpoint.cs b/src/Turnierplan.App/Endpoints/ApiKeys/CreateApiKeyEndpoint.cs index c182f1ea..6a51ce69 100644 --- a/src/Turnierplan.App/Endpoints/ApiKeys/CreateApiKeyEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/ApiKeys/CreateApiKeyEndpoint.cs @@ -10,7 +10,6 @@ using Turnierplan.Core.Organization; using Turnierplan.Core.PublicId; using Turnierplan.Core.RoleAssignment; -using Turnierplan.Core.User; using Turnierplan.Dal; using Turnierplan.Dal.Extensions; @@ -26,8 +25,6 @@ internal sealed class CreateApiKeyEndpoint : EndpointBase private static async Task Handle( [FromBody] CreateApiKeyEndpointRequest request, - HttpContext httpContext, - IUserRepository userRepository, IOrganizationRepository organizationRepository, IAccessValidator accessValidator, IPasswordHasher secretHasher, @@ -41,13 +38,6 @@ private static async Task Handle( return result; } - var user = await userRepository.GetByIdAsync(httpContext.GetCurrentUserIdOrThrow()).ConfigureAwait(false); - - if (user is null) - { - return Results.Unauthorized(); - } - var organization = await organizationRepository.GetByPublicIdAsync(request.OrganizationId).ConfigureAwait(false); if (organization is null) @@ -63,7 +53,6 @@ private static async Task Handle( var expiryDate = DateTime.UtcNow.AddDays(request.Validity); var apiKey = new ApiKey(organization, request.Name.Trim(), request.Description.Trim(), expiryDate); - apiKey.AddRoleAssignment(Role.Owner, user.AsPrincipal()); apiKey.AssignNewSecret(plainText => secretHasher.HashPassword(apiKey, plainText), out var secret); await apiKeyRepository.CreateAsync(apiKey).ConfigureAwait(false); diff --git a/src/Turnierplan.App/Endpoints/Images/UploadImageEndpoint.cs b/src/Turnierplan.App/Endpoints/Images/UploadImageEndpoint.cs index 59275049..a96a4961 100644 --- a/src/Turnierplan.App/Endpoints/Images/UploadImageEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/Images/UploadImageEndpoint.cs @@ -5,12 +5,9 @@ using Turnierplan.App.Mapping; using Turnierplan.App.Models; using Turnierplan.App.Security; -using Turnierplan.Core.Extensions; using Turnierplan.Core.Image; using Turnierplan.Core.Organization; using Turnierplan.Core.PublicId; -using Turnierplan.Core.RoleAssignment; -using Turnierplan.Core.User; using Turnierplan.Dal; using Turnierplan.ImageStorage; @@ -28,9 +25,8 @@ internal sealed class UploadImageEndpoint : EndpointBase protected override Delegate Handler => Handle; private static async Task Handle( - [FromForm] UploadImageEndpointRequest request, // note that we use [FromForm] instead of [FromBody] - HttpContext httpContext, - IUserRepository userRepository, + // Note the usage of [FromForm] instead of [FromBody] + [FromForm] UploadImageEndpointRequest request, IOrganizationRepository organizationRepository, IAccessValidator accessValidator, IImageStorage imageStorage, @@ -48,13 +44,6 @@ private static async Task Handle( return Results.BadRequest("Invalid organization ID provided."); } - var user = await userRepository.GetByIdAsync(httpContext.GetCurrentUserIdOrThrow()).ConfigureAwait(false); - - if (user is null) - { - return Results.Unauthorized(); - } - var organization = await organizationRepository.GetByPublicIdAsync(organizationId.Value).ConfigureAwait(false); if (organization is null) @@ -104,7 +93,6 @@ private static async Task Handle( memoryStream.Seek(0, SeekOrigin.Begin); var image = new Image(organization, request.ImageName, request.ImageType, "webp", memoryStream.Length, (ushort)imageData.Width, (ushort)imageData.Height); - image.AddRoleAssignment(Role.Owner, user.AsPrincipal()); // Dispose here because Image() ctor accesses width and height of imageData imageData.Dispose(); diff --git a/src/Turnierplan.App/Endpoints/Tournaments/CreateTournamentEndpoint.cs b/src/Turnierplan.App/Endpoints/Tournaments/CreateTournamentEndpoint.cs index 4aa62d09..8eba841d 100644 --- a/src/Turnierplan.App/Endpoints/Tournaments/CreateTournamentEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/Tournaments/CreateTournamentEndpoint.cs @@ -4,13 +4,10 @@ using Turnierplan.App.Mapping; using Turnierplan.App.Models; using Turnierplan.App.Security; -using Turnierplan.Core.Extensions; using Turnierplan.Core.Folder; using Turnierplan.Core.Organization; using Turnierplan.Core.PublicId; -using Turnierplan.Core.RoleAssignment; using Turnierplan.Core.Tournament; -using Turnierplan.Core.User; using Turnierplan.Dal; namespace Turnierplan.App.Endpoints.Tournaments; @@ -25,8 +22,6 @@ internal sealed class CreateTournamentEndpoint : EndpointBase private static async Task Handle( [FromBody] CreateTournamentEndpointRequest request, - HttpContext httpContext, - IUserRepository userRepository, IOrganizationRepository organizationRepository, IAccessValidator accessValidator, IFolderRepository folderRepository, @@ -39,13 +34,6 @@ private static async Task Handle( return result; } - var user = await userRepository.GetByIdAsync(httpContext.GetCurrentUserIdOrThrow()).ConfigureAwait(false); - - if (user is null) - { - return Results.Unauthorized(); - } - var queryDetails = request.FolderId is not null ? IOrganizationRepository.Include.Folders : IOrganizationRepository.Include.None; var organization = await organizationRepository.GetByPublicIdAsync(request.OrganizationId, queryDetails).ConfigureAwait(false); @@ -73,13 +61,11 @@ private static async Task Handle( else if (request.FolderName is not null) { folder = new Folder(organization, request.FolderName); - folder.AddRoleAssignment(Role.Owner, user.AsPrincipal()); await folderRepository.CreateAsync(folder).ConfigureAwait(false); } var tournament = new Tournament(organization, request.Name.Trim(), request.Visibility); - tournament.AddRoleAssignment(Role.Owner, user.AsPrincipal()); tournament.SetFolder(folder); await tournamentRepository.CreateAsync(tournament).ConfigureAwait(false); diff --git a/src/Turnierplan.App/Endpoints/Tournaments/SetTournamentFolderEndpoint.cs b/src/Turnierplan.App/Endpoints/Tournaments/SetTournamentFolderEndpoint.cs index b8b21ea0..8fe20736 100644 --- a/src/Turnierplan.App/Endpoints/Tournaments/SetTournamentFolderEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/Tournaments/SetTournamentFolderEndpoint.cs @@ -2,12 +2,9 @@ using Microsoft.AspNetCore.Mvc; using Turnierplan.App.Extensions; using Turnierplan.App.Security; -using Turnierplan.Core.Extensions; using Turnierplan.Core.Folder; using Turnierplan.Core.PublicId; -using Turnierplan.Core.RoleAssignment; using Turnierplan.Core.Tournament; -using Turnierplan.Core.User; using Turnierplan.Dal; namespace Turnierplan.App.Endpoints.Tournaments; @@ -23,8 +20,6 @@ internal sealed class SetTournamentFolderEndpoint : EndpointBase private static async Task Handle( [FromRoute] PublicId id, [FromBody] SetTournamentFolderEndpointRequest request, - HttpContext httpContext, - IUserRepository userRepository, ITournamentRepository tournamentRepository, IFolderRepository folderRepository, IAccessValidator accessValidator, @@ -60,15 +55,7 @@ private static async Task Handle( { // Assign tournament to a newly created folder - var user = await userRepository.GetByIdAsync(httpContext.GetCurrentUserIdOrThrow()).ConfigureAwait(false); - - if (user is null) - { - return Results.Unauthorized(); - } - var folder = new Folder(tournament.Organization, request.FolderName); - folder.AddRoleAssignment(Role.Owner, user.AsPrincipal()); tournament.SetFolder(folder); diff --git a/src/Turnierplan.App/Endpoints/Venues/CreateVenueEndpoint.cs b/src/Turnierplan.App/Endpoints/Venues/CreateVenueEndpoint.cs index 73b5e442..80f01aea 100644 --- a/src/Turnierplan.App/Endpoints/Venues/CreateVenueEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/Venues/CreateVenueEndpoint.cs @@ -4,11 +4,8 @@ using Turnierplan.App.Mapping; using Turnierplan.App.Models; using Turnierplan.App.Security; -using Turnierplan.Core.Extensions; using Turnierplan.Core.Organization; using Turnierplan.Core.PublicId; -using Turnierplan.Core.RoleAssignment; -using Turnierplan.Core.User; using Turnierplan.Core.Venue; using Turnierplan.Dal; @@ -24,8 +21,6 @@ internal sealed class CreateVenueEndpoint : EndpointBase private static async Task Handle( [FromBody] CreateVenueEndpointRequest request, - HttpContext httpContext, - IUserRepository userRepository, IOrganizationRepository organizationRepository, IAccessValidator accessValidator, IVenueRepository venueRepository, @@ -37,13 +32,6 @@ private static async Task Handle( return result; } - var user = await userRepository.GetByIdAsync(httpContext.GetCurrentUserIdOrThrow()).ConfigureAwait(false); - - if (user is null) - { - return Results.Unauthorized(); - } - var organization = await organizationRepository.GetByPublicIdAsync(request.OrganizationId).ConfigureAwait(false); if (organization is null) @@ -57,7 +45,6 @@ private static async Task Handle( } var venue = new Venue(organization, request.Name.Trim(), string.Empty); - venue.AddRoleAssignment(Role.Owner, user.AsPrincipal()); await venueRepository.CreateAsync(venue).ConfigureAwait(false); await venueRepository.UnitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); From f188e552799d188b11c9af1dc073c5cf98ddce64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sun, 6 Jul 2025 09:16:48 +0200 Subject: [PATCH 5/5] Switch to using 'PrivilegedDelete' action --- src/Turnierplan.App/Endpoints/ApiKeys/DeleteApiKeyEndpoint.cs | 2 +- .../Endpoints/Documents/DeleteDocumentEndpoint.cs | 2 +- src/Turnierplan.App/Endpoints/Images/DeleteImageEndpoint.cs | 2 +- .../Endpoints/Organizations/DeleteOrganizationEndpoint.cs | 2 +- .../Endpoints/Tournaments/DeleteTournamentEndpoint.cs | 2 +- src/Turnierplan.App/Endpoints/Venues/DeleteVenueEndpoint.cs | 2 +- src/Turnierplan.App/Security/Actions.cs | 4 ++-- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Turnierplan.App/Endpoints/ApiKeys/DeleteApiKeyEndpoint.cs b/src/Turnierplan.App/Endpoints/ApiKeys/DeleteApiKeyEndpoint.cs index deb6fb08..c5da061b 100644 --- a/src/Turnierplan.App/Endpoints/ApiKeys/DeleteApiKeyEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/ApiKeys/DeleteApiKeyEndpoint.cs @@ -26,7 +26,7 @@ private static async Task Handle( return Results.NotFound(); } - if (!accessValidator.IsActionAllowed(apiKey, Actions.GenericDelete)) + if (!accessValidator.IsActionAllowed(apiKey, Actions.GenericWrite)) { return Results.Forbid(); } diff --git a/src/Turnierplan.App/Endpoints/Documents/DeleteDocumentEndpoint.cs b/src/Turnierplan.App/Endpoints/Documents/DeleteDocumentEndpoint.cs index a83c2598..9b6d384a 100644 --- a/src/Turnierplan.App/Endpoints/Documents/DeleteDocumentEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/Documents/DeleteDocumentEndpoint.cs @@ -26,7 +26,7 @@ private static async Task Handle( return Results.NotFound(); } - if (!accessValidator.IsActionAllowed(document.Tournament, Actions.GenericDelete)) + if (!accessValidator.IsActionAllowed(document.Tournament, Actions.GenericWrite)) { return Results.Forbid(); } diff --git a/src/Turnierplan.App/Endpoints/Images/DeleteImageEndpoint.cs b/src/Turnierplan.App/Endpoints/Images/DeleteImageEndpoint.cs index ddc1a5dc..7db4165a 100644 --- a/src/Turnierplan.App/Endpoints/Images/DeleteImageEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/Images/DeleteImageEndpoint.cs @@ -28,7 +28,7 @@ private static async Task Handle( return Results.NotFound(); } - if (!accessValidator.IsActionAllowed(image, Actions.GenericDelete)) + if (!accessValidator.IsActionAllowed(image, Actions.GenericWrite)) { return Results.Forbid(); } diff --git a/src/Turnierplan.App/Endpoints/Organizations/DeleteOrganizationEndpoint.cs b/src/Turnierplan.App/Endpoints/Organizations/DeleteOrganizationEndpoint.cs index 786bdafe..70f54d33 100644 --- a/src/Turnierplan.App/Endpoints/Organizations/DeleteOrganizationEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/Organizations/DeleteOrganizationEndpoint.cs @@ -28,7 +28,7 @@ private static async Task Handle( return Results.NotFound(); } - if (!accessValidator.IsActionAllowed(organization, Actions.GenericDelete)) + if (!accessValidator.IsActionAllowed(organization, Actions.PrivilegedDelete)) { return Results.Forbid(); } diff --git a/src/Turnierplan.App/Endpoints/Tournaments/DeleteTournamentEndpoint.cs b/src/Turnierplan.App/Endpoints/Tournaments/DeleteTournamentEndpoint.cs index 5ba9fc99..1ea6e3e5 100644 --- a/src/Turnierplan.App/Endpoints/Tournaments/DeleteTournamentEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/Tournaments/DeleteTournamentEndpoint.cs @@ -28,7 +28,7 @@ private static async Task Handle( return Results.NotFound(); } - if (!accessValidator.IsActionAllowed(tournament, Actions.GenericDelete)) + if (!accessValidator.IsActionAllowed(tournament, Actions.GenericWrite)) { return Results.Forbid(); } diff --git a/src/Turnierplan.App/Endpoints/Venues/DeleteVenueEndpoint.cs b/src/Turnierplan.App/Endpoints/Venues/DeleteVenueEndpoint.cs index c53b390c..01266e0e 100644 --- a/src/Turnierplan.App/Endpoints/Venues/DeleteVenueEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/Venues/DeleteVenueEndpoint.cs @@ -26,7 +26,7 @@ private static async Task Handle( return Results.NotFound(); } - if (!accessValidator.IsActionAllowed(venue, Actions.GenericDelete)) + if (!accessValidator.IsActionAllowed(venue, Actions.GenericWrite)) { return Results.Forbid(); } diff --git a/src/Turnierplan.App/Security/Actions.cs b/src/Turnierplan.App/Security/Actions.cs index defae370..5a85b255 100644 --- a/src/Turnierplan.App/Security/Actions.cs +++ b/src/Turnierplan.App/Security/Actions.cs @@ -10,9 +10,9 @@ internal static class Actions public static readonly Action ReadOrWriteRoleAssignments = new(Role.Owner); /// - /// Any action that deletes some entity. + /// A special kind of delete action which shall require the role on the target entity. /// - public static readonly Action GenericDelete = new(Role.Owner); + public static readonly Action PrivilegedDelete = new(Role.Owner); /// /// Any action that modifies some entity.