| | | 1 | | using ArturRios.Data.Relational.Core.Interfaces; |
| | | 2 | | using ArturRios.Heimdall.Domain.Entities; |
| | | 3 | | using ArturRios.Heimdall.Query.Input; |
| | | 4 | | using ArturRios.Heimdall.Query.Output; |
| | | 5 | | using ArturRios.Heimdall.Shared.Messages; |
| | | 6 | | using ArturRios.Heimdall.Shared.Services; |
| | | 7 | | using ArturRios.Mediator.Query.Interfaces; |
| | | 8 | | using ArturRios.Output; |
| | | 9 | | using Microsoft.EntityFrameworkCore; |
| | | 10 | | |
| | | 11 | | namespace ArturRios.Heimdall.Query.Handlers; |
| | | 12 | | |
| | | 13 | | /// <summary> |
| | | 14 | | /// Handles <see cref="GetScopePermissionByIdQuery" /> (UC-32, FR-SP-04): retrieves a scope |
| | | 15 | | /// permission by its <c>PublicId</c> within the addressed scope, excluding logically deleted |
| | | 16 | | /// permissions unless explicitly requested (FR-SP-09), then applies the use case's visibility |
| | | 17 | | /// rule — a System Admin sees any permission, anyone else must own the scope. A miss is AF-32a |
| | | 18 | | /// (<c>ScopePermissionNotFound</c>); an actor who does not own the scope is AF-32e |
| | | 19 | | /// (<c>NotScopeOwner</c>). Both are returned as errors rather than thrown. The permission's |
| | | 20 | | /// internal scope key is needed for the ownership check, so the entity is loaded (not projected) |
| | | 21 | | /// before authorization — mirroring the UC-34 delete handler. |
| | | 22 | | /// </summary> |
| | 16 | 23 | | public class GetScopePermissionByIdQueryHandler( |
| | 16 | 24 | | IAsyncReadOnlyRepository<ScopePermission, long> permissionReader, |
| | 16 | 25 | | IScopeOwnershipChecker scopeOwnership) |
| | | 26 | | : IQueryHandlerAsync<GetScopePermissionByIdQuery, ScopePermissionOutput> |
| | | 27 | | { |
| | | 28 | | public async Task<DataOutput<ScopePermissionOutput?>> HandleAsync(GetScopePermissionByIdQuery query, CancellationTok |
| | | 29 | | { |
| | 16 | 30 | | var output = DataOutput<ScopePermissionOutput?>.New; |
| | | 31 | | |
| | | 32 | | // AF-32a: no such permission under this scope (or it is logically deleted and was not |
| | | 33 | | // explicitly requested). The route's scopeId qualifies the lookup, so a permission that |
| | | 34 | | // lives in another scope is not the resource this path addresses. Checked before |
| | | 35 | | // authorization, so AF-32a and AF-32e both stay observable — a GUID nobody holds cannot be |
| | | 36 | | // told apart from one the caller may not see. |
| | 16 | 37 | | var permission = await permissionReader.Query() |
| | 16 | 38 | | .Include(x => x.Scope) |
| | 16 | 39 | | .FirstOrDefaultAsync(x => x.PublicId == query.Id && |
| | 16 | 40 | | x.Scope.PublicId == query.ScopeId && |
| | 16 | 41 | | (query.IncludeDeleted || !x.IsDeleted)); |
| | | 42 | | |
| | 16 | 43 | | if (permission is null) |
| | | 44 | | { |
| | 7 | 45 | | return output.WithError(ScopePermissionMessages.ScopePermissionNotFound); |
| | | 46 | | } |
| | | 47 | | |
| | | 48 | | // AF-32e: a System Admin sees every permission; anyone else must own the scope. A scope |
| | | 49 | | // permission has no owner of its own, so owning the scope is the only authorization. |
| | 9 | 50 | | if (!await scopeOwnership.ActorMayManageScopeAsync( |
| | 9 | 51 | | query.ActingRole, query.ActingPersonId, permission.ScopeId)) |
| | | 52 | | { |
| | 2 | 53 | | return output.WithError(ScopePermissionMessages.NotScopeOwner); |
| | | 54 | | } |
| | | 55 | | |
| | 7 | 56 | | return output |
| | 7 | 57 | | .WithData(new ScopePermissionOutput |
| | 7 | 58 | | { |
| | 7 | 59 | | Id = permission.PublicId, |
| | 7 | 60 | | Name = permission.Name, |
| | 7 | 61 | | Description = permission.Description, |
| | 7 | 62 | | IncludeAsJwtClaim = permission.IncludeAsJwtClaim, |
| | 7 | 63 | | ScopeId = permission.Scope.PublicId, |
| | 7 | 64 | | IsDeleted = permission.IsDeleted, |
| | 7 | 65 | | CreatedAt = permission.CreatedAt, |
| | 7 | 66 | | UpdatedAt = permission.UpdatedAt |
| | 7 | 67 | | }) |
| | 7 | 68 | | .WithMessage(ScopePermissionMessages.ScopePermissionRetrievedSuccessfully); |
| | 16 | 69 | | } |
| | | 70 | | } |