| | | 1 | | using ArturRios.Heimdall.Domain.Entities; |
| | | 2 | | using ArturRios.Heimdall.Domain.Enums; |
| | | 3 | | |
| | | 4 | | namespace ArturRios.Heimdall.Command.Services; |
| | | 5 | | |
| | | 6 | | /// <summary> |
| | | 7 | | /// Builds the <see cref="AuthTokenSubject" /> UC-11 step 6 and UC-38 step 5 both need before |
| | | 8 | | /// issuing the full authentication token — a <c>User</c> claims the scope they belong to, a |
| | | 9 | | /// <c>ScopeAdmin</c> the live scopes they own, a <c>SystemAdmin</c> neither — and forwards to |
| | | 10 | | /// <see cref="IAuthTokenIssuer" />. Shared so the scope-eligibility rules (AF-11d/AF-11e) and the |
| | | 11 | | /// token-issuing call live in exactly one place, whichever use case is finishing the login: |
| | | 12 | | /// <c>LoginCommandHandler</c> directly, or <c>VerifyTwoFactorAuthCommandHandler</c> after a |
| | | 13 | | /// second factor checks out. |
| | | 14 | | /// </summary> |
| | 278 | 15 | | public class PersonAuthTokenService(IAuthTokenIssuer tokenIssuer) |
| | | 16 | | { |
| | | 17 | | /// <summary> |
| | | 18 | | /// AF-11d/AF-11e: a <c>User</c> whose scope is logically deleted, or a <c>ScopeAdmin</c> with |
| | | 19 | | /// no live owned scope, is not eligible for a token — <paramref name="subject" /> is |
| | | 20 | | /// <c>null</c> and this returns <see langword="false" /> in that case. Requires |
| | | 21 | | /// <paramref name="person" />'s <c>ScopeMembership.Scope</c> and <c>ScopeOwnerships.Scope</c> |
| | | 22 | | /// navigations to already be loaded. |
| | | 23 | | /// </summary> |
| | | 24 | | public bool TryBuildSubject(Person person, out AuthTokenSubject? subject) |
| | | 25 | | { |
| | 122 | 26 | | var liveOwnedScopeIds = person.ScopeOwnerships |
| | 14 | 27 | | .Where(ownership => !ownership.Scope.IsDeleted) |
| | 8 | 28 | | .Select(ownership => ownership.Scope.PublicId) |
| | 122 | 29 | | .ToList(); |
| | | 30 | | |
| | 122 | 31 | | if (person.RoleId == (long)Roles.User && person.ScopeMembership!.Scope.IsDeleted) |
| | | 32 | | { |
| | 3 | 33 | | subject = null; |
| | 3 | 34 | | return false; |
| | | 35 | | } |
| | | 36 | | |
| | 119 | 37 | | if (person.RoleId == (long)Roles.ScopeAdmin && liveOwnedScopeIds.Count == 0) |
| | | 38 | | { |
| | 2 | 39 | | subject = null; |
| | 2 | 40 | | return false; |
| | | 41 | | } |
| | | 42 | | |
| | 117 | 43 | | subject = new AuthTokenSubject( |
| | 117 | 44 | | person.PublicId, |
| | 117 | 45 | | person.Name, |
| | 117 | 46 | | (int)person.RoleId, |
| | 117 | 47 | | person.RoleId == (long)Roles.User ? person.ScopeMembership!.Scope.PublicId : null, |
| | 117 | 48 | | liveOwnedScopeIds, |
| | 117 | 49 | | person.SecurityStamp); |
| | | 50 | | |
| | 117 | 51 | | return true; |
| | | 52 | | } |
| | | 53 | | |
| | | 54 | | /// <summary>Issues the full authentication token for a subject already built by <see cref="TryBuildSubject" />.</su |
| | 84 | 55 | | public Task<AuthToken> IssueAsync(AuthTokenSubject subject) => tokenIssuer.IssueAsync(subject); |
| | | 56 | | |
| | | 57 | | /// <summary> |
| | | 58 | | /// Issues the subject-rights-only token a restricted person receives instead of a full one |
| | | 59 | | /// (NFR-24). No subject is built first: the scope-eligibility rules decide what a token may |
| | | 60 | | /// claim, and this one claims no scope at all. |
| | | 61 | | /// </summary> |
| | | 62 | | public Task<AuthToken> IssueSubjectRightsOnlyAsync(Person person) => |
| | 14 | 63 | | tokenIssuer.IssueSubjectRightsOnlyAsync(person.PublicId, (int)person.RoleId, person.SecurityStamp); |
| | | 64 | | } |