| | | 1 | | using ArturRios.Heimdall.Command.Input; |
| | | 2 | | using ArturRios.Heimdall.Command.Output; |
| | | 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.Heimdall.WebApi.Security; |
| | | 8 | | using ArturRios.Mediator.Command; |
| | | 9 | | using ArturRios.Mediator.Query; |
| | | 10 | | using ArturRios.Output; |
| | | 11 | | using ArturRios.Util.WebApi.AspNetCore; |
| | | 12 | | using ArturRios.Util.WebApi.Security.Attributes; |
| | | 13 | | using Microsoft.AspNetCore.Mvc; |
| | | 14 | | |
| | | 15 | | namespace ArturRios.Heimdall.WebApi.Controllers; |
| | | 16 | | |
| | | 17 | | [Route("api/scopes/{scopeId:guid}/permissions")] |
| | 66 | 18 | | public class ScopePermissionController(CommandMediator commandMediator, QueryMediator queryMediator) : Controller |
| | | 19 | | { |
| | | 20 | | /// <summary> |
| | | 21 | | /// Creates a scope-specific permission within a scope (UC-31, FR-SP-01/02). Only a System Admin |
| | | 22 | | /// or a Scope Admin may call it: the endpoint's <c>[RoleRequirement]</c> refuses a <c>User</c>, |
| | | 23 | | /// who has no standing to manage a scope's permissions. The remaining rules depend on data the |
| | | 24 | | /// attribute cannot see and are enforced by the handler — the target scope must exist and be |
| | | 25 | | /// active (AF-31a), the input must be well-formed (AF-31d), and an acting Scope Admin must own |
| | | 26 | | /// the scope (AF-31e). A scope permission has no owner of its own, so owning the scope is the |
| | | 27 | | /// whole of the authorization. |
| | | 28 | | /// </summary> |
| | | 29 | | [HttpPost] |
| | | 30 | | [RoleRequirement((int)Roles.SystemAdmin, (int)Roles.ScopeAdmin)] |
| | | 31 | | public async Task<ActionResult<DataOutput<CreateScopePermissionCommandOutput?>>> Create( |
| | | 32 | | Guid scopeId, [FromBody] CreateScopePermissionCommand command) |
| | | 33 | | { |
| | 24 | 34 | | command.ScopeId = scopeId; |
| | 24 | 35 | | HttpContext.ApplyActor(command); |
| | | 36 | | |
| | 24 | 37 | | var result = await commandMediator |
| | 24 | 38 | | .ExecuteCommandAsync<CreateScopePermissionCommand, CreateScopePermissionCommandOutput>(command); |
| | | 39 | | |
| | 24 | 40 | | return result.ToActionResult(statusMap: ScopePermissionMessageMap.StatusCodes); |
| | 24 | 41 | | } |
| | | 42 | | |
| | | 43 | | /// <summary> |
| | | 44 | | /// Retrieves a single scope permission by its public identifier within the scope (UC-32, |
| | | 45 | | /// FR-SP-04/09). A <c>User</c> is refused by the attribute; among the remaining actors the |
| | | 46 | | /// rule is data-dependent and lives in the handler — a System Admin sees any permission, a |
| | | 47 | | /// Scope Admin only one whose scope they own (AF-32e). A permission that does not exist under |
| | | 48 | | /// the addressed scope, or that is logically deleted and was not explicitly requested, is |
| | | 49 | | /// AF-32a. |
| | | 50 | | /// </summary> |
| | | 51 | | [HttpGet("{id:guid}")] |
| | | 52 | | [RoleRequirement((int)Roles.SystemAdmin, (int)Roles.ScopeAdmin)] |
| | | 53 | | public async Task<ActionResult<DataOutput<ScopePermissionOutput?>>> GetById( |
| | | 54 | | Guid scopeId, Guid id, [FromQuery] bool includeDeleted = false) |
| | | 55 | | { |
| | 7 | 56 | | var query = new GetScopePermissionByIdQuery { ScopeId = scopeId, Id = id, IncludeDeleted = includeDeleted }; |
| | 7 | 57 | | HttpContext.ApplyActor(query); |
| | | 58 | | |
| | 7 | 59 | | var result = await queryMediator.ExecuteQueryAsync<GetScopePermissionByIdQuery, ScopePermissionOutput>(query); |
| | | 60 | | |
| | 7 | 61 | | return result.ToActionResult(statusMap: ScopePermissionMessageMap.StatusCodes); |
| | 7 | 62 | | } |
| | | 63 | | |
| | | 64 | | /// <summary> |
| | | 65 | | /// Lists the permissions of a scope (UC-32, FR-SP-05/09). A System Admin sees every |
| | | 66 | | /// permission in the scope; a Scope Admin must own the scope (AF-32e) and then sees every |
| | | 67 | | /// permission in it — a scope permission has no owner of its own, so there is no per-owner |
| | | 68 | | /// narrowing. Both the ownership gate and the scope lookup are enforced by the handler from |
| | | 69 | | /// the acting user; a missing or logically deleted scope reuses AF-31a. |
| | | 70 | | /// </summary> |
| | | 71 | | [HttpGet] |
| | | 72 | | [RoleRequirement((int)Roles.SystemAdmin, (int)Roles.ScopeAdmin)] |
| | | 73 | | public async Task<ActionResult<PaginatedOutput<ScopePermissionOutput>>> List( |
| | | 74 | | Guid scopeId, [FromQuery] ListScopePermissionsQuery query) |
| | | 75 | | { |
| | 10 | 76 | | query.ScopeId = scopeId; |
| | 10 | 77 | | HttpContext.ApplyActor(query); |
| | | 78 | | |
| | 10 | 79 | | var result = await queryMediator |
| | 10 | 80 | | .ExecutePaginatedQueryAsync<ListScopePermissionsQuery, ScopePermissionOutput>(query); |
| | | 81 | | |
| | 10 | 82 | | return result.ToActionResult(statusMap: ScopePermissionMessageMap.StatusCodes); |
| | 10 | 83 | | } |
| | | 84 | | |
| | | 85 | | /// <summary> |
| | | 86 | | /// Updates a scope permission's name, description, and JWT-claim flag (UC-33, FR-SP-06). A |
| | | 87 | | /// <c>User</c> is refused by the attribute; among the remaining actors the rule is |
| | | 88 | | /// data-dependent and lives in the handler — a System Admin updates any permission, a Scope |
| | | 89 | | /// Admin only one whose scope they own (AF-33e). The handler resolves the permission inside |
| | | 90 | | /// the addressed scope (AF-33a) and validates the input shape, reusing UC-31's messages. |
| | | 91 | | /// </summary> |
| | | 92 | | [HttpPut("{id:guid}")] |
| | | 93 | | [RoleRequirement((int)Roles.SystemAdmin, (int)Roles.ScopeAdmin)] |
| | | 94 | | public async Task<ActionResult<DataOutput<UpdateScopePermissionCommandOutput?>>> Update( |
| | | 95 | | Guid scopeId, Guid id, [FromBody] UpdateScopePermissionCommand command) |
| | | 96 | | { |
| | 9 | 97 | | command.ScopeId = scopeId; |
| | 9 | 98 | | command.Id = id; |
| | 9 | 99 | | HttpContext.ApplyActor(command); |
| | | 100 | | |
| | 9 | 101 | | var result = await commandMediator |
| | 9 | 102 | | .ExecuteCommandAsync<UpdateScopePermissionCommand, UpdateScopePermissionCommandOutput>(command); |
| | | 103 | | |
| | 9 | 104 | | return result.ToActionResult(statusMap: ScopePermissionMessageMap.StatusCodes); |
| | 9 | 105 | | } |
| | | 106 | | |
| | | 107 | | /// <summary> |
| | | 108 | | /// Logically deletes a scope permission by setting <c>IsDeleted = true</c> (UC-34, FR-SP-07). |
| | | 109 | | /// A <c>User</c> is refused by the attribute; among the remaining actors the rule is |
| | | 110 | | /// data-dependent and lives in the handler — a System Admin deletes any permission, a Scope |
| | | 111 | | /// Admin only one whose scope they own (AF-34e). The handler resolves the permission inside |
| | | 112 | | /// the addressed scope (AF-34a) and answers an already-deleted permission idempotently |
| | | 113 | | /// (AF-34b). Nothing cascades: a scope permission owns no dependent row. |
| | | 114 | | /// </summary> |
| | | 115 | | [HttpDelete("{id:guid}")] |
| | | 116 | | [RoleRequirement((int)Roles.SystemAdmin, (int)Roles.ScopeAdmin)] |
| | | 117 | | public async Task<ActionResult<DataOutput<DeleteScopePermissionCommandOutput?>>> Delete(Guid scopeId, Guid id) |
| | | 118 | | { |
| | 9 | 119 | | var command = new DeleteScopePermissionCommand { ScopeId = scopeId, Id = id }; |
| | 9 | 120 | | HttpContext.ApplyActor(command); |
| | | 121 | | |
| | 9 | 122 | | var result = await commandMediator |
| | 9 | 123 | | .ExecuteCommandAsync<DeleteScopePermissionCommand, DeleteScopePermissionCommandOutput>(command); |
| | | 124 | | |
| | 9 | 125 | | return result.ToActionResult(statusMap: ScopePermissionMessageMap.StatusCodes); |
| | 9 | 126 | | } |
| | | 127 | | |
| | | 128 | | /// <summary> |
| | | 129 | | /// Permanently (hard) deletes a scope permission, removing the record from the database |
| | | 130 | | /// (UC-35, FR-SP-08). Restricted to System Admins: a Scope Admin may logically delete a |
| | | 131 | | /// permission in a scope they own (UC-34), but never purge it, so the attribute settles |
| | | 132 | | /// authorization on its own and the handler applies no further rule. The handler resolves the |
| | | 133 | | /// permission inside the addressed scope in any deletion state and reports AF-35a when it |
| | | 134 | | /// does not exist — which includes a repeated call, as the removal leaves nothing to find. |
| | | 135 | | /// Nothing cascades: the permission's scope is untouched. |
| | | 136 | | /// </summary> |
| | | 137 | | [HttpDelete("{id:guid}/hard")] |
| | | 138 | | [RoleRequirement((int)Roles.SystemAdmin)] |
| | | 139 | | public async Task<ActionResult<DataOutput<HardDeleteScopePermissionCommandOutput?>>> HardDelete( |
| | | 140 | | Guid scopeId, Guid id) |
| | | 141 | | { |
| | 7 | 142 | | var result = await commandMediator |
| | 7 | 143 | | .ExecuteCommandAsync<HardDeleteScopePermissionCommand, HardDeleteScopePermissionCommandOutput>( |
| | 7 | 144 | | new HardDeleteScopePermissionCommand { ScopeId = scopeId, Id = id }); |
| | | 145 | | |
| | 7 | 146 | | return result.ToActionResult(statusMap: ScopePermissionMessageMap.StatusCodes); |
| | 7 | 147 | | } |
| | | 148 | | } |