mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-07 04:42:08 +00:00
feat(permissions): expose team member permission overrides (SH-328)
This commit is contained in:
parent
d6c3cd2e24
commit
441735d39d
9 changed files with 549 additions and 0 deletions
|
|
@ -0,0 +1,80 @@
|
||||||
|
using Data.SeaHavenIndustries;
|
||||||
|
using Data.SeaHavenIndustries.Enums;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SeaHaven.DataServices.Implementation;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Api.SeaHavenIndustries.Tests;
|
||||||
|
|
||||||
|
public sealed class TeamPermissionOverrideDataServiceTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task GetUserAsync_ReturnsIdentityRoleAndStoredOverrides()
|
||||||
|
{
|
||||||
|
await using var context = NewContext();
|
||||||
|
SeedUserWithRole(context, "u1", "Dispatcher");
|
||||||
|
context.UserPermissionOverrides.Add(new UserPermissionOverride
|
||||||
|
{
|
||||||
|
UserId = "u1",
|
||||||
|
PermissionKey = "deleteSites",
|
||||||
|
State = UserPermissionState.Deny
|
||||||
|
});
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var result = await new TeamPermissionOverrideDataService(context)
|
||||||
|
.GetUserAsync("u1", CancellationToken.None);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result!.RoleName.Should().Be("Dispatcher");
|
||||||
|
result.Overrides["deleteSites"].Should().Be(UserPermissionState.Deny);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SetOverrideAsync_UpsertsOneCompositeKey()
|
||||||
|
{
|
||||||
|
await using var context = NewContext();
|
||||||
|
SeedUserWithRole(context, "u1", "Scheduler");
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
var service = new TeamPermissionOverrideDataService(context);
|
||||||
|
|
||||||
|
await service.SetOverrideAsync("u1", "deleteSites", UserPermissionState.Allow, CancellationToken.None);
|
||||||
|
await service.SetOverrideAsync("u1", "deleteSites", UserPermissionState.Deny, CancellationToken.None);
|
||||||
|
|
||||||
|
var rows = await context.UserPermissionOverrides.ToListAsync();
|
||||||
|
rows.Should().ContainSingle();
|
||||||
|
rows[0].State.Should().Be(UserPermissionState.Deny);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ApplicationDbContext NewContext()
|
||||||
|
{
|
||||||
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
||||||
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||||
|
.Options;
|
||||||
|
return new ApplicationDbContext(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SeedUserWithRole(ApplicationDbContext context, string userId, string roleName)
|
||||||
|
{
|
||||||
|
context.Users.Add(new ApplicationUser
|
||||||
|
{
|
||||||
|
Id = userId,
|
||||||
|
UserName = userId,
|
||||||
|
NormalizedUserName = userId.ToUpperInvariant(),
|
||||||
|
Email = userId + "@example.com",
|
||||||
|
NormalizedEmail = (userId + "@example.com").ToUpperInvariant()
|
||||||
|
});
|
||||||
|
context.Roles.Add(new IdentityRole
|
||||||
|
{
|
||||||
|
Id = "role-1",
|
||||||
|
Name = roleName,
|
||||||
|
NormalizedName = roleName.ToUpperInvariant()
|
||||||
|
});
|
||||||
|
context.UserRoles.Add(new IdentityUserRole<string>
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
RoleId = "role-1"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
using Data.SeaHavenIndustries;
|
||||||
|
using Data.SeaHavenIndustries.Enums;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using SeaHaven.Services.DTOs;
|
||||||
|
using SeaHaven.Services.Interfaces;
|
||||||
|
|
||||||
|
namespace Api.SeaHavenIndustries.Controllers;
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/User/{userId}/Permissions")]
|
||||||
|
public sealed class TeamPermissionController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly ITeamPermissionService _permissionService;
|
||||||
|
|
||||||
|
public TeamPermissionController(ITeamPermissionService permissionService)
|
||||||
|
{
|
||||||
|
_permissionService = permissionService;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GetProfile(
|
||||||
|
string userId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await _permissionService.GetProfileAsync(userId, User, cancellationToken);
|
||||||
|
return ToActionResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{permissionKey}")]
|
||||||
|
public async Task<IActionResult> SetOverride(
|
||||||
|
string userId,
|
||||||
|
string permissionKey,
|
||||||
|
[FromBody] SetTeamPermissionOverrideDTO request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!Enum.IsDefined(request.State))
|
||||||
|
return BadRequest(new Response { Status = "Error", Message = "Invalid permission state." });
|
||||||
|
|
||||||
|
var result = await _permissionService.SetOverrideAsync(
|
||||||
|
userId,
|
||||||
|
permissionKey,
|
||||||
|
request.State,
|
||||||
|
User,
|
||||||
|
cancellationToken);
|
||||||
|
return ToActionResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IActionResult ToActionResult(
|
||||||
|
TeamPermissionResult<TeamPermissionProfileDTO> result)
|
||||||
|
{
|
||||||
|
return result.Status switch
|
||||||
|
{
|
||||||
|
TeamPermissionResultStatus.Success => new OkObjectResult(result.Value),
|
||||||
|
TeamPermissionResultStatus.Forbidden => new ForbidResult(),
|
||||||
|
TeamPermissionResultStatus.NotFound => new NotFoundObjectResult(
|
||||||
|
new Response { Status = "Error", Message = "User not found." }),
|
||||||
|
TeamPermissionResultStatus.InvalidPermissionKey => new BadRequestObjectResult(
|
||||||
|
new Response { Status = "Error", Message = "Unknown permission key." }),
|
||||||
|
_ => new BadRequestObjectResult(
|
||||||
|
new Response { Status = "Error", Message = "Unable to update permissions." })
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
11
SeaHaven.DataServices/Dto/TeamPermissionReadModels.cs
Normal file
11
SeaHaven.DataServices/Dto/TeamPermissionReadModels.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
using Data.SeaHavenIndustries.Enums;
|
||||||
|
|
||||||
|
namespace SeaHaven.DataServices.Dto;
|
||||||
|
|
||||||
|
public sealed class TeamPermissionUserData
|
||||||
|
{
|
||||||
|
public required string UserId { get; init; }
|
||||||
|
public string? RoleName { get; init; }
|
||||||
|
public IReadOnlyDictionary<string, UserPermissionState> Overrides { get; init; }
|
||||||
|
= new Dictionary<string, UserPermissionState>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
using Data.SeaHavenIndustries;
|
||||||
|
using Data.SeaHavenIndustries.Enums;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SeaHaven.DataServices.Dto;
|
||||||
|
using SeaHaven.DataServices.Interfaces;
|
||||||
|
|
||||||
|
namespace SeaHaven.DataServices.Implementation;
|
||||||
|
|
||||||
|
public sealed class TeamPermissionOverrideDataService : ITeamPermissionOverrideDataService
|
||||||
|
{
|
||||||
|
private readonly ApplicationDbContext _context;
|
||||||
|
|
||||||
|
public TeamPermissionOverrideDataService(ApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TeamPermissionUserData?> GetUserAsync(
|
||||||
|
string userId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var userExists = await _context.Users
|
||||||
|
.AsNoTracking()
|
||||||
|
.AnyAsync(user => user.Id == userId, cancellationToken);
|
||||||
|
|
||||||
|
if (!userExists)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var roleName = await (
|
||||||
|
from userRole in _context.UserRoles.AsNoTracking()
|
||||||
|
join role in _context.Roles.AsNoTracking()
|
||||||
|
on userRole.RoleId equals role.Id
|
||||||
|
where userRole.UserId == userId
|
||||||
|
select role.Name)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
var overrides = await _context.UserPermissionOverrides
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(permission => permission.UserId == userId)
|
||||||
|
.ToDictionaryAsync(
|
||||||
|
permission => permission.PermissionKey,
|
||||||
|
permission => permission.State,
|
||||||
|
StringComparer.OrdinalIgnoreCase,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
return new TeamPermissionUserData
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
RoleName = roleName,
|
||||||
|
Overrides = overrides
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetOverrideAsync(
|
||||||
|
string userId,
|
||||||
|
string permissionKey,
|
||||||
|
UserPermissionState state,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var existing = await _context.UserPermissionOverrides
|
||||||
|
.SingleOrDefaultAsync(
|
||||||
|
permission => permission.UserId == userId
|
||||||
|
&& permission.PermissionKey == permissionKey,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
if (existing is null)
|
||||||
|
{
|
||||||
|
await _context.UserPermissionOverrides.AddAsync(
|
||||||
|
new UserPermissionOverride
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
PermissionKey = permissionKey,
|
||||||
|
State = state
|
||||||
|
},
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
existing.State = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
using Data.SeaHavenIndustries.Enums;
|
||||||
|
using SeaHaven.DataServices.Dto;
|
||||||
|
|
||||||
|
namespace SeaHaven.DataServices.Interfaces;
|
||||||
|
|
||||||
|
public interface ITeamPermissionOverrideDataService
|
||||||
|
{
|
||||||
|
Task<TeamPermissionUserData?> GetUserAsync(string userId, CancellationToken cancellationToken);
|
||||||
|
Task SetOverrideAsync(
|
||||||
|
string userId,
|
||||||
|
string permissionKey,
|
||||||
|
UserPermissionState state,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
130
SeaHaven.Services.Tests/TeamPermissionServiceTests.cs
Normal file
130
SeaHaven.Services.Tests/TeamPermissionServiceTests.cs
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
using Data.SeaHavenIndustries.Enums;
|
||||||
|
using FluentAssertions;
|
||||||
|
using SeaHaven.DataServices.Dto;
|
||||||
|
using SeaHaven.DataServices.Interfaces;
|
||||||
|
using SeaHaven.Services.Constants;
|
||||||
|
using SeaHaven.Services.Implementation;
|
||||||
|
using SeaHaven.Services.DTOs;
|
||||||
|
using SeaHaven.Services.Interfaces;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace SeaHaven.Services.Tests;
|
||||||
|
|
||||||
|
public sealed class TeamPermissionServiceTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task GetProfile_ReturnsEveryKeyAndRoleDefaults()
|
||||||
|
{
|
||||||
|
var data = new FakeDataService("u1", "Dispatcher");
|
||||||
|
var result = await Service(data).GetProfileAsync("u1", Admin(), CancellationToken.None);
|
||||||
|
|
||||||
|
result.IsSuccess.Should().BeTrue();
|
||||||
|
result.Value!.Permissions.Should().HaveCount(22);
|
||||||
|
result.Value.Permissions.Single(p => p.PermissionKey == TeamPermissionKeys.CreateSites).IsGranted.Should().BeTrue();
|
||||||
|
result.Value.Permissions.Single(p => p.PermissionKey == TeamPermissionKeys.DeleteSites).IsGranted.Should().BeFalse();
|
||||||
|
result.Value.Permissions.Should().OnlyContain(p => p.OverrideState == UserPermissionState.Unset);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SetOverride_ChangesOnlyOnePermissionAndPersistsCanonicalKey()
|
||||||
|
{
|
||||||
|
var data = new FakeDataService("u1", "Dispatcher");
|
||||||
|
var result = await Service(data).SetOverrideAsync(
|
||||||
|
"u1", "DELETEsites", UserPermissionState.Allow, Admin(), CancellationToken.None);
|
||||||
|
|
||||||
|
result.IsSuccess.Should().BeTrue();
|
||||||
|
result.Value!.Permissions.Single(p => p.PermissionKey == TeamPermissionKeys.DeleteSites).IsGranted.Should().BeTrue();
|
||||||
|
result.Value.Permissions.Single(p => p.PermissionKey == TeamPermissionKeys.DeleteSites).OverrideState.Should().Be(UserPermissionState.Allow);
|
||||||
|
result.Value.Permissions.Single(p => p.PermissionKey == TeamPermissionKeys.CreateSites).IsGranted.Should().BeTrue();
|
||||||
|
data.LastSet.Should().Be(("u1", TeamPermissionKeys.DeleteSites, UserPermissionState.Allow));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SetOverride_RejectsUnknownKeyBeforeDataAccess()
|
||||||
|
{
|
||||||
|
var data = new FakeDataService("u1", "Dispatcher");
|
||||||
|
var result = await Service(data).SetOverrideAsync(
|
||||||
|
"u1", "not-a-permission", UserPermissionState.Allow, Admin(), CancellationToken.None);
|
||||||
|
|
||||||
|
result.Status.Should().Be(TeamPermissionResultStatus.InvalidPermissionKey);
|
||||||
|
data.GetCalls.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task NonAdminCannotReadOrWritePermissions()
|
||||||
|
{
|
||||||
|
var data = new FakeDataService("u1", "Dispatcher");
|
||||||
|
var service = Service(data);
|
||||||
|
|
||||||
|
var read = await service.GetProfileAsync("u1", User("Dispatcher"), CancellationToken.None);
|
||||||
|
var write = await service.SetOverrideAsync("u1", TeamPermissionKeys.CreateSites, UserPermissionState.Deny, User("Dispatcher"), CancellationToken.None);
|
||||||
|
|
||||||
|
read.Status.Should().Be(TeamPermissionResultStatus.Forbidden);
|
||||||
|
write.Status.Should().Be(TeamPermissionResultStatus.Forbidden);
|
||||||
|
data.GetCalls.Should().Be(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UnknownRoleReceivesNoGrantEvenWhenAnOverrideIsSet()
|
||||||
|
{
|
||||||
|
var data = new FakeDataService("u1", "Legacy");
|
||||||
|
var result = await Service(data).SetOverrideAsync(
|
||||||
|
"u1", TeamPermissionKeys.CreateSites, UserPermissionState.Allow, Admin(), CancellationToken.None);
|
||||||
|
|
||||||
|
result.Value!.Permissions.Should().OnlyContain(p => !p.IsGranted);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task MissingUserReturnsNotFound()
|
||||||
|
{
|
||||||
|
var data = new FakeDataService(null, null);
|
||||||
|
var result = await Service(data).GetProfileAsync("missing", Admin(), CancellationToken.None);
|
||||||
|
|
||||||
|
result.Status.Should().Be(TeamPermissionResultStatus.NotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TeamPermissionService Service(FakeDataService data) =>
|
||||||
|
new(data, new TeamPermissionPolicy());
|
||||||
|
|
||||||
|
private static ClaimsPrincipal Admin() => User("Admin");
|
||||||
|
|
||||||
|
private static ClaimsPrincipal User(string role) =>
|
||||||
|
new(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Role, role) }, "test"));
|
||||||
|
|
||||||
|
private sealed class FakeDataService : ITeamPermissionOverrideDataService
|
||||||
|
{
|
||||||
|
private readonly string? _userId;
|
||||||
|
private readonly string? _roleName;
|
||||||
|
private readonly Dictionary<string, UserPermissionState> _overrides = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public FakeDataService(string? userId, string? roleName)
|
||||||
|
{
|
||||||
|
_userId = userId;
|
||||||
|
_roleName = roleName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetCalls { get; private set; }
|
||||||
|
public (string UserId, string PermissionKey, UserPermissionState State)? LastSet { get; private set; }
|
||||||
|
|
||||||
|
public Task<TeamPermissionUserData?> GetUserAsync(string userId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
GetCalls++;
|
||||||
|
return Task.FromResult<TeamPermissionUserData?>(_userId != userId
|
||||||
|
? null
|
||||||
|
: new TeamPermissionUserData
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
RoleName = _roleName,
|
||||||
|
Overrides = new Dictionary<string, UserPermissionState>(_overrides, StringComparer.OrdinalIgnoreCase)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task SetOverrideAsync(string userId, string permissionKey, UserPermissionState state, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_overrides[permissionKey] = state;
|
||||||
|
LastSet = (userId, permissionKey, state);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
49
SeaHaven.Services/DTOs/TeamPermissionDTOs.cs
Normal file
49
SeaHaven.Services/DTOs/TeamPermissionDTOs.cs
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
using Data.SeaHavenIndustries.Enums;
|
||||||
|
|
||||||
|
namespace SeaHaven.Services.DTOs;
|
||||||
|
|
||||||
|
public sealed class TeamPermissionProfileDTO
|
||||||
|
{
|
||||||
|
public required string UserId { get; init; }
|
||||||
|
public string? RoleName { get; init; }
|
||||||
|
public required IReadOnlyList<TeamPermissionValueDTO> Permissions { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class TeamPermissionValueDTO
|
||||||
|
{
|
||||||
|
public required string PermissionKey { get; init; }
|
||||||
|
public UserPermissionState OverrideState { get; init; }
|
||||||
|
public bool IsGranted { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class SetTeamPermissionOverrideDTO
|
||||||
|
{
|
||||||
|
public UserPermissionState State { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum TeamPermissionResultStatus
|
||||||
|
{
|
||||||
|
Success,
|
||||||
|
Forbidden,
|
||||||
|
NotFound,
|
||||||
|
InvalidPermissionKey
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class TeamPermissionResult<T>
|
||||||
|
{
|
||||||
|
private TeamPermissionResult(TeamPermissionResultStatus status, T? value)
|
||||||
|
{
|
||||||
|
Status = status;
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TeamPermissionResultStatus Status { get; }
|
||||||
|
public T? Value { get; }
|
||||||
|
public bool IsSuccess => Status == TeamPermissionResultStatus.Success;
|
||||||
|
|
||||||
|
public static TeamPermissionResult<T> Success(T value) =>
|
||||||
|
new(TeamPermissionResultStatus.Success, value);
|
||||||
|
|
||||||
|
public static TeamPermissionResult<T> Failure(TeamPermissionResultStatus status) =>
|
||||||
|
new(status, default);
|
||||||
|
}
|
||||||
96
SeaHaven.Services/Implementation/TeamPermissionService.cs
Normal file
96
SeaHaven.Services/Implementation/TeamPermissionService.cs
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
using Data.SeaHavenIndustries.Enums;
|
||||||
|
using SeaHaven.DataServices.Dto;
|
||||||
|
using SeaHaven.DataServices.Interfaces;
|
||||||
|
using SeaHaven.Services.Constants;
|
||||||
|
using SeaHaven.Services.DTOs;
|
||||||
|
using SeaHaven.Services.Interfaces;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace SeaHaven.Services.Implementation;
|
||||||
|
|
||||||
|
public sealed class TeamPermissionService : ITeamPermissionService
|
||||||
|
{
|
||||||
|
private readonly ITeamPermissionOverrideDataService _dataService;
|
||||||
|
private readonly ITeamPermissionPolicy _policy;
|
||||||
|
|
||||||
|
public TeamPermissionService(
|
||||||
|
ITeamPermissionOverrideDataService dataService,
|
||||||
|
ITeamPermissionPolicy policy)
|
||||||
|
{
|
||||||
|
_dataService = dataService;
|
||||||
|
_policy = policy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TeamPermissionResult<TeamPermissionProfileDTO>> GetProfileAsync(
|
||||||
|
string userId,
|
||||||
|
ClaimsPrincipal caller,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!IsAdmin(caller))
|
||||||
|
return TeamPermissionResult<TeamPermissionProfileDTO>.Failure(TeamPermissionResultStatus.Forbidden);
|
||||||
|
|
||||||
|
var user = await _dataService.GetUserAsync(userId, cancellationToken);
|
||||||
|
return user is null
|
||||||
|
? TeamPermissionResult<TeamPermissionProfileDTO>.Failure(TeamPermissionResultStatus.NotFound)
|
||||||
|
: TeamPermissionResult<TeamPermissionProfileDTO>.Success(BuildProfile(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TeamPermissionResult<TeamPermissionProfileDTO>> SetOverrideAsync(
|
||||||
|
string userId,
|
||||||
|
string permissionKey,
|
||||||
|
UserPermissionState state,
|
||||||
|
ClaimsPrincipal caller,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!IsAdmin(caller))
|
||||||
|
return TeamPermissionResult<TeamPermissionProfileDTO>.Failure(TeamPermissionResultStatus.Forbidden);
|
||||||
|
|
||||||
|
var canonicalKey = TeamPermissionKeys.All.FirstOrDefault(
|
||||||
|
key => string.Equals(key, permissionKey, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (canonicalKey is null)
|
||||||
|
return TeamPermissionResult<TeamPermissionProfileDTO>.Failure(
|
||||||
|
TeamPermissionResultStatus.InvalidPermissionKey);
|
||||||
|
|
||||||
|
var user = await _dataService.GetUserAsync(userId, cancellationToken);
|
||||||
|
if (user is null)
|
||||||
|
return TeamPermissionResult<TeamPermissionProfileDTO>.Failure(TeamPermissionResultStatus.NotFound);
|
||||||
|
|
||||||
|
await _dataService.SetOverrideAsync(userId, canonicalKey, state, cancellationToken);
|
||||||
|
|
||||||
|
var updatedOverrides = user.Overrides.ToDictionary(
|
||||||
|
pair => pair.Key,
|
||||||
|
pair => pair.Value,
|
||||||
|
StringComparer.OrdinalIgnoreCase);
|
||||||
|
updatedOverrides[canonicalKey] = state;
|
||||||
|
|
||||||
|
return TeamPermissionResult<TeamPermissionProfileDTO>.Success(
|
||||||
|
BuildProfile(new TeamPermissionUserData
|
||||||
|
{
|
||||||
|
UserId = user.UserId,
|
||||||
|
RoleName = user.RoleName,
|
||||||
|
Overrides = updatedOverrides
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private TeamPermissionProfileDTO BuildProfile(TeamPermissionUserData user)
|
||||||
|
{
|
||||||
|
return new TeamPermissionProfileDTO
|
||||||
|
{
|
||||||
|
UserId = user.UserId,
|
||||||
|
RoleName = user.RoleName,
|
||||||
|
Permissions = TeamPermissionKeys.All
|
||||||
|
.Select(key => new TeamPermissionValueDTO
|
||||||
|
{
|
||||||
|
PermissionKey = key,
|
||||||
|
OverrideState = user.Overrides.TryGetValue(key, out var state)
|
||||||
|
? state
|
||||||
|
: UserPermissionState.Unset,
|
||||||
|
IsGranted = _policy.IsAllowed(user.RoleName, key, user.Overrides)
|
||||||
|
})
|
||||||
|
.ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsAdmin(ClaimsPrincipal? caller) =>
|
||||||
|
caller?.IsInRole("Admin") == true;
|
||||||
|
}
|
||||||
20
SeaHaven.Services/Interfaces/ITeamPermissionService.cs
Normal file
20
SeaHaven.Services/Interfaces/ITeamPermissionService.cs
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
using Data.SeaHavenIndustries.Enums;
|
||||||
|
using SeaHaven.Services.DTOs;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace SeaHaven.Services.Interfaces;
|
||||||
|
|
||||||
|
public interface ITeamPermissionService
|
||||||
|
{
|
||||||
|
Task<TeamPermissionResult<TeamPermissionProfileDTO>> GetProfileAsync(
|
||||||
|
string userId,
|
||||||
|
ClaimsPrincipal caller,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task<TeamPermissionResult<TeamPermissionProfileDTO>> SetOverrideAsync(
|
||||||
|
string userId,
|
||||||
|
string permissionKey,
|
||||||
|
UserPermissionState state,
|
||||||
|
ClaimsPrincipal caller,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue