< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.WebApi.Controllers.ScopePermissionController
Assembly: ArturRios.Heimdall.WebApi
File(s): /home/runner/work/heimdall-api/heimdall-api/src/Presentation/ArturRios.Heimdall.WebApi/Controllers/ScopePermissionController.cs
Line coverage
100%
Covered lines: 36
Uncovered lines: 0
Coverable lines: 36
Total lines: 148
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage
100%
Covered methods: 7
Fully covered methods: 7
Total methods: 7
Method coverage: 100%
Full method coverage: 100%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
Create()100%11100%
GetById()100%11100%
List()100%11100%
Update()100%11100%
Delete()100%11100%
HardDelete()100%11100%

File(s)

/home/runner/work/heimdall-api/heimdall-api/src/Presentation/ArturRios.Heimdall.WebApi/Controllers/ScopePermissionController.cs

#LineLine coverage
 1using ArturRios.Heimdall.Command.Input;
 2using ArturRios.Heimdall.Command.Output;
 3using ArturRios.Heimdall.Domain.Enums;
 4using ArturRios.Heimdall.Query.Input;
 5using ArturRios.Heimdall.Query.Output;
 6using ArturRios.Heimdall.Shared.Messages;
 7using ArturRios.Heimdall.WebApi.Security;
 8using ArturRios.Mediator.Command;
 9using ArturRios.Mediator.Query;
 10using ArturRios.Output;
 11using ArturRios.Util.WebApi.AspNetCore;
 12using ArturRios.Util.WebApi.Security.Attributes;
 13using Microsoft.AspNetCore.Mvc;
 14
 15namespace ArturRios.Heimdall.WebApi.Controllers;
 16
 17[Route("api/scopes/{scopeId:guid}/permissions")]
 6618public 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    {
 2434        command.ScopeId = scopeId;
 2435        HttpContext.ApplyActor(command);
 36
 2437        var result = await commandMediator
 2438            .ExecuteCommandAsync<CreateScopePermissionCommand, CreateScopePermissionCommandOutput>(command);
 39
 2440        return result.ToActionResult(statusMap: ScopePermissionMessageMap.StatusCodes);
 2441    }
 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    {
 756        var query = new GetScopePermissionByIdQuery { ScopeId = scopeId, Id = id, IncludeDeleted = includeDeleted };
 757        HttpContext.ApplyActor(query);
 58
 759        var result = await queryMediator.ExecuteQueryAsync<GetScopePermissionByIdQuery, ScopePermissionOutput>(query);
 60
 761        return result.ToActionResult(statusMap: ScopePermissionMessageMap.StatusCodes);
 762    }
 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    {
 1076        query.ScopeId = scopeId;
 1077        HttpContext.ApplyActor(query);
 78
 1079        var result = await queryMediator
 1080            .ExecutePaginatedQueryAsync<ListScopePermissionsQuery, ScopePermissionOutput>(query);
 81
 1082        return result.ToActionResult(statusMap: ScopePermissionMessageMap.StatusCodes);
 1083    }
 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    {
 997        command.ScopeId = scopeId;
 998        command.Id = id;
 999        HttpContext.ApplyActor(command);
 100
 9101        var result = await commandMediator
 9102            .ExecuteCommandAsync<UpdateScopePermissionCommand, UpdateScopePermissionCommandOutput>(command);
 103
 9104        return result.ToActionResult(statusMap: ScopePermissionMessageMap.StatusCodes);
 9105    }
 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    {
 9119        var command = new DeleteScopePermissionCommand { ScopeId = scopeId, Id = id };
 9120        HttpContext.ApplyActor(command);
 121
 9122        var result = await commandMediator
 9123            .ExecuteCommandAsync<DeleteScopePermissionCommand, DeleteScopePermissionCommandOutput>(command);
 124
 9125        return result.ToActionResult(statusMap: ScopePermissionMessageMap.StatusCodes);
 9126    }
 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    {
 7142        var result = await commandMediator
 7143            .ExecuteCommandAsync<HardDeleteScopePermissionCommand, HardDeleteScopePermissionCommandOutput>(
 7144                new HardDeleteScopePermissionCommand { ScopeId = scopeId, Id = id });
 145
 7146        return result.ToActionResult(statusMap: ScopePermissionMessageMap.StatusCodes);
 7147    }
 148}