| | | 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.Shared.Messages; |
| | | 6 | | using ArturRios.Heimdall.Shared.Services; |
| | | 7 | | using ArturRios.Mediator.Command.Interfaces; |
| | | 8 | | using ArturRios.Output; |
| | | 9 | | using FluentValidation; |
| | | 10 | | using Microsoft.EntityFrameworkCore; |
| | | 11 | | using Scope = ArturRios.Heimdall.Domain.Entities.Scope; |
| | | 12 | | |
| | | 13 | | namespace ArturRios.Heimdall.Command.Handlers; |
| | | 14 | | |
| | | 15 | | /// <summary> |
| | | 16 | | /// Handles <see cref="CreateScopePermissionCommand" /> (UC-31, FR-SP-01/02): validates input |
| | | 17 | | /// (AF-31d), verifies the target scope exists and is active (AF-31a), enforces the acting role's |
| | | 18 | | /// rule — a Scope Admin must own the scope (AF-31e), a System Admin bypasses it — then creates the |
| | | 19 | | /// scope-permission record. A scope permission is a scope-child resource with no separate owner, |
| | | 20 | | /// so the scope-ownership check is the only authorization. |
| | | 21 | | /// </summary> |
| | 33 | 22 | | public class CreateScopePermissionCommandHandler( |
| | 33 | 23 | | IValidator<CreateScopePermissionCommand> validator, |
| | 33 | 24 | | IAsyncReadOnlyRepository<Scope, long> scopeReader, |
| | 33 | 25 | | IAsyncRepository<ScopePermission, long> permissionWriter, |
| | 33 | 26 | | IScopeOwnershipChecker scopeOwnership) |
| | | 27 | | : ICommandHandlerAsync<CreateScopePermissionCommand, CreateScopePermissionCommandOutput> |
| | | 28 | | { |
| | | 29 | | public async Task<DataOutput<CreateScopePermissionCommandOutput?>> HandleAsync( |
| | | 30 | | CreateScopePermissionCommand command, CancellationToken cancellationToken = default) |
| | | 31 | | { |
| | 33 | 32 | | var output = DataOutput<CreateScopePermissionCommandOutput?>.New; |
| | | 33 | | |
| | | 34 | | // AF-31d: validate input shape. |
| | 33 | 35 | | var validation = await validator.ValidateAsync(command); |
| | | 36 | | |
| | 33 | 37 | | if (!validation.IsValid) |
| | | 38 | | { |
| | 6 | 39 | | return output.WithErrors(validation.Errors.Select(failure => failure.ErrorMessage)); |
| | | 40 | | } |
| | | 41 | | |
| | | 42 | | // AF-31a: the target scope must exist and not be logically deleted. |
| | 30 | 43 | | var scope = await scopeReader.Query() |
| | 30 | 44 | | .FirstOrDefaultAsync(x => x.PublicId == command.ScopeId && !x.IsDeleted); |
| | | 45 | | |
| | 30 | 46 | | if (scope is null) |
| | | 47 | | { |
| | 4 | 48 | | return output.WithError(ScopePermissionMessages.ScopeNotFound); |
| | | 49 | | } |
| | | 50 | | |
| | | 51 | | // AF-31e: a Scope Admin actor may only act on a scope they own; a System Admin bypasses the |
| | | 52 | | // check inside the checker. |
| | 26 | 53 | | if (!await scopeOwnership.ActorMayManageScopeAsync( |
| | 26 | 54 | | command.ActingRole, command.ActingPersonId, scope.Id)) |
| | | 55 | | { |
| | 2 | 56 | | return output.WithError(ScopePermissionMessages.NotScopeOwner); |
| | | 57 | | } |
| | | 58 | | |
| | | 59 | | // FR-SP-01/02: create the permission in the scope, active. |
| | 24 | 60 | | var permission = new ScopePermission |
| | 24 | 61 | | { |
| | 24 | 62 | | Name = command.Name, |
| | 24 | 63 | | Description = command.Description, |
| | 24 | 64 | | IncludeAsJwtClaim = command.IncludeAsJwtClaim, |
| | 24 | 65 | | IsDeleted = false, |
| | 24 | 66 | | ScopeId = scope.Id |
| | 24 | 67 | | }; |
| | | 68 | | |
| | 24 | 69 | | var creation = await permissionWriter.CreateAsync(permission); |
| | | 70 | | |
| | 24 | 71 | | if (!creation.Success) |
| | | 72 | | { |
| | 0 | 73 | | return output.WithErrors(creation.Errors); |
| | | 74 | | } |
| | | 75 | | |
| | 24 | 76 | | return output |
| | 24 | 77 | | .WithData(new CreateScopePermissionCommandOutput |
| | 24 | 78 | | { |
| | 24 | 79 | | Id = permission.PublicId, |
| | 24 | 80 | | Name = permission.Name, |
| | 24 | 81 | | Description = permission.Description, |
| | 24 | 82 | | IncludeAsJwtClaim = permission.IncludeAsJwtClaim, |
| | 24 | 83 | | ScopeId = scope.PublicId, |
| | 24 | 84 | | CreatedAt = permission.CreatedAt |
| | 24 | 85 | | }) |
| | 24 | 86 | | .WithMessage(ScopePermissionMessages.ScopePermissionCreatedSuccessfully); |
| | 33 | 87 | | } |
| | | 88 | | } |