| | | 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="GetGoogleUserByIdQuery" /> (UC-27, FR-GO-14): retrieves a Google User by its |
| | | 15 | | /// <c>PublicId</c> within the addressed scope, excluding logically deleted records unless |
| | | 16 | | /// explicitly requested (FR-GO-17), then applies the use case's visibility rule. A miss is AF-27a |
| | | 17 | | /// (<c>GoogleUserNotFound</c>); a record the caller may not see is AF-27b |
| | | 18 | | /// (<c>NotAuthorizedToViewGoogleUser</c>). Both are returned as errors rather than thrown. |
| | | 19 | | /// </summary> |
| | 19 | 20 | | public class GetGoogleUserByIdQueryHandler( |
| | 19 | 21 | | IAsyncReadOnlyRepository<GoogleUser, long> googleUserReader, |
| | 19 | 22 | | IScopeOwnershipChecker scopeOwnership) |
| | | 23 | | : IQueryHandlerAsync<GetGoogleUserByIdQuery, GoogleUserOutput> |
| | | 24 | | { |
| | | 25 | | /// <summary> |
| | | 26 | | /// The Google User's payload plus the internal scope id the ownership rule needs. Internal ids |
| | | 27 | | /// never reach the caller — only <see cref="Output" /> is returned (NFR-15). |
| | | 28 | | /// </summary> |
| | | 29 | | private sealed class GoogleUserProjection |
| | | 30 | | { |
| | 22 | 31 | | public long ScopeInternalId { get; init; } |
| | | 32 | | |
| | 32 | 33 | | public GoogleUserOutput Output { get; init; } = null!; |
| | | 34 | | } |
| | | 35 | | |
| | | 36 | | public async Task<DataOutput<GoogleUserOutput?>> HandleAsync(GetGoogleUserByIdQuery query, CancellationToken cancell |
| | | 37 | | { |
| | 19 | 38 | | var output = DataOutput<GoogleUserOutput?>.New; |
| | | 39 | | |
| | | 40 | | // The route's scopeId qualifies the lookup: a Google User that exists in another scope is not |
| | | 41 | | // the resource this path addresses, so it falls out here rather than at the rule below. The |
| | | 42 | | // same arrangement GetApplicationByIdQueryHandler uses. |
| | 19 | 43 | | var googleUser = await googleUserReader.Query() |
| | 19 | 44 | | .Where(x => x.PublicId == query.Id && x.Scope.PublicId == query.ScopeId && |
| | 19 | 45 | | (query.IncludeDeleted || !x.IsDeleted)) |
| | 19 | 46 | | .Select(x => new GoogleUserProjection |
| | 19 | 47 | | { |
| | 19 | 48 | | ScopeInternalId = x.ScopeId, |
| | 19 | 49 | | Output = new GoogleUserOutput |
| | 19 | 50 | | { |
| | 19 | 51 | | Id = x.PublicId, |
| | 19 | 52 | | GoogleId = x.GoogleId, |
| | 19 | 53 | | Name = x.Name, |
| | 19 | 54 | | Email = x.Email, |
| | 19 | 55 | | EmailVerified = x.EmailVerified, |
| | 19 | 56 | | ProfilePictureUrl = x.ProfilePictureUrl, |
| | 19 | 57 | | IsDeleted = x.IsDeleted, |
| | 19 | 58 | | ScopeId = x.Scope.PublicId, |
| | 19 | 59 | | CreatedAt = x.CreatedAt, |
| | 19 | 60 | | UpdatedAt = x.UpdatedAt |
| | 19 | 61 | | } |
| | 19 | 62 | | }) |
| | 19 | 63 | | .FirstOrDefaultAsync(); |
| | | 64 | | |
| | | 65 | | // AF-27a: no such Google User under this scope (or it is logically deleted and was not |
| | | 66 | | // explicitly requested). Checked before authorization, so both alternative flows stay |
| | | 67 | | // observable — a GUID nobody holds cannot be told apart from one the caller may not see. |
| | 19 | 68 | | if (googleUser is null) |
| | | 69 | | { |
| | 7 | 70 | | return output.WithError(GoogleUserMessages.GoogleUserNotFound); |
| | | 71 | | } |
| | | 72 | | |
| | | 73 | | // AF-27b (UC-27 step 2). The use case grants three actors: a Google User reading themselves, |
| | | 74 | | // a System Admin reading anyone, and a Scope Admin reading the scopes they own. The last two |
| | | 75 | | // are exactly what IScopeOwnershipChecker decides, so the rule is the self-read plus that |
| | | 76 | | // check — which also inherits its guard that a logically deleted actor owns nothing. A |
| | | 77 | | // password User matches neither half, which is correct: the authorization matrix grants a |
| | | 78 | | // User this read only as self, and a person is never a Google User's self. |
| | 12 | 79 | | if (query.ActingPersonId != googleUser.Output.Id && |
| | 12 | 80 | | !await scopeOwnership.ActorMayManageScopeAsync( |
| | 12 | 81 | | query.ActingRole, query.ActingPersonId, googleUser.ScopeInternalId)) |
| | | 82 | | { |
| | 4 | 83 | | return output.WithError(GoogleUserMessages.NotAuthorizedToViewGoogleUser); |
| | | 84 | | } |
| | | 85 | | |
| | | 86 | | // UC-27 step 4. |
| | 8 | 87 | | return output |
| | 8 | 88 | | .WithData(googleUser.Output) |
| | 8 | 89 | | .WithMessage(GoogleUserMessages.GoogleUserRetrievedSuccessfully); |
| | 19 | 90 | | } |
| | | 91 | | } |