| | | 1 | | using ArturRios.Data.Relational.Core.Interfaces; |
| | | 2 | | using ArturRios.Heimdall.Domain.Enums; |
| | | 3 | | using ArturRios.Heimdall.Query.Input; |
| | | 4 | | using ArturRios.Heimdall.Query.Output; |
| | | 5 | | using ArturRios.Heimdall.Shared.Messages; |
| | | 6 | | using ArturRios.Mediator.Query.Interfaces; |
| | | 7 | | using ArturRios.Output; |
| | | 8 | | using Microsoft.EntityFrameworkCore; |
| | | 9 | | using Application = ArturRios.Heimdall.Domain.Entities.Application; |
| | | 10 | | |
| | | 11 | | namespace ArturRios.Heimdall.Query.Handlers; |
| | | 12 | | |
| | | 13 | | /// <summary> |
| | | 14 | | /// Handles <see cref="GetApplicationByIdQuery" /> (UC-17, FR-AP-04): retrieves an application by |
| | | 15 | | /// its <c>PublicId</c> within the addressed scope, excluding logically deleted applications |
| | | 16 | | /// unless explicitly requested (FR-AP-09), then applies the use case's visibility rule — a System |
| | | 17 | | /// Admin sees any application, a Scope Admin only the ones they own. A miss is AF-17a |
| | | 18 | | /// (<c>ApplicationNotFound</c>); an application the caller may not see is AF-17b |
| | | 19 | | /// (<c>NotAuthorizedToViewApplication</c>). Both are returned as errors rather than thrown. |
| | | 20 | | /// </summary> |
| | 18 | 21 | | public class GetApplicationByIdQueryHandler(IAsyncReadOnlyRepository<Application, long> applicationReader) |
| | | 22 | | : IQueryHandlerAsync<GetApplicationByIdQuery, ApplicationOutput> |
| | | 23 | | { |
| | | 24 | | public async Task<DataOutput<ApplicationOutput?>> HandleAsync(GetApplicationByIdQuery query, CancellationToken cance |
| | | 25 | | { |
| | 18 | 26 | | var output = DataOutput<ApplicationOutput?>.New; |
| | | 27 | | |
| | | 28 | | // The route's scopeId qualifies the lookup: an application that exists in another scope is |
| | | 29 | | // not the resource this path addresses, so it falls out here rather than at the rule below. |
| | 18 | 30 | | var found = await applicationReader.Query() |
| | 18 | 31 | | .Where(x => x.PublicId == query.Id && x.Scope.PublicId == query.ScopeId && |
| | 18 | 32 | | (query.IncludeDeleted || !x.IsDeleted)) |
| | 18 | 33 | | .Select(x => new |
| | 18 | 34 | | { |
| | 18 | 35 | | Output = new ApplicationOutput |
| | 18 | 36 | | { |
| | 18 | 37 | | Id = x.PublicId, |
| | 18 | 38 | | Name = x.Name, |
| | 18 | 39 | | ScopeId = x.Scope.PublicId, |
| | 18 | 40 | | OwnerId = x.Owner.PublicId, |
| | 18 | 41 | | IsDeleted = x.IsDeleted, |
| | 18 | 42 | | CreatedAt = x.CreatedAt, |
| | 18 | 43 | | UpdatedAt = x.UpdatedAt |
| | 18 | 44 | | }, |
| | 18 | 45 | | OwnerOwnsScope = x.Owner.ScopeOwnerships.Any(ownership => ownership.ScopeId == x.ScopeId) |
| | 18 | 46 | | }) |
| | 18 | 47 | | .FirstOrDefaultAsync(); |
| | | 48 | | |
| | 18 | 49 | | var application = found?.Output; |
| | | 50 | | |
| | | 51 | | // AF-17a: no such application under this scope (or it is logically deleted and was not |
| | | 52 | | // explicitly requested). Checked before authorization, so both alternative flows stay |
| | | 53 | | // observable — a GUID nobody holds cannot be told apart from one the caller may not see. |
| | 18 | 54 | | if (application is null) |
| | | 55 | | { |
| | 7 | 56 | | return output.WithError(ApplicationMessages.ApplicationNotFound); |
| | | 57 | | } |
| | | 58 | | |
| | | 59 | | // AF-17b (UC-17 step 2): a System Admin sees every application; anyone else must own it. |
| | | 60 | | // Owning the *scope* is not by itself grounds to read another owner's application, so the |
| | | 61 | | // rule compares the owner rather than consulting IScopeOwnershipChecker. |
| | | 62 | | // |
| | | 63 | | // Owning the application is not enough by itself either: FR-AP-03 makes an owner someone who |
| | | 64 | | // owns the application's scope, and UC-22 can take that away while leaving them named as the |
| | | 65 | | // owner. Without this a Scope Admin removed from a scope kept reading, renaming and deleting |
| | | 66 | | // its applications — everything but listing them, which does check the scope. |
| | 11 | 67 | | if (query.ActingRole != (int)Roles.SystemAdmin && |
| | 11 | 68 | | (application.OwnerId != query.ActingPersonId || !found!.OwnerOwnsScope)) |
| | | 69 | | { |
| | 4 | 70 | | return output.WithError(ApplicationMessages.NotAuthorizedToViewApplication); |
| | | 71 | | } |
| | | 72 | | |
| | 7 | 73 | | return output |
| | 7 | 74 | | .WithData(application) |
| | 7 | 75 | | .WithMessage(ApplicationMessages.ApplicationRetrievedSuccessfully); |
| | 18 | 76 | | } |
| | | 77 | | } |