diff --git a/DysonNetwork.Pass/PermissionController.cs b/DysonNetwork.Pass/PermissionController.cs new file mode 100644 index 0000000..c10d9b4 --- /dev/null +++ b/DysonNetwork.Pass/PermissionController.cs @@ -0,0 +1,345 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using DysonNetwork.Pass.Permission; +using DysonNetwork.Shared.Models; +using NodaTime; +using System.Text.Json; + +namespace DysonNetwork.Pass; + +[ApiController] +[Route("/api/permissions")] +[Authorize] +public class PermissionController( + PermissionService permissionService, + AppDatabase db +) : ControllerBase +{ + /// + /// Check if an actor has a specific permission + /// + [HttpGet("check/{actor}/{area}/{key}")] + [RequiredPermission("maintenance", "permissions.check")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task CheckPermission(string actor, string area, string key) + { + try + { + var hasPermission = await permissionService.HasPermissionAsync(actor, area, key); + return Ok(hasPermission); + } + catch (ArgumentException ex) + { + return BadRequest(new { error = ex.Message }); + } + catch (Exception ex) + { + return StatusCode(500, new { error = "Failed to check permission", details = ex.Message }); + } + } + + /// + /// Get all effective permissions for an actor (including group permissions) + /// + [HttpGet("actors/{actor}/permissions/effective")] + [RequiredPermission("maintenance", "permissions.check")] + [ProducesResponseType>(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task GetEffectivePermissions(string actor) + { + try + { + var permissions = await permissionService.ListEffectivePermissionsAsync(actor); + return Ok(permissions); + } + catch (ArgumentException ex) + { + return BadRequest(new { error = ex.Message }); + } + catch (Exception ex) + { + return StatusCode(500, new { error = "Failed to list permissions", details = ex.Message }); + } + } + + /// + /// Get all direct permissions for an actor (excluding group permissions) + /// + [HttpGet("actors/{actor}/permissions/direct")] + [RequiredPermission("maintenance", "permissions.check")] + [ProducesResponseType>(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task GetDirectPermissions(string actor) + { + try + { + var permissions = await permissionService.ListDirectPermissionsAsync(actor); + return Ok(permissions); + } + catch (ArgumentException ex) + { + return BadRequest(new { error = ex.Message }); + } + catch (Exception ex) + { + return StatusCode(500, new { error = "Failed to list permissions", details = ex.Message }); + } + } + + /// + /// Give a permission to an actor + /// + [HttpPost("actors/{actor}/permissions/{area}/{key}")] + [RequiredPermission("maintenance", "permissions.manage")] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task GivePermission( + string actor, + string area, + string key, + [FromBody] PermissionRequest request) + { + try + { + var permission = await permissionService.AddPermissionNode( + actor, + area, + key, + JsonDocument.Parse(JsonSerializer.Serialize(request.Value)), + request.ExpiredAt, + request.AffectedAt + ); + return Created($"/api/permissions/actors/{actor}/permissions/{area}/{key}", permission); + } + catch (ArgumentException ex) + { + return BadRequest(new { error = ex.Message }); + } + catch (Exception ex) + { + return StatusCode(500, new { error = "Failed to add permission", details = ex.Message }); + } + } + + /// + /// Remove a permission from an actor + /// + [HttpDelete("actors/{actor}/permissions/{area}/{key}")] + [RequiredPermission("maintenance", "permissions.manage")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task RemovePermission(string actor, string area, string key) + { + try + { + await permissionService.RemovePermissionNode(actor, area, key); + return NoContent(); + } + catch (ArgumentException ex) + { + return BadRequest(new { error = ex.Message }); + } + catch (Exception ex) + { + return StatusCode(500, new { error = "Failed to remove permission", details = ex.Message }); + } + } + + /// + /// Get all groups for an actor + /// + [HttpGet("actors/{actor}/groups")] + [RequiredPermission("maintenance", "permissions.groups.check")] + [ProducesResponseType>(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task GetActorGroups(string actor) + { + try + { + var now = SystemClock.Instance.GetCurrentInstant(); + var groups = await db.PermissionGroupMembers + .Where(m => m.Actor == actor) + .Where(m => m.ExpiredAt == null || m.ExpiredAt > now) + .Where(m => m.AffectedAt == null || m.AffectedAt <= now) + .Include(m => m.Group) + .ToListAsync(); + + return Ok(groups); + } + catch (Exception ex) + { + return StatusCode(500, new { error = "Failed to list actor groups", details = ex.Message }); + } + } + + /// + /// Add an actor to a permission group + /// + [HttpPost("actors/{actor}/groups/{groupId}")] + [RequiredPermission("maintenance", "permissions.groups.manage")] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task AddActorToGroup( + string actor, + Guid groupId, + [FromBody] GroupMembershipRequest? request = null) + { + try + { + var group = await db.PermissionGroups.FindAsync(groupId); + if (group == null) + { + return NotFound(new { error = "Permission group not found" }); + } + + // Check if actor is already in the group + var existing = await db.PermissionGroupMembers + .FirstOrDefaultAsync(m => m.Actor == actor && m.GroupId == groupId); + + if (existing != null) + { + return BadRequest(new { error = "Actor is already in this group" }); + } + + var member = new SnPermissionGroupMember + { + Actor = actor, + GroupId = groupId, + Group = group, + ExpiredAt = request?.ExpiredAt, + AffectedAt = request?.AffectedAt + }; + + db.PermissionGroupMembers.Add(member); + await db.SaveChangesAsync(); + + // Clear actor cache + await permissionService.ClearActorCacheAsync(actor); + + return Created($"/api/permissions/actors/{actor}/groups/{groupId}", member); + } + catch (Exception ex) + { + return StatusCode(500, new { error = "Failed to add actor to group", details = ex.Message }); + } + } + + /// + /// Remove an actor from a permission group + /// + [HttpDelete("actors/{actor}/groups/{groupId}")] + [RequiredPermission("maintenance", "permissions.groups.manage")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task RemoveActorFromGroup(string actor, Guid groupId) + { + try + { + var member = await db.PermissionGroupMembers + .FirstOrDefaultAsync(m => m.Actor == actor && m.GroupId == groupId); + + if (member == null) + { + return NotFound(new { error = "Actor is not in this group" }); + } + + db.PermissionGroupMembers.Remove(member); + await db.SaveChangesAsync(); + + // Clear actor cache + await permissionService.ClearActorCacheAsync(actor); + + return NoContent(); + } + catch (Exception ex) + { + return StatusCode(500, new { error = "Failed to remove actor from group", details = ex.Message }); + } + } + + /// + /// Clear permission cache for an actor + /// + [HttpPost("actors/{actor}/cache/clear")] + [RequiredPermission("maintenance", "permissions.cache.manage")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task ClearActorCache(string actor) + { + try + { + await permissionService.ClearActorCacheAsync(actor); + return NoContent(); + } + catch (ArgumentException ex) + { + return BadRequest(new { error = ex.Message }); + } + catch (Exception ex) + { + return StatusCode(500, new { error = "Failed to clear cache", details = ex.Message }); + } + } + + /// + /// Validate a permission pattern + /// + [HttpPost("validate-pattern")] + [RequiredPermission("maintenance", "permissions.check")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public IActionResult ValidatePattern([FromBody] PatternValidationRequest request) + { + try + { + var isValid = PermissionService.IsValidPermissionPattern(request.Pattern); + return Ok(new PatternValidationResponse + { + Pattern = request.Pattern, + IsValid = isValid, + Message = isValid ? "Pattern is valid" : "Pattern contains invalid characters or consecutive wildcards" + }); + } + catch (Exception ex) + { + return BadRequest(new { error = ex.Message }); + } + } +} + +public class PermissionRequest +{ + public object? Value { get; set; } + public NodaTime.Instant? ExpiredAt { get; set; } + public NodaTime.Instant? AffectedAt { get; set; } +} + +public class GroupMembershipRequest +{ + public NodaTime.Instant? ExpiredAt { get; set; } + public NodaTime.Instant? AffectedAt { get; set; } +} + +public class PatternValidationRequest +{ + public string Pattern { get; set; } = string.Empty; +} + +public class PatternValidationResponse +{ + public string Pattern { get; set; } = string.Empty; + public bool IsValid { get; set; } + public string Message { get; set; } = string.Empty; +}