diff --git a/src/Turnierplan.App/Endpoints/RoleAssignments/CreateRoleAssignmentEndpoint.cs b/src/Turnierplan.App/Endpoints/RoleAssignments/CreateRoleAssignmentEndpoint.cs new file mode 100644 index 00000000..ddd14de3 --- /dev/null +++ b/src/Turnierplan.App/Endpoints/RoleAssignments/CreateRoleAssignmentEndpoint.cs @@ -0,0 +1,170 @@ +using FluentValidation; +using Microsoft.AspNetCore.Mvc; +using Turnierplan.App.Extensions; +using Turnierplan.App.Helpers; +using Turnierplan.App.Mapping; +using Turnierplan.App.Models; +using Turnierplan.App.Security; +using Turnierplan.Core.ApiKey; +using Turnierplan.Core.Extensions; +using Turnierplan.Core.Folder; +using Turnierplan.Core.Image; +using Turnierplan.Core.Organization; +using Turnierplan.Core.PublicId; +using Turnierplan.Core.RoleAssignment; +using Turnierplan.Core.SeedWork; +using Turnierplan.Core.Tournament; +using Turnierplan.Core.User; +using Turnierplan.Core.Venue; +using Turnierplan.Dal; + +namespace Turnierplan.App.Endpoints.RoleAssignments; + +internal sealed class CreateRoleAssignmentEndpoint : EndpointBase +{ + protected override HttpMethod Method => HttpMethod.Post; + + protected override string Route => "/api/role-assignments"; + + protected override Delegate Handler => Handle; + + private static async Task Handle( + [FromBody] CreateRoleAssignmentEndpointRequest request, + IApiKeyRepository apiKeyRepository, + IFolderRepository folderRepository, + IImageRepository imageRepository, + IOrganizationRepository organizationRepository, + ITournamentRepository tournamentRepository, + IUserRepository userRepository, + IVenueRepository venueRepository, + IAccessValidator accessValidator, + IServiceProvider serviceProvider, + IMapper mapper, + CancellationToken cancellationToken) + { + if (!Validator.Instance.ValidateAndGetResult(request, out var result)) + { + return result; + } + + if (!RbacScopeHelper.TryParseScopeId(request.ScopeId, out var typeName, out var targetId)) + { + return Results.BadRequest("Invalid scope identifier provided."); + } + + var task = typeName switch + { + "ApiKey" => CreateRoleAssignmentAsync(request, apiKeyRepository, targetId, accessValidator, apiKeyRepository, userRepository, serviceProvider.GetRequiredService>(), mapper, cancellationToken), + "Folder" => CreateRoleAssignmentAsync(request, folderRepository, targetId, accessValidator, apiKeyRepository, userRepository, serviceProvider.GetRequiredService>(), mapper, cancellationToken), + "Image" => CreateRoleAssignmentAsync(request, imageRepository, targetId, accessValidator, apiKeyRepository, userRepository, serviceProvider.GetRequiredService>(), mapper, cancellationToken), + "Organization" => CreateRoleAssignmentAsync(request, organizationRepository, targetId, accessValidator, apiKeyRepository, userRepository, serviceProvider.GetRequiredService>(), mapper, cancellationToken), + "Tournament" => CreateRoleAssignmentAsync(request, tournamentRepository, targetId, accessValidator, apiKeyRepository, userRepository, serviceProvider.GetRequiredService>(), mapper, cancellationToken), + "Venue" => CreateRoleAssignmentAsync(request, venueRepository, targetId, accessValidator, apiKeyRepository, userRepository, serviceProvider.GetRequiredService>(), mapper, cancellationToken), + _ => null + }; + + return task is null + ? Results.BadRequest("Invalid scope identifier provided.") + : await task.ConfigureAwait(false); + } + + private static async Task CreateRoleAssignmentAsync( + CreateRoleAssignmentEndpointRequest request, + IRepositoryWithPublicId repository, + PublicId targetId, + IAccessValidator accessValidator, + IApiKeyRepository apiKeyRepository, + IUserRepository userRepository, + IRoleAssignmentRepository roleAssignmentRepository, + IMapper mapper, + CancellationToken cancellationToken) + where T : Entity, IEntityWithRoleAssignments + { + var entity = await repository.GetByPublicIdAsync(targetId).ConfigureAwait(false); + + if (entity is null) + { + return Results.NotFound(); + } + + if (!accessValidator.IsActionAllowed(entity, Actions.ReadOrWriteRoleAssignments)) + { + return Results.Forbid(); + } + + var principal = await GetPrincipalAsync(request, apiKeyRepository, userRepository).ConfigureAwait(false); + + if (principal is null) + { + return Results.BadRequest("Could not determine principal based on the provided information."); + } + + if (entity.RoleAssignments.Any(x => x.Role == request.Role && x.Principal.Equals(principal))) + { + return Results.Conflict("There already exists a role assignment for the specified principal/role combination."); + } + + var roleAssignment = entity.AddRoleAssignment(request.Role, principal, request.Description); + + await roleAssignmentRepository.CreateAsync(roleAssignment).ConfigureAwait(false); + await repository.UnitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + + return Results.Ok(mapper.Map(roleAssignment)); + } + + private static async Task GetPrincipalAsync( + CreateRoleAssignmentEndpointRequest request, + IApiKeyRepository apiKeyRepository, + IUserRepository userRepository) + { + if (request.ApiKeyId.HasValue) + { + var apiKey = await apiKeyRepository.GetByPublicIdAsync(request.ApiKeyId.Value).ConfigureAwait(false); + + return apiKey?.AsPrincipal(); + } + + if (request.UserEmail is not null) + { + var user = await userRepository.GetByEmailAsync(request.UserEmail).ConfigureAwait(false); + + return user?.AsPrincipal(); + } + + throw new InvalidOperationException("Invalid request object provided."); + } + + public sealed record CreateRoleAssignmentEndpointRequest + { + public required string ScopeId { get; init; } + + public required Role Role { get; init; } + + public required PublicId? ApiKeyId { get; init; } + + public required string? UserEmail { get; init; } + + public required string Description { get; init; } + } + + private sealed class Validator : AbstractValidator + { + public static readonly Validator Instance = new(); + + private Validator() + { + RuleFor(x => x.ScopeId) + .Matches(RbacScopeHelper.ScopeIdRegex()); + + RuleFor(x => x.Role) + .IsInEnum(); + + RuleFor(x => x) + .Must(x => x.ApiKeyId is null ^ x.UserEmail is null) + .WithMessage($"Exactly only one of {nameof(CreateRoleAssignmentEndpointRequest.ApiKeyId)} and {nameof(CreateRoleAssignmentEndpointRequest.UserEmail)} must be specified."); + + RuleFor(x => x.Description) + .MaximumLength(ValidationConstants.RoleAssignment.MaxDescriptionLength); + } + } +} diff --git a/src/Turnierplan.App/Endpoints/RoleAssignments/DeleteRoleAssignmentEndpoint.cs b/src/Turnierplan.App/Endpoints/RoleAssignments/DeleteRoleAssignmentEndpoint.cs new file mode 100644 index 00000000..f255d6e3 --- /dev/null +++ b/src/Turnierplan.App/Endpoints/RoleAssignments/DeleteRoleAssignmentEndpoint.cs @@ -0,0 +1,103 @@ +using Microsoft.AspNetCore.Mvc; +using SkiaSharp; +using Turnierplan.App.Helpers; +using Turnierplan.App.Security; +using Turnierplan.Core.ApiKey; +using Turnierplan.Core.Folder; +using Turnierplan.Core.Image; +using Turnierplan.Core.Organization; +using Turnierplan.Core.PublicId; +using Turnierplan.Core.RoleAssignment; +using Turnierplan.Core.SeedWork; +using Turnierplan.Core.Tournament; +using Turnierplan.Core.Venue; + +namespace Turnierplan.App.Endpoints.RoleAssignments; + +internal sealed class DeleteRoleAssignmentEndpoint : EndpointBase +{ + protected override HttpMethod Method => HttpMethod.Delete; + + protected override string Route => "/api/role-assignments/{scopeId}/{roleAssignmentId}"; + + protected override Delegate Handler => Handle; + + private static async Task Handle( + [FromRoute] string scopeId, + [FromRoute] string roleAssignmentId, + IApiKeyRepository apiKeyRepository, + IFolderRepository folderRepository, + IImageRepository imageRepository, + IOrganizationRepository organizationRepository, + ITournamentRepository tournamentRepository, + IVenueRepository venueRepository, + IAccessValidator accessValidator, + CancellationToken cancellationToken) + { + if (!RbacScopeHelper.TryParseScopeId(scopeId, out var typeName, out var targetId)) + { + return Results.BadRequest("Invalid scope identifier provided."); + } + + if (!Guid.TryParse(roleAssignmentId, out var roleAssignmentGuid)) + { + return Results.BadRequest("Invalid role assignment provided."); + } + + var task = typeName switch + { + "ApiKey" => DeleteRoleAssignmentAsync(apiKeyRepository, targetId, accessValidator, roleAssignmentGuid, cancellationToken), + "Folder" => DeleteRoleAssignmentAsync(folderRepository, targetId, accessValidator, roleAssignmentGuid, cancellationToken), + "Image" => DeleteRoleAssignmentAsync(imageRepository, targetId, accessValidator, roleAssignmentGuid, cancellationToken), + "Organization" => DeleteRoleAssignmentAsync(organizationRepository, targetId, accessValidator, roleAssignmentGuid, cancellationToken), + "Tournament" => DeleteRoleAssignmentAsync(tournamentRepository, targetId, accessValidator, roleAssignmentGuid, cancellationToken), + "Venue" => DeleteRoleAssignmentAsync(venueRepository, targetId, accessValidator, roleAssignmentGuid, cancellationToken), + _ => null + }; + + return task is null + ? Results.BadRequest("Invalid scope identifier provided.") + : await task.ConfigureAwait(false); + } + + private static async Task DeleteRoleAssignmentAsync( + IRepositoryWithPublicId repository, + PublicId targetId, + IAccessValidator accessValidator, + Guid roleAssignmentId, + CancellationToken cancellationToken) + where T : Entity, IEntityWithRoleAssignments + { + var entity = await repository.GetByPublicIdAsync(targetId).ConfigureAwait(false); + + if (entity is null) + { + return Results.NotFound(); + } + + if (!accessValidator.IsActionAllowed(entity, Actions.ReadOrWriteRoleAssignments)) + { + return Results.Forbid(); + } + + var roleAssignment = entity.RoleAssignments.FirstOrDefault(x => x.Id == roleAssignmentId); + + if (roleAssignment is null) + { + return Results.NotFound(); + } + + entity.RemoveRoleAssignment(roleAssignment); + + if (entity is Organization organization && !organization.RoleAssignments.Any(x => x.Role is Role.Owner)) + { + // An organization must always have at least one owner + + return Results.BadRequest("When deleting role assignments from an Organization, at least one owner must always remain."); + } + + await repository.UnitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + + return Results.NoContent(); + } +} diff --git a/src/Turnierplan.App/Endpoints/RoleAssignments/GetRoleAssignmentsEndpoint.cs b/src/Turnierplan.App/Endpoints/RoleAssignments/GetRoleAssignmentsEndpoint.cs new file mode 100644 index 00000000..da41bcb4 --- /dev/null +++ b/src/Turnierplan.App/Endpoints/RoleAssignments/GetRoleAssignmentsEndpoint.cs @@ -0,0 +1,91 @@ +using Microsoft.AspNetCore.Mvc; +using Turnierplan.App.Helpers; +using Turnierplan.App.Mapping; +using Turnierplan.App.Models; +using Turnierplan.App.Security; +using Turnierplan.Core.ApiKey; +using Turnierplan.Core.Folder; +using Turnierplan.Core.Image; +using Turnierplan.Core.Organization; +using Turnierplan.Core.PublicId; +using Turnierplan.Core.SeedWork; +using Turnierplan.Core.Tournament; +using Turnierplan.Core.Venue; + +namespace Turnierplan.App.Endpoints.RoleAssignments; + +internal sealed class GetRoleAssignmentsEndpoint : EndpointBase> +{ + protected override HttpMethod Method => HttpMethod.Get; + + protected override string Route => "/api/role-assignments/{scopeId}"; + + protected override Delegate Handler => Handle; + + private static async Task Handle( + [FromRoute] string scopeId, + IApiKeyRepository apiKeyRepository, + IFolderRepository folderRepository, + IImageRepository imageRepository, + IOrganizationRepository organizationRepository, + ITournamentRepository tournamentRepository, + IVenueRepository venueRepository, + IAccessValidator accessValidator, + IMapper mapper) + { + if (!RbacScopeHelper.TryParseScopeId(scopeId, out var typeName, out var targetId)) + { + return Results.BadRequest("Invalid scope identifier provided."); + } + + var task = typeName switch + { + "ApiKey" => GetRoleAssignmentsAsync(apiKeyRepository, targetId, accessValidator, mapper), + "Folder" => GetRoleAssignmentsAsync(folderRepository, targetId, accessValidator, mapper), + "Image" => GetRoleAssignmentsAsync(imageRepository, targetId, accessValidator, mapper), + "Organization" => GetRoleAssignmentsAsync(organizationRepository, targetId, accessValidator, mapper), + "Tournament" => GetRoleAssignmentsAsync(tournamentRepository, targetId, accessValidator, mapper), + "Venue" => GetRoleAssignmentsAsync(venueRepository, targetId, accessValidator, mapper), + _ => null + }; + + return task is null + ? Results.BadRequest("Invalid scope identifier provided.") + : await task.ConfigureAwait(false); + } + + private static async Task GetRoleAssignmentsAsync(IRepositoryWithPublicId repository, PublicId targetId, IAccessValidator accessValidator, IMapper mapper) + where T : Entity, IEntityWithRoleAssignments + { + var entity = await repository.GetByPublicIdAsync(targetId).ConfigureAwait(false); + + if (entity is null) + { + return Results.NotFound(); + } + + if (!accessValidator.IsActionAllowed(entity, Actions.ReadOrWriteRoleAssignments)) + { + return Results.Forbid(); + } + + var result = new List(); + + result.AddRange(mapper.MapCollection(entity.RoleAssignments)); + + if (entity is IEntityWithOrganization entityWithOrganization) + { + result.AddRange(mapper.MapCollection(entityWithOrganization.Organization.RoleAssignments) + .Select(r => r with { IsInherited = true })); + } + + // Special case in generic method is not the cleanest... + if (entity is Tournament { Folder: not null } tournament) + { + result.AddRange(mapper.MapCollection(tournament.Folder.RoleAssignments) + .Select(r => r with { IsInherited = true })); + } + + return Results.Ok(result); + } +} diff --git a/src/Turnierplan.App/Endpoints/Tournaments/GetTournamentEndpoint.cs b/src/Turnierplan.App/Endpoints/Tournaments/GetTournamentEndpoint.cs index 938bcc0a..7b10f0e7 100644 --- a/src/Turnierplan.App/Endpoints/Tournaments/GetTournamentEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/Tournaments/GetTournamentEndpoint.cs @@ -23,7 +23,7 @@ private static async Task Handle( IAccessValidator accessValidator, IMapper mapper) { - var tournament = await repository.GetByPublicIdAsync(id, ITournamentRepository.Include.GameRelevant | ITournamentRepository.Include.Venue | ITournamentRepository.Include.Folder).ConfigureAwait(false); + var tournament = await repository.GetByPublicIdAsync(id, ITournamentRepository.Include.GameRelevant | ITournamentRepository.Include.Venue).ConfigureAwait(false); if (tournament is null) { diff --git a/src/Turnierplan.App/Helpers/RbacScopeHelper.cs b/src/Turnierplan.App/Helpers/RbacScopeHelper.cs new file mode 100644 index 00000000..4805c003 --- /dev/null +++ b/src/Turnierplan.App/Helpers/RbacScopeHelper.cs @@ -0,0 +1,35 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.RegularExpressions; +using Turnierplan.Core.PublicId; +using Turnierplan.Core.SeedWork; + +namespace Turnierplan.App.Helpers; + +internal static partial class RbacScopeHelper +{ + public static string GetScopeId(this T entity) + where T : Entity, IEntityWithRoleAssignments + { + return $"{typeof(T).Name}:{entity.PublicId.ToString()}"; + } + + public static bool TryParseScopeId(string scopeId, [NotNullWhen(true)] out string? objectTypeName, out PublicId targetObjectId) + { + var match = ScopeIdRegex().Match(scopeId); + + if (!match.Success || !PublicId.TryParse(match.Groups["ObjectId"].Value, out targetObjectId)) + { + objectTypeName = null; + targetObjectId = PublicId.Empty; + + return false; + } + + objectTypeName = match.Groups["TypeName"].Value; + + return true; + } + + [GeneratedRegex(@"^(?ApiKey|Folder|Image|Organization|Tournament|Venue):(?[A-Za-z0-9_-]{11})$")] + public static partial Regex ScopeIdRegex(); +} diff --git a/src/Turnierplan.App/Mapping/Rules/ApiKeyMappingRule.cs b/src/Turnierplan.App/Mapping/Rules/ApiKeyMappingRule.cs index cc8c0399..1e2cbc5c 100644 --- a/src/Turnierplan.App/Mapping/Rules/ApiKeyMappingRule.cs +++ b/src/Turnierplan.App/Mapping/Rules/ApiKeyMappingRule.cs @@ -1,3 +1,4 @@ +using Turnierplan.App.Helpers; using Turnierplan.App.Models; using Turnierplan.Core.ApiKey; @@ -10,6 +11,7 @@ protected override ApiKeyDto Map(IMapper mapper, MappingContext context, ApiKey return new ApiKeyDto { Id = source.PublicId, + RbacScopeId = source.GetScopeId(), Name = source.Name, Description = source.Description, Secret = null, diff --git a/src/Turnierplan.App/Mapping/Rules/FolderMappingRule.cs b/src/Turnierplan.App/Mapping/Rules/FolderMappingRule.cs index 63634dfa..42e188d8 100644 --- a/src/Turnierplan.App/Mapping/Rules/FolderMappingRule.cs +++ b/src/Turnierplan.App/Mapping/Rules/FolderMappingRule.cs @@ -1,3 +1,4 @@ +using Turnierplan.App.Helpers; using Turnierplan.App.Models; using Turnierplan.Core.Folder; @@ -11,6 +12,7 @@ protected override FolderDto Map(IMapper mapper, MappingContext context, Folder { Id = source.PublicId, OrganizationId = source.Organization.PublicId, + RbacScopeId = source.GetScopeId(), Name = source.Name }; } diff --git a/src/Turnierplan.App/Mapping/Rules/ImageMappingRule.cs b/src/Turnierplan.App/Mapping/Rules/ImageMappingRule.cs index 2506e024..b04d43e9 100644 --- a/src/Turnierplan.App/Mapping/Rules/ImageMappingRule.cs +++ b/src/Turnierplan.App/Mapping/Rules/ImageMappingRule.cs @@ -1,3 +1,4 @@ +using Turnierplan.App.Helpers; using Turnierplan.App.Models; using Turnierplan.Core.Image; using Turnierplan.ImageStorage; @@ -18,6 +19,7 @@ protected override ImageDto Map(IMapper mapper, MappingContext context, Image so return new ImageDto { Id = source.PublicId, + RbacScopeId = source.GetScopeId(), CreatedAt = source.CreatedAt, Name = source.Name, Url = _imageStorage.GetFullImageUrl(source), diff --git a/src/Turnierplan.App/Mapping/Rules/OrganizationMappingRule.cs b/src/Turnierplan.App/Mapping/Rules/OrganizationMappingRule.cs index 02c54607..b5b796c0 100644 --- a/src/Turnierplan.App/Mapping/Rules/OrganizationMappingRule.cs +++ b/src/Turnierplan.App/Mapping/Rules/OrganizationMappingRule.cs @@ -1,3 +1,4 @@ +using Turnierplan.App.Helpers; using Turnierplan.App.Models; using Turnierplan.Core.Organization; @@ -10,6 +11,7 @@ protected override OrganizationDto Map(IMapper mapper, MappingContext context, O return new OrganizationDto { Id = source.PublicId, + RbacScopeId = source.GetScopeId(), Name = source.Name }; } diff --git a/src/Turnierplan.App/Mapping/Rules/RoleAssignmentMappingRule.cs b/src/Turnierplan.App/Mapping/Rules/RoleAssignmentMappingRule.cs new file mode 100644 index 00000000..c491bf89 --- /dev/null +++ b/src/Turnierplan.App/Mapping/Rules/RoleAssignmentMappingRule.cs @@ -0,0 +1,46 @@ +using Turnierplan.App.Helpers; +using Turnierplan.App.Models; +using Turnierplan.Core.ApiKey; +using Turnierplan.Core.Folder; +using Turnierplan.Core.Image; +using Turnierplan.Core.Organization; +using Turnierplan.Core.RoleAssignment; +using Turnierplan.Core.SeedWork; +using Turnierplan.Core.Tournament; +using Turnierplan.Core.Venue; + +namespace Turnierplan.App.Mapping.Rules; + +internal abstract class RoleAssignmentMappingRuleBase : MappingRuleBase, RoleAssignmentDto> + where T : Entity, IEntityWithRoleAssignments +{ + protected override RoleAssignmentDto Map(IMapper mapper, MappingContext context, RoleAssignment source) + { + return new RoleAssignmentDto + { + Id = source.Id, + Scope = source.Scope.GetScopeId(), + CreatedAt = source.CreatedAt, + Role = source.Role, + Principal = new PrincipalDto + { + Kind = source.Principal.Kind, + ObjectId = source.Principal.ObjectId + }, + Description = source.Description, + IsInherited = false + }; + } +} + +internal sealed class ApiKeyRoleAssignmentMappingRule : RoleAssignmentMappingRuleBase; + +internal sealed class FolderRoleAssignmentMappingRule : RoleAssignmentMappingRuleBase; + +internal sealed class ImageRoleAssignmentMappingRule : RoleAssignmentMappingRuleBase; + +internal sealed class OrganizationRoleAssignmentMappingRule : RoleAssignmentMappingRuleBase; + +internal sealed class TournamentRoleAssignmentMappingRule : RoleAssignmentMappingRuleBase; + +internal sealed class VenueRoleAssignmentMappingRule : RoleAssignmentMappingRuleBase; diff --git a/src/Turnierplan.App/Mapping/Rules/TournamentMappingRule.cs b/src/Turnierplan.App/Mapping/Rules/TournamentMappingRule.cs index 18058715..5fc599d6 100644 --- a/src/Turnierplan.App/Mapping/Rules/TournamentMappingRule.cs +++ b/src/Turnierplan.App/Mapping/Rules/TournamentMappingRule.cs @@ -1,3 +1,4 @@ +using Turnierplan.App.Helpers; using Turnierplan.App.Models; using Turnierplan.App.Models.Enums; using Turnierplan.Core.Tournament; @@ -14,6 +15,7 @@ protected override TournamentDto Map(IMapper mapper, MappingContext context, Tou { Id = source.PublicId, OrganizationId = source.Organization.PublicId, + RbacScopeId = source.GetScopeId(), FolderId = source.Folder?.PublicId, VenueId = source.Venue?.PublicId, Name = source.Name, diff --git a/src/Turnierplan.App/Mapping/Rules/VenueMappingRule.cs b/src/Turnierplan.App/Mapping/Rules/VenueMappingRule.cs index 82dbff64..3f150c96 100644 --- a/src/Turnierplan.App/Mapping/Rules/VenueMappingRule.cs +++ b/src/Turnierplan.App/Mapping/Rules/VenueMappingRule.cs @@ -1,3 +1,4 @@ +using Turnierplan.App.Helpers; using Turnierplan.App.Models; using Turnierplan.Core.Venue; @@ -11,6 +12,7 @@ protected override VenueDto Map(IMapper mapper, MappingContext context, Venue so { Id = source.PublicId, OrganizationId = source.Organization.PublicId, + RbacScopeId = source.GetScopeId(), Name = source.Name, Description = source.Description, AddressDetails = source.AddressDetails.ToArray(), diff --git a/src/Turnierplan.App/Models/ApiKeyDto.cs b/src/Turnierplan.App/Models/ApiKeyDto.cs index 0c090996..e29b16f4 100644 --- a/src/Turnierplan.App/Models/ApiKeyDto.cs +++ b/src/Turnierplan.App/Models/ApiKeyDto.cs @@ -6,6 +6,8 @@ public sealed record ApiKeyDto { public required PublicId Id { get; init; } + public required string RbacScopeId { get; init; } + public required string Name { get; init; } public required string Description { get; init; } diff --git a/src/Turnierplan.App/Models/FolderDto.cs b/src/Turnierplan.App/Models/FolderDto.cs index ebaf95cb..1d84ef87 100644 --- a/src/Turnierplan.App/Models/FolderDto.cs +++ b/src/Turnierplan.App/Models/FolderDto.cs @@ -8,5 +8,7 @@ public sealed record FolderDto public required PublicId OrganizationId { get; init; } + public required string RbacScopeId { get; init; } + public required string Name { get; init; } } diff --git a/src/Turnierplan.App/Models/ImageDto.cs b/src/Turnierplan.App/Models/ImageDto.cs index 07ecaf31..dba8f82b 100644 --- a/src/Turnierplan.App/Models/ImageDto.cs +++ b/src/Turnierplan.App/Models/ImageDto.cs @@ -6,6 +6,8 @@ public sealed record ImageDto { public required PublicId Id { get; init; } + public required string RbacScopeId { get; init; } + public required DateTime CreatedAt { get; init; } public required string Name { get; init; } diff --git a/src/Turnierplan.App/Models/OrganizationDto.cs b/src/Turnierplan.App/Models/OrganizationDto.cs index ccf69187..7a4e1259 100644 --- a/src/Turnierplan.App/Models/OrganizationDto.cs +++ b/src/Turnierplan.App/Models/OrganizationDto.cs @@ -6,5 +6,7 @@ public sealed record OrganizationDto { public required PublicId Id { get; init; } + public required string RbacScopeId { get; init; } + public required string Name { get; init; } } diff --git a/src/Turnierplan.App/Models/PrincipalDto.cs b/src/Turnierplan.App/Models/PrincipalDto.cs new file mode 100644 index 00000000..0cdc2f42 --- /dev/null +++ b/src/Turnierplan.App/Models/PrincipalDto.cs @@ -0,0 +1,10 @@ +using Turnierplan.Core.RoleAssignment; + +namespace Turnierplan.App.Models; + +public sealed record PrincipalDto +{ + public required PrincipalKind Kind { get; init; } + + public required string ObjectId { get; init; } +} diff --git a/src/Turnierplan.App/Models/RoleAssignmentDto.cs b/src/Turnierplan.App/Models/RoleAssignmentDto.cs new file mode 100644 index 00000000..a1bcffe0 --- /dev/null +++ b/src/Turnierplan.App/Models/RoleAssignmentDto.cs @@ -0,0 +1,20 @@ +using Turnierplan.Core.RoleAssignment; + +namespace Turnierplan.App.Models; + +public sealed record RoleAssignmentDto +{ + public required Guid Id { get; init; } + + public required string Scope { get; init; } + + public required DateTime CreatedAt { get; init; } + + public required Role Role { get; init; } + + public required PrincipalDto Principal { get; init; } + + public required string Description { get; init; } + + public required bool IsInherited { get; init; } +} diff --git a/src/Turnierplan.App/Models/TournamentDto.cs b/src/Turnierplan.App/Models/TournamentDto.cs index 95ed78da..efe6bf5c 100644 --- a/src/Turnierplan.App/Models/TournamentDto.cs +++ b/src/Turnierplan.App/Models/TournamentDto.cs @@ -9,6 +9,8 @@ public sealed record TournamentDto public required PublicId OrganizationId { get; init; } + public required string RbacScopeId { get; init; } + public PublicId? FolderId { get; init; } public PublicId? VenueId { get; init; } diff --git a/src/Turnierplan.App/Models/VenueDto.cs b/src/Turnierplan.App/Models/VenueDto.cs index d38ffa24..4eb4d75e 100644 --- a/src/Turnierplan.App/Models/VenueDto.cs +++ b/src/Turnierplan.App/Models/VenueDto.cs @@ -8,6 +8,8 @@ public sealed record VenueDto public required PublicId OrganizationId { get; init; } + public required string RbacScopeId { get; init; } + public required string Name { get; init; } public required string Description { get; init; } diff --git a/src/Turnierplan.Core/ApiKey/ApiKey.cs b/src/Turnierplan.Core/ApiKey/ApiKey.cs index 29b7a21c..76e50c67 100644 --- a/src/Turnierplan.Core/ApiKey/ApiKey.cs +++ b/src/Turnierplan.Core/ApiKey/ApiKey.cs @@ -3,7 +3,7 @@ namespace Turnierplan.Core.ApiKey; -public sealed class ApiKey : Entity, IEntityWithPublicId, IEntityWithRoleAssignments +public sealed class ApiKey : Entity, IEntityWithRoleAssignments, IEntityWithOrganization { internal readonly List> _roleAssignments = new(); internal readonly List _requests = new(); @@ -67,6 +67,11 @@ public RoleAssignment AddRoleAssignment(Role role, Principal principal, return roleAssignment; } + public void RemoveRoleAssignment(RoleAssignment roleAssignment) + { + _roleAssignments.Remove(roleAssignment); + } + public void AssignNewSecret(Func secretHashFunc, out string plainTextSecret) { plainTextSecret = GenerateSecret(); diff --git a/src/Turnierplan.Core/Folder/Folder.cs b/src/Turnierplan.Core/Folder/Folder.cs index 04f68a63..3eecc3ad 100644 --- a/src/Turnierplan.Core/Folder/Folder.cs +++ b/src/Turnierplan.Core/Folder/Folder.cs @@ -3,7 +3,7 @@ namespace Turnierplan.Core.Folder; -public sealed class Folder : Entity, IEntityWithPublicId, IEntityWithRoleAssignments +public sealed class Folder : Entity, IEntityWithRoleAssignments, IEntityWithOrganization { internal readonly List> _roleAssignments = new(); internal readonly List _tournaments = new(); @@ -48,4 +48,9 @@ public RoleAssignment AddRoleAssignment(Role role, Principal principal, return roleAssignment; } + + public void RemoveRoleAssignment(RoleAssignment roleAssignment) + { + _roleAssignments.Remove(roleAssignment); + } } diff --git a/src/Turnierplan.Core/Image/Image.cs b/src/Turnierplan.Core/Image/Image.cs index d2578226..2f88d043 100644 --- a/src/Turnierplan.Core/Image/Image.cs +++ b/src/Turnierplan.Core/Image/Image.cs @@ -4,7 +4,7 @@ namespace Turnierplan.Core.Image; -public sealed class Image : Entity, IEntityWithPublicId, IEntityWithRoleAssignments +public sealed class Image : Entity, IEntityWithRoleAssignments, IEntityWithOrganization { internal readonly List> _roleAssignments = new(); @@ -73,6 +73,11 @@ public RoleAssignment AddRoleAssignment(Role role, Principal principal, s return roleAssignment; } + public void RemoveRoleAssignment(RoleAssignment roleAssignment) + { + _roleAssignments.Remove(roleAssignment); + } + private static void ValidateImageSize(ImageType type, ushort width, ushort height) { var constraints = ImageConstraints.GetImageConstraints(type); diff --git a/src/Turnierplan.Core/Organization/Organization.cs b/src/Turnierplan.Core/Organization/Organization.cs index 722cae13..377e745e 100644 --- a/src/Turnierplan.Core/Organization/Organization.cs +++ b/src/Turnierplan.Core/Organization/Organization.cs @@ -3,7 +3,7 @@ namespace Turnierplan.Core.Organization; -public sealed class Organization : Entity, IEntityWithPublicId, IEntityWithRoleAssignments +public sealed class Organization : Entity, IEntityWithRoleAssignments { internal readonly List> _roleAssignments = new(); internal readonly List _apiKeys = new(); @@ -55,4 +55,9 @@ public RoleAssignment AddRoleAssignment(Role role, Principal princ return roleAssignment; } + + public void RemoveRoleAssignment(RoleAssignment roleAssignment) + { + _roleAssignments.Remove(roleAssignment); + } } diff --git a/src/Turnierplan.Core/RoleAssignment/IRoleAssignmentRepository.cs b/src/Turnierplan.Core/RoleAssignment/IRoleAssignmentRepository.cs new file mode 100644 index 00000000..e2a745af --- /dev/null +++ b/src/Turnierplan.Core/RoleAssignment/IRoleAssignmentRepository.cs @@ -0,0 +1,6 @@ +using Turnierplan.Core.SeedWork; + +namespace Turnierplan.Core.RoleAssignment; + +public interface IRoleAssignmentRepository : IRepository, Guid> + where T : Entity, IEntityWithRoleAssignments; diff --git a/src/Turnierplan.Core/RoleAssignment/RoleAssignment.cs b/src/Turnierplan.Core/RoleAssignment/RoleAssignment.cs index 75cdd1cf..d1dec9b0 100644 --- a/src/Turnierplan.Core/RoleAssignment/RoleAssignment.cs +++ b/src/Turnierplan.Core/RoleAssignment/RoleAssignment.cs @@ -2,12 +2,12 @@ namespace Turnierplan.Core.RoleAssignment; -public sealed class RoleAssignment : Entity +public sealed class RoleAssignment : Entity where T : Entity, IEntityWithRoleAssignments { internal RoleAssignment(T scope, Role role, Principal principal, string? description = null) { - Id = 0; + Id = Guid.NewGuid(); Scope = scope; CreatedAt = DateTime.UtcNow; Role = role; @@ -15,7 +15,7 @@ internal RoleAssignment(T scope, Role role, Principal principal, string? descrip Description = description ?? string.Empty; } - internal RoleAssignment(long id, DateTime createdAt, Role role, Principal principal, string description) + internal RoleAssignment(Guid id, DateTime createdAt, Role role, Principal principal, string description) { Id = id; CreatedAt = createdAt; @@ -24,7 +24,7 @@ internal RoleAssignment(long id, DateTime createdAt, Role role, Principal princi Description = description; } - public override long Id { get; protected set; } + public override Guid Id { get; protected set; } public T Scope { get; internal set; } = null!; diff --git a/src/Turnierplan.Core/SeedWork/IEntityWithOrganization.cs b/src/Turnierplan.Core/SeedWork/IEntityWithOrganization.cs new file mode 100644 index 00000000..43e91f1e --- /dev/null +++ b/src/Turnierplan.Core/SeedWork/IEntityWithOrganization.cs @@ -0,0 +1,6 @@ +namespace Turnierplan.Core.SeedWork; + +public interface IEntityWithOrganization +{ + Organization.Organization Organization { get; } +} diff --git a/src/Turnierplan.Core/SeedWork/IEntityWithRoleAssignments.cs b/src/Turnierplan.Core/SeedWork/IEntityWithRoleAssignments.cs index 640e89f3..50fe072e 100644 --- a/src/Turnierplan.Core/SeedWork/IEntityWithRoleAssignments.cs +++ b/src/Turnierplan.Core/SeedWork/IEntityWithRoleAssignments.cs @@ -2,10 +2,12 @@ namespace Turnierplan.Core.SeedWork; -public interface IEntityWithRoleAssignments +public interface IEntityWithRoleAssignments : IEntityWithPublicId where T : Entity, IEntityWithRoleAssignments { IReadOnlyList> RoleAssignments { get; } RoleAssignment AddRoleAssignment(Role role, Principal principal, string? description = null); + + void RemoveRoleAssignment(RoleAssignment roleAssignment); } diff --git a/src/Turnierplan.Core/Tournament/ITournamentRepository.cs b/src/Turnierplan.Core/Tournament/ITournamentRepository.cs index d651d6e8..fa8a1db1 100644 --- a/src/Turnierplan.Core/Tournament/ITournamentRepository.cs +++ b/src/Turnierplan.Core/Tournament/ITournamentRepository.cs @@ -15,9 +15,8 @@ public enum Include Matches = 4, Documents = 8, Venue = 16, - Folder = 32, - FolderWithTournaments = 64, - Images = 128, + FolderWithTournaments = 32, + Images = 64, GameRelevant = Teams | Groups | Matches } diff --git a/src/Turnierplan.Core/Tournament/Tournament.cs b/src/Turnierplan.Core/Tournament/Tournament.cs index 135033a5..74ab2d14 100644 --- a/src/Turnierplan.Core/Tournament/Tournament.cs +++ b/src/Turnierplan.Core/Tournament/Tournament.cs @@ -9,7 +9,7 @@ namespace Turnierplan.Core.Tournament; -public sealed class Tournament : Entity, IEntityWithPublicId, IEntityWithRoleAssignments +public sealed class Tournament : Entity, IEntityWithRoleAssignments, IEntityWithOrganization { internal readonly GroupParticipantComparer _groupParticipantComparer; internal int? _nextEntityId; @@ -155,6 +155,11 @@ public RoleAssignment AddRoleAssignment(Role role, Principal princip return roleAssignment; } + public void RemoveRoleAssignment(RoleAssignment roleAssignment) + { + _roleAssignments.Remove(roleAssignment); + } + public Team AddTeam(string name) { var team = new Team(GetNextId(), name); diff --git a/src/Turnierplan.Core/Venue/Venue.cs b/src/Turnierplan.Core/Venue/Venue.cs index 1de0f9ba..0978ad7f 100644 --- a/src/Turnierplan.Core/Venue/Venue.cs +++ b/src/Turnierplan.Core/Venue/Venue.cs @@ -3,7 +3,7 @@ namespace Turnierplan.Core.Venue; -public sealed class Venue : Entity, IEntityWithPublicId, IEntityWithRoleAssignments +public sealed class Venue : Entity, IEntityWithRoleAssignments, IEntityWithOrganization { internal readonly List> _roleAssignments = new(); internal readonly List _tournaments = new(); @@ -56,4 +56,9 @@ public RoleAssignment AddRoleAssignment(Role role, Principal principal, s return roleAssignment; } + + public void RemoveRoleAssignment(RoleAssignment roleAssignment) + { + _roleAssignments.Remove(roleAssignment); + } } diff --git a/src/Turnierplan.Dal/Extensions/ServiceCollectionExtensions.cs b/src/Turnierplan.Dal/Extensions/ServiceCollectionExtensions.cs index c6225e73..4dad355c 100644 --- a/src/Turnierplan.Dal/Extensions/ServiceCollectionExtensions.cs +++ b/src/Turnierplan.Dal/Extensions/ServiceCollectionExtensions.cs @@ -10,6 +10,7 @@ using Turnierplan.Core.Folder; using Turnierplan.Core.Image; using Turnierplan.Core.Organization; +using Turnierplan.Core.RoleAssignment; using Turnierplan.Core.Tournament; using Turnierplan.Core.User; using Turnierplan.Core.Venue; @@ -58,6 +59,13 @@ public static IServiceCollection AddTurnierplanDataAccessLayer(this IServiceColl services.AddScoped(); services.AddScoped(); + services.AddScoped, ApiKeyRoleAssignmentRepository>(); + services.AddScoped, FolderRoleAssignmentRepository>(); + services.AddScoped, ImageRoleAssignmentRepository>(); + services.AddScoped, OrganizationRoleAssignmentRepository>(); + services.AddScoped, TournamentRoleAssignmentRepository>(); + services.AddScoped, VenueRoleAssignmentRepository>(); + return services; } } diff --git a/src/Turnierplan.Dal/Migrations/20250615124413_Add_RBAC.Designer.cs b/src/Turnierplan.Dal/Migrations/20250615154535_Add_RBAC.Designer.cs similarity index 97% rename from src/Turnierplan.Dal/Migrations/20250615124413_Add_RBAC.Designer.cs rename to src/Turnierplan.Dal/Migrations/20250615154535_Add_RBAC.Designer.cs index cb307f87..24a40f37 100644 --- a/src/Turnierplan.Dal/Migrations/20250615124413_Add_RBAC.Designer.cs +++ b/src/Turnierplan.Dal/Migrations/20250615154535_Add_RBAC.Designer.cs @@ -13,7 +13,7 @@ namespace Turnierplan.Dal.Migrations { [DbContext(typeof(TurnierplanContext))] - [Migration("20250615124413_Add_RBAC")] + [Migration("20250615154535_Add_RBAC")] partial class Add_RBAC { /// @@ -266,11 +266,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + .HasColumnType("uuid"); b.Property("ApiKeyId") .HasColumnType("bigint"); @@ -299,11 +297,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + .HasColumnType("uuid"); b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); @@ -332,11 +328,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + .HasColumnType("uuid"); b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); @@ -365,11 +359,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + .HasColumnType("uuid"); b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); @@ -398,11 +390,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + .HasColumnType("uuid"); b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); @@ -431,11 +421,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + .HasColumnType("uuid"); b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); diff --git a/src/Turnierplan.Dal/Migrations/20250615124413_Add_RBAC.cs b/src/Turnierplan.Dal/Migrations/20250615154535_Add_RBAC.cs similarity index 90% rename from src/Turnierplan.Dal/Migrations/20250615124413_Add_RBAC.cs rename to src/Turnierplan.Dal/Migrations/20250615154535_Add_RBAC.cs index ba987f69..99d20e83 100644 --- a/src/Turnierplan.Dal/Migrations/20250615124413_Add_RBAC.cs +++ b/src/Turnierplan.Dal/Migrations/20250615154535_Add_RBAC.cs @@ -1,6 +1,5 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; #nullable disable @@ -46,8 +45,7 @@ protected override void Up(MigrationBuilder migrationBuilder) schema: "turnierplan", columns: table => new { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Id = table.Column(type: "uuid", nullable: false), ApiKeyId = table.Column(type: "bigint", nullable: false), CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), Role = table.Column(type: "integer", nullable: false), @@ -71,8 +69,7 @@ protected override void Up(MigrationBuilder migrationBuilder) schema: "turnierplan", columns: table => new { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Id = table.Column(type: "uuid", nullable: false), FolderId = table.Column(type: "bigint", nullable: false), CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), Role = table.Column(type: "integer", nullable: false), @@ -96,8 +93,7 @@ protected override void Up(MigrationBuilder migrationBuilder) schema: "turnierplan", columns: table => new { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Id = table.Column(type: "uuid", nullable: false), ImageId = table.Column(type: "bigint", nullable: false), CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), Role = table.Column(type: "integer", nullable: false), @@ -121,8 +117,7 @@ protected override void Up(MigrationBuilder migrationBuilder) schema: "turnierplan", columns: table => new { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Id = table.Column(type: "uuid", nullable: false), OrganizationId = table.Column(type: "bigint", nullable: false), CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), Role = table.Column(type: "integer", nullable: false), @@ -146,8 +141,7 @@ protected override void Up(MigrationBuilder migrationBuilder) schema: "turnierplan", columns: table => new { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Id = table.Column(type: "uuid", nullable: false), TournamentId = table.Column(type: "bigint", nullable: false), CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), Role = table.Column(type: "integer", nullable: false), @@ -171,8 +165,7 @@ protected override void Up(MigrationBuilder migrationBuilder) schema: "turnierplan", columns: table => new { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Id = table.Column(type: "uuid", nullable: false), VenueId = table.Column(type: "bigint", nullable: false), CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), Role = table.Column(type: "integer", nullable: false), @@ -233,8 +226,8 @@ protected override void Up(MigrationBuilder migrationBuilder) // 1000 is the numerical value for the "Owner" role migrationBuilder.Sql(""" -INSERT INTO turnierplan."IAM_Organization" ("OrganizationId", "CreatedAt", "Role", "Principal", "Description") -SELECT "Organizations"."Id", NOW(), 1000, ('User:' || "Organizations"."OwnerId"), '' +INSERT INTO turnierplan."IAM_Organization" ("Id", "OrganizationId", "CreatedAt", "Role", "Principal", "Description") +SELECT gen_random_uuid(), "Organizations"."Id", NOW(), 1000, ('User:' || "Organizations"."OwnerId"), '' FROM turnierplan."Organizations"; """); diff --git a/src/Turnierplan.Dal/Migrations/TurnierplanContextModelSnapshot.cs b/src/Turnierplan.Dal/Migrations/TurnierplanContextModelSnapshot.cs index c975cf02..0b302773 100644 --- a/src/Turnierplan.Dal/Migrations/TurnierplanContextModelSnapshot.cs +++ b/src/Turnierplan.Dal/Migrations/TurnierplanContextModelSnapshot.cs @@ -263,11 +263,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + .HasColumnType("uuid"); b.Property("ApiKeyId") .HasColumnType("bigint"); @@ -296,11 +294,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + .HasColumnType("uuid"); b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); @@ -329,11 +325,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + .HasColumnType("uuid"); b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); @@ -362,11 +356,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + .HasColumnType("uuid"); b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); @@ -395,11 +387,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + .HasColumnType("uuid"); b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); @@ -428,11 +418,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Turnierplan.Core.RoleAssignment.RoleAssignment", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + .HasColumnType("uuid"); b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); diff --git a/src/Turnierplan.Dal/Repositories/RoleAssignmentRepositoryBase.cs b/src/Turnierplan.Dal/Repositories/RoleAssignmentRepositoryBase.cs new file mode 100644 index 00000000..6319ecc7 --- /dev/null +++ b/src/Turnierplan.Dal/Repositories/RoleAssignmentRepositoryBase.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore; +using Turnierplan.Core.ApiKey; +using Turnierplan.Core.Folder; +using Turnierplan.Core.Image; +using Turnierplan.Core.Organization; +using Turnierplan.Core.RoleAssignment; +using Turnierplan.Core.SeedWork; +using Turnierplan.Core.Tournament; +using Turnierplan.Core.Venue; + +namespace Turnierplan.Dal.Repositories; + +internal abstract class RoleAssignmentRepositoryBase(TurnierplanContext context, DbSet> dbSet) : RepositoryBase, Guid>(context, dbSet), IRoleAssignmentRepository + where T : Entity, IEntityWithRoleAssignments; + +internal sealed class ApiKeyRoleAssignmentRepository(TurnierplanContext context) : RoleAssignmentRepositoryBase(context, context.ApiKeyRoleAssignments); + +internal sealed class FolderRoleAssignmentRepository(TurnierplanContext context) : RoleAssignmentRepositoryBase(context, context.FolderRoleAssignments); + +internal sealed class ImageRoleAssignmentRepository(TurnierplanContext context) : RoleAssignmentRepositoryBase(context, context.ImageRoleAssignments); + +internal sealed class OrganizationRoleAssignmentRepository(TurnierplanContext context) : RoleAssignmentRepositoryBase(context, context.OrganizationRoleAssignments); + +internal sealed class TournamentRoleAssignmentRepository(TurnierplanContext context) : RoleAssignmentRepositoryBase(context, context.TournamentRoleAssignments); + +internal sealed class VenueRoleAssignmentRepository(TurnierplanContext context) : RoleAssignmentRepositoryBase(context, context.VenueRoleAssignments); diff --git a/src/Turnierplan.Dal/Repositories/TournamentRepository.cs b/src/Turnierplan.Dal/Repositories/TournamentRepository.cs index 3ba7f40e..302fbbee 100644 --- a/src/Turnierplan.Dal/Repositories/TournamentRepository.cs +++ b/src/Turnierplan.Dal/Repositories/TournamentRepository.cs @@ -48,11 +48,6 @@ internal sealed class TournamentRepository(TurnierplanContext context) : Reposit if (include.HasFlag(ITournamentRepository.Include.FolderWithTournaments)) { query = query.Include(x => x.Folder).ThenInclude(x => x!.Tournaments); - query = query.Include(x => x.Folder).ThenInclude(x => x!.RoleAssignments); - } - else if (include.HasFlag(ITournamentRepository.Include.Folder)) - { - query = query.Include(x => x.Folder).ThenInclude(x => x!.RoleAssignments); } if (include.HasFlag(ITournamentRepository.Include.Images)) @@ -63,6 +58,7 @@ internal sealed class TournamentRepository(TurnierplanContext context) : Reposit } query = query.Include(x => x.Organization).ThenInclude(x => x.RoleAssignments); + query = query.Include(x => x.Folder).ThenInclude(x => x!.RoleAssignments); query = query.Include(x => x.RoleAssignments); query = query.AsSplitQuery();