| | | 1 | | using ArturRios.Data.Relational.Core.Interfaces; |
| | | 2 | | using ArturRios.Heimdall.Domain.Entities; |
| | | 3 | | using ArturRios.Heimdall.Domain.Enums; |
| | | 4 | | using ArturRios.Heimdall.Query.Input; |
| | | 5 | | using ArturRios.Heimdall.Query.Output; |
| | | 6 | | using ArturRios.Heimdall.Shared.Messages; |
| | | 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="GetScopeByIdQuery" /> (UC-02, FR-SC-02): retrieves a scope by its |
| | | 15 | | /// <c>PublicId</c>, excluding logically deleted scopes unless explicitly requested (FR-SC-07), |
| | | 16 | | /// then applies the use case's per-actor visibility rule. A missing scope is AF-02a |
| | | 17 | | /// (<c>ScopeNotFound</c>); a scope the caller may not see is AF-02b |
| | | 18 | | /// (<c>NotAuthorizedToViewScope</c>). Both are returned as errors rather than thrown. |
| | | 19 | | /// </summary> |
| | 18 | 20 | | public class GetScopeByIdQueryHandler(IAsyncReadOnlyRepository<Scope, long> scopeReader) |
| | | 21 | | : IQueryHandlerAsync<GetScopeByIdQuery, ScopeOutput> |
| | | 22 | | { |
| | | 23 | | /// <summary> |
| | | 24 | | /// The scope plus the one membership fact the visibility rule needs and |
| | | 25 | | /// <see cref="ScopeOutput" /> does not carry. Ownership needs no extra field: the output |
| | | 26 | | /// already lists the owners' <c>PublicId</c>s. |
| | | 27 | | /// </summary> |
| | | 28 | | private sealed class ScopeProjection |
| | | 29 | | { |
| | 19 | 30 | | public bool ActorBelongsToScope { get; init; } |
| | | 31 | | |
| | 26 | 32 | | public ScopeOutput Output { get; init; } = null!; |
| | | 33 | | } |
| | | 34 | | |
| | | 35 | | public async Task<DataOutput<ScopeOutput?>> HandleAsync(GetScopeByIdQuery query, CancellationToken cancellationToken |
| | | 36 | | { |
| | 18 | 37 | | var output = DataOutput<ScopeOutput?>.New; |
| | | 38 | | |
| | 18 | 39 | | var scope = await scopeReader.Query() |
| | 18 | 40 | | .Where(x => x.PublicId == query.Id && (query.IncludeDeleted || !x.IsDeleted)) |
| | 18 | 41 | | .Select(x => new ScopeProjection |
| | 18 | 42 | | { |
| | 18 | 43 | | ActorBelongsToScope = x.Users.Any(user => user.Person.PublicId == query.ActingPersonId), |
| | 18 | 44 | | Output = new ScopeOutput |
| | 18 | 45 | | { |
| | 18 | 46 | | Id = x.PublicId, |
| | 18 | 47 | | Name = x.Name, |
| | 18 | 48 | | Description = x.Description, |
| | 18 | 49 | | GoogleSignInEnabled = x.GoogleSignInEnabled, |
| | 18 | 50 | | SelfRegistrationEnabled = x.SelfRegistrationEnabled, |
| | 18 | 51 | | DefaultLegalBasis = x.DefaultLegalBasis, |
| | 18 | 52 | | PrivacyNoticeUri = x.PrivacyNoticeUri, |
| | 18 | 53 | | EmailVerificationUrl = x.EmailVerificationUrl, |
| | 18 | 54 | | PasswordResetUrl = x.PasswordResetUrl, |
| | 18 | 55 | | IsDeleted = x.IsDeleted, |
| | 18 | 56 | | OwnerIds = x.Owners.Select(owner => owner.Person.PublicId).ToList(), |
| | 18 | 57 | | CreatedAt = x.CreatedAt, |
| | 18 | 58 | | UpdatedAt = x.UpdatedAt |
| | 18 | 59 | | } |
| | 18 | 60 | | }) |
| | 18 | 61 | | .FirstOrDefaultAsync(); |
| | | 62 | | |
| | | 63 | | // AF-02a: no such scope (or it is logically deleted and was not explicitly requested). |
| | 18 | 64 | | if (scope is null) |
| | | 65 | | { |
| | 4 | 66 | | return output.WithError(ScopeMessages.ScopeNotFound); |
| | | 67 | | } |
| | | 68 | | |
| | | 69 | | // AF-02b: the caller is not allowed to see this scope. |
| | 14 | 70 | | if (!MayView(query, scope)) |
| | | 71 | | { |
| | 6 | 72 | | return output.WithError(ScopeMessages.NotAuthorizedToViewScope); |
| | | 73 | | } |
| | | 74 | | |
| | 8 | 75 | | return output |
| | 8 | 76 | | .WithData(scope.Output) |
| | 8 | 77 | | .WithMessage(ScopeMessages.ScopeRetrievedSuccessfully); |
| | 18 | 78 | | } |
| | | 79 | | |
| | | 80 | | /// <summary> |
| | | 81 | | /// UC-02 step 2: a System Admin sees every scope, a Scope Admin only the scopes they own, and |
| | | 82 | | /// a User only the scope they belong to. Any other role is denied, so a role added later |
| | | 83 | | /// cannot inherit read access to every scope by default. |
| | | 84 | | /// </summary> |
| | 14 | 85 | | private static bool MayView(GetScopeByIdQuery query, ScopeProjection scope) => query.ActingRole switch |
| | 14 | 86 | | { |
| | 4 | 87 | | (int)Roles.SystemAdmin => true, |
| | 4 | 88 | | (int)Roles.ScopeAdmin => scope.Output.OwnerIds.Contains(query.ActingPersonId), |
| | 5 | 89 | | (int)Roles.User => scope.ActorBelongsToScope, |
| | 1 | 90 | | _ => false |
| | 14 | 91 | | }; |
| | | 92 | | } |