Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<RoleAssignmentDto>
{
protected override HttpMethod Method => HttpMethod.Post;

protected override string Route => "/api/role-assignments";

protected override Delegate Handler => Handle;

private static async Task<IResult> 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<IRoleAssignmentRepository<ApiKey>>(), mapper, cancellationToken),
"Folder" => CreateRoleAssignmentAsync(request, folderRepository, targetId, accessValidator, apiKeyRepository, userRepository, serviceProvider.GetRequiredService<IRoleAssignmentRepository<Folder>>(), mapper, cancellationToken),
"Image" => CreateRoleAssignmentAsync(request, imageRepository, targetId, accessValidator, apiKeyRepository, userRepository, serviceProvider.GetRequiredService<IRoleAssignmentRepository<Image>>(), mapper, cancellationToken),
"Organization" => CreateRoleAssignmentAsync(request, organizationRepository, targetId, accessValidator, apiKeyRepository, userRepository, serviceProvider.GetRequiredService<IRoleAssignmentRepository<Organization>>(), mapper, cancellationToken),
"Tournament" => CreateRoleAssignmentAsync(request, tournamentRepository, targetId, accessValidator, apiKeyRepository, userRepository, serviceProvider.GetRequiredService<IRoleAssignmentRepository<Tournament>>(), mapper, cancellationToken),
"Venue" => CreateRoleAssignmentAsync(request, venueRepository, targetId, accessValidator, apiKeyRepository, userRepository, serviceProvider.GetRequiredService<IRoleAssignmentRepository<Venue>>(), mapper, cancellationToken),
_ => null
};

return task is null
? Results.BadRequest("Invalid scope identifier provided.")
: await task.ConfigureAwait(false);
}

private static async Task<IResult> CreateRoleAssignmentAsync<T>(
CreateRoleAssignmentEndpointRequest request,
IRepositoryWithPublicId<T, long> repository,
PublicId targetId,
IAccessValidator accessValidator,
IApiKeyRepository apiKeyRepository,
IUserRepository userRepository,
IRoleAssignmentRepository<T> roleAssignmentRepository,
IMapper mapper,
CancellationToken cancellationToken)
where T : Entity<long>, IEntityWithRoleAssignments<T>
{
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<RoleAssignmentDto>(roleAssignment));
}

private static async Task<Principal?> 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<CreateRoleAssignmentEndpointRequest>
{
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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<IResult> 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<IResult> DeleteRoleAssignmentAsync<T>(
IRepositoryWithPublicId<T, long> repository,
PublicId targetId,
IAccessValidator accessValidator,
Guid roleAssignmentId,
CancellationToken cancellationToken)
where T : Entity<long>, IEntityWithRoleAssignments<T>
{
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();
}
}
Original file line number Diff line number Diff line change
@@ -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<IEnumerable<RoleAssignmentDto>>
{
protected override HttpMethod Method => HttpMethod.Get;

protected override string Route => "/api/role-assignments/{scopeId}";

protected override Delegate Handler => Handle;

private static async Task<IResult> 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<IResult> GetRoleAssignmentsAsync<T>(IRepositoryWithPublicId<T, long> repository, PublicId targetId, IAccessValidator accessValidator, IMapper mapper)
where T : Entity<long>, IEntityWithRoleAssignments<T>
{
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<RoleAssignmentDto>();

result.AddRange(mapper.MapCollection<RoleAssignmentDto>(entity.RoleAssignments));

if (entity is IEntityWithOrganization entityWithOrganization)
{
result.AddRange(mapper.MapCollection<RoleAssignmentDto>(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<RoleAssignmentDto>(tournament.Folder.RoleAssignments)
.Select(r => r with { IsInherited = true }));
}

return Results.Ok(result);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ private static async Task<IResult> 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)
{
Expand Down
Loading
Loading