| | | 1 | | using ArturRios.Data.Relational.Core.Interfaces; |
| | | 2 | | using ArturRios.Heimdall.Command.Input; |
| | | 3 | | using ArturRios.Heimdall.Command.Output; |
| | | 4 | | using ArturRios.Heimdall.Domain.Entities; |
| | | 5 | | using ArturRios.Heimdall.Domain.Enums; |
| | | 6 | | using ArturRios.Heimdall.Shared.Messages; |
| | | 7 | | using ArturRios.Heimdall.Shared.Services; |
| | | 8 | | using ArturRios.Mediator.Command.Interfaces; |
| | | 9 | | using ArturRios.Output; |
| | | 10 | | using FluentValidation; |
| | | 11 | | using Microsoft.EntityFrameworkCore; |
| | | 12 | | using Application = ArturRios.Heimdall.Domain.Entities.Application; |
| | | 13 | | |
| | | 14 | | namespace ArturRios.Heimdall.Command.Handlers; |
| | | 15 | | |
| | | 16 | | /// <summary> |
| | | 17 | | /// Handles <see cref="CreateApplicationCommand" /> (UC-16, FR-AP-01/02/03): validates input |
| | | 18 | | /// (AF-16d), verifies the target scope exists and is active (AF-16a), enforces the acting role's |
| | | 19 | | /// rule — a Scope Admin must own the scope (AF-16e) and may only name themself as owner |
| | | 20 | | /// (AF-16c), a System Admin bypasses both — verifies the owner is a non-deleted |
| | | 21 | | /// <c>ScopeAdmin</c> who owns the scope (AF-16b), then creates the application record. |
| | | 22 | | /// </summary> |
| | 30 | 23 | | public class CreateApplicationCommandHandler( |
| | 30 | 24 | | IValidator<CreateApplicationCommand> validator, |
| | 30 | 25 | | IAsyncReadOnlyRepository<Scope, long> scopeReader, |
| | 30 | 26 | | IAsyncReadOnlyRepository<Person, long> personReader, |
| | 30 | 27 | | IAsyncRepository<Application, long> applicationWriter, |
| | 30 | 28 | | IScopeOwnershipChecker scopeOwnership) |
| | | 29 | | : ICommandHandlerAsync<CreateApplicationCommand, CreateApplicationCommandOutput> |
| | | 30 | | { |
| | | 31 | | public async Task<DataOutput<CreateApplicationCommandOutput?>> HandleAsync(CreateApplicationCommand command, Cancell |
| | | 32 | | { |
| | 30 | 33 | | var output = DataOutput<CreateApplicationCommandOutput?>.New; |
| | | 34 | | |
| | | 35 | | // AF-16d: validate input shape. |
| | 30 | 36 | | var validation = await validator.ValidateAsync(command); |
| | | 37 | | |
| | 30 | 38 | | if (!validation.IsValid) |
| | | 39 | | { |
| | 4 | 40 | | return output.WithErrors(validation.Errors.Select(failure => failure.ErrorMessage)); |
| | | 41 | | } |
| | | 42 | | |
| | | 43 | | // AF-16a: the target scope must exist and not be logically deleted. |
| | 28 | 44 | | var scope = await scopeReader.Query() |
| | 28 | 45 | | .FirstOrDefaultAsync(x => x.PublicId == command.ScopeId && !x.IsDeleted); |
| | | 46 | | |
| | 28 | 47 | | if (scope is null) |
| | | 48 | | { |
| | 5 | 49 | | return output.WithError(ApplicationMessages.ScopeNotFound); |
| | | 50 | | } |
| | | 51 | | |
| | | 52 | | // AF-16e: a Scope Admin actor may only act on a scope they own; a System Admin bypasses the |
| | | 53 | | // check inside the checker. Asked before who was named, so an actor acting where they have no |
| | | 54 | | // standing is told that, rather than being told about the owner. |
| | 23 | 55 | | if (!await scopeOwnership.ActorMayManageScopeAsync( |
| | 23 | 56 | | command.ActingRole, command.ActingPersonId, scope.Id)) |
| | | 57 | | { |
| | 2 | 58 | | return output.WithError(ApplicationMessages.NotScopeOwner); |
| | | 59 | | } |
| | | 60 | | |
| | | 61 | | // AF-16c: a Scope Admin may only create applications they own. Decided from the command |
| | | 62 | | // alone, before the owner is read, so the refusal cannot reveal whether the named person |
| | | 63 | | // exists. A System Admin may name any owner FR-AP-03 accepts. |
| | 21 | 64 | | if (command.ActingRole != (int)Roles.SystemAdmin && command.OwnerId != command.ActingPersonId) |
| | | 65 | | { |
| | 2 | 66 | | return output.WithError(ApplicationMessages.CannotSetAnotherOwner); |
| | | 67 | | } |
| | | 68 | | |
| | | 69 | | // AF-16b / FR-AP-03: the owner must be an existing, non-logically-deleted administrator — a |
| | | 70 | | // ScopeAdmin or a SystemAdmin — who owns the scope. The role is asserted alongside the |
| | | 71 | | // SCOPE_OWNER row: SRD §4.5 already restricts that table to administrators, so this states |
| | | 72 | | // the requirement rather than adding to it. |
| | 19 | 73 | | var owner = await personReader.Query().FirstOrDefaultAsync(person => |
| | 19 | 74 | | person.PublicId == command.OwnerId && !person.IsDeleted && |
| | 19 | 75 | | (person.RoleId == (long)Roles.ScopeAdmin || person.RoleId == (long)Roles.SystemAdmin) && |
| | 19 | 76 | | person.ScopeOwnerships.Any(ownership => ownership.ScopeId == scope.Id)); |
| | | 77 | | |
| | 19 | 78 | | if (owner is null) |
| | | 79 | | { |
| | 9 | 80 | | return output.WithError(ApplicationMessages.OwnerNotValidForScope); |
| | | 81 | | } |
| | | 82 | | |
| | | 83 | | // FR-AP-01/02: create the application in the scope, active. |
| | 10 | 84 | | var application = new Application |
| | 10 | 85 | | { |
| | 10 | 86 | | Name = command.Name, |
| | 10 | 87 | | IsDeleted = false, |
| | 10 | 88 | | ScopeId = scope.Id, |
| | 10 | 89 | | OwnerId = owner.Id |
| | 10 | 90 | | }; |
| | | 91 | | |
| | 10 | 92 | | var creation = await applicationWriter.CreateAsync(application); |
| | | 93 | | |
| | 10 | 94 | | if (!creation.Success) |
| | | 95 | | { |
| | 0 | 96 | | return output.WithErrors(creation.Errors); |
| | | 97 | | } |
| | | 98 | | |
| | 10 | 99 | | return output |
| | 10 | 100 | | .WithData(new CreateApplicationCommandOutput |
| | 10 | 101 | | { |
| | 10 | 102 | | Id = application.PublicId, |
| | 10 | 103 | | Name = application.Name, |
| | 10 | 104 | | ScopeId = scope.PublicId, |
| | 10 | 105 | | OwnerId = owner.PublicId, |
| | 10 | 106 | | CreatedAt = application.CreatedAt |
| | 10 | 107 | | }) |
| | 10 | 108 | | .WithMessage(ApplicationMessages.ApplicationCreatedSuccessfully); |
| | 30 | 109 | | } |
| | | 110 | | } |