| | | 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 | | |
| | | 12 | | namespace ArturRios.Heimdall.Command.Handlers; |
| | | 13 | | |
| | | 14 | | /// <summary> |
| | | 15 | | /// Handles <see cref="UpdateScopePermissionCommand" /> (UC-33, FR-SP-06): validates input, |
| | | 16 | | /// loads the permission inside the addressed scope (AF-33a), enforces the acting role's rule — a |
| | | 17 | | /// System Admin may update any permission, anyone else only one whose scope they own (AF-33e) — |
| | | 18 | | /// then applies the changes and stamps <c>UpdatedAt</c>. All failures are returned as errors on |
| | | 19 | | /// the output rather than thrown. |
| | | 20 | | /// </summary> |
| | 20 | 21 | | public class UpdateScopePermissionCommandHandler( |
| | 20 | 22 | | IValidator<UpdateScopePermissionCommand> validator, |
| | 20 | 23 | | IAsyncReadOnlyRepository<ScopePermission, long> permissionReader, |
| | 20 | 24 | | IAsyncRepository<ScopePermission, long> permissionWriter, |
| | 20 | 25 | | IScopeOwnershipChecker scopeOwnership) |
| | | 26 | | : ICommandHandlerAsync<UpdateScopePermissionCommand, UpdateScopePermissionCommandOutput> |
| | | 27 | | { |
| | | 28 | | public async Task<DataOutput<UpdateScopePermissionCommandOutput?>> HandleAsync( |
| | | 29 | | UpdateScopePermissionCommand command, CancellationToken cancellationToken = default) |
| | | 30 | | { |
| | 20 | 31 | | var output = DataOutput<UpdateScopePermissionCommandOutput?>.New; |
| | | 32 | | |
| | | 33 | | // UC-33 step 2: validate input shape. UC-33 defines no alternative flow for this, so the |
| | | 34 | | // validator's UC-31 messages carry their existing 400. |
| | 20 | 35 | | var validation = await validator.ValidateAsync(command); |
| | | 36 | | |
| | 20 | 37 | | if (!validation.IsValid) |
| | | 38 | | { |
| | 6 | 39 | | return output.WithErrors(validation.Errors.Select(failure => failure.ErrorMessage)); |
| | | 40 | | } |
| | | 41 | | |
| | | 42 | | // AF-33a: the permission must exist inside the addressed scope and not be logically deleted. |
| | | 43 | | // The route's scopeId qualifies the lookup, as UC-32's by-id read does: a permission that |
| | | 44 | | // lives in another scope is not the resource this path addresses. Checked before |
| | | 45 | | // authorization so AF-33a and AF-33e both stay observable. |
| | 17 | 46 | | var permission = await permissionReader.Query() |
| | 17 | 47 | | .Include(x => x.Scope) |
| | 17 | 48 | | .FirstOrDefaultAsync(x => |
| | 17 | 49 | | x.PublicId == command.Id && x.Scope.PublicId == command.ScopeId && !x.IsDeleted); |
| | | 50 | | |
| | 17 | 51 | | if (permission is null) |
| | | 52 | | { |
| | 7 | 53 | | return output.WithError(ScopePermissionMessages.ScopePermissionNotFound); |
| | | 54 | | } |
| | | 55 | | |
| | | 56 | | // AF-33e: a System Admin may update any scope's permission; anyone else must own the scope. |
| | | 57 | | // A scope permission carries no separate owner, so the scope-ownership check is the only |
| | | 58 | | // authorization. Runs before any mutation so AF-33e stays observable on otherwise-no-op |
| | | 59 | | // updates. |
| | 10 | 60 | | if (!await scopeOwnership.ActorMayManageScopeAsync( |
| | 10 | 61 | | command.ActingRole, command.ActingPersonId, permission.ScopeId)) |
| | | 62 | | { |
| | 4 | 63 | | return output.WithError(ScopePermissionMessages.NotScopeOwner); |
| | | 64 | | } |
| | | 65 | | |
| | | 66 | | // UC-33 step 5: apply the updates and stamp UpdatedAt (no DB trigger maintains it). |
| | 6 | 67 | | permission.Name = command.Name; |
| | 6 | 68 | | permission.Description = command.Description; |
| | 6 | 69 | | permission.IncludeAsJwtClaim = command.IncludeAsJwtClaim; |
| | 6 | 70 | | permission.UpdatedAt = DateTime.UtcNow; |
| | | 71 | | |
| | 6 | 72 | | var update = await permissionWriter.UpdateAsync(permission); |
| | | 73 | | |
| | 6 | 74 | | if (!update.Success) |
| | | 75 | | { |
| | 0 | 76 | | return output.WithErrors(update.Errors); |
| | | 77 | | } |
| | | 78 | | |
| | | 79 | | // UC-33 step 6: return the updated permission. |
| | 6 | 80 | | return output |
| | 6 | 81 | | .WithData(new UpdateScopePermissionCommandOutput |
| | 6 | 82 | | { |
| | 6 | 83 | | Id = permission.PublicId, |
| | 6 | 84 | | Name = permission.Name, |
| | 6 | 85 | | Description = permission.Description, |
| | 6 | 86 | | IncludeAsJwtClaim = permission.IncludeAsJwtClaim, |
| | 6 | 87 | | ScopeId = permission.Scope.PublicId, |
| | 6 | 88 | | CreatedAt = permission.CreatedAt, |
| | 6 | 89 | | UpdatedAt = permission.UpdatedAt |
| | 6 | 90 | | }) |
| | 6 | 91 | | .WithMessage(ScopePermissionMessages.ScopePermissionUpdatedSuccessfully); |
| | 20 | 92 | | } |
| | | 93 | | } |