mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 03:43:11 +00:00
GET api/team-members/me/permissions returns the keys the signed-in user holds after role defaults and their own overrides, evaluated by the same policy that guards writes. The user comes from the token; a missing or unknown identity gets 401.
174 lines
7 KiB
C#
174 lines
7 KiB
C#
using Data.SeaHavenIndustries.Enums;
|
|
using FluentAssertions;
|
|
using SeaHaven.DataServices.Dto;
|
|
using SeaHaven.DataServices.Interfaces;
|
|
using SeaHaven.Services.Constants;
|
|
using SeaHaven.Services.DTOs;
|
|
using SeaHaven.Services.Implementation;
|
|
using System.Security.Claims;
|
|
using Xunit;
|
|
|
|
namespace SeaHaven.Services.Tests;
|
|
|
|
public sealed class TeamEffectivePermissionsTests
|
|
{
|
|
[Fact]
|
|
public async Task Scheduler_Receives_Role_Defaults()
|
|
{
|
|
var data = new FakeUsers().Add("u1", "Scheduler");
|
|
|
|
var result = await Service(data).GetEffectivePermissionsAsync(Caller("u1"), CancellationToken.None);
|
|
|
|
result.IsSuccess.Should().BeTrue();
|
|
result.Value!.Permissions.Should().Contain(new[]
|
|
{
|
|
TeamPermissionKeys.CreateCompletionDocTemplates,
|
|
TeamPermissionKeys.EditCompletionDocTemplates
|
|
});
|
|
result.Value.Permissions.Should().NotContain(TeamPermissionKeys.DeleteCompletionDocTemplates);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Grant_Override_Adds_A_Key_Outside_The_Role_Defaults()
|
|
{
|
|
var data = new FakeUsers().Add(
|
|
"u1", "Dispatcher", (TeamPermissionKeys.CreateCompletionDocTemplates, UserPermissionState.Allow));
|
|
|
|
var result = await Service(data).GetEffectivePermissionsAsync(Caller("u1"), CancellationToken.None);
|
|
|
|
result.Value!.Permissions.Should().Contain(TeamPermissionKeys.CreateCompletionDocTemplates);
|
|
result.Value.Permissions.Should().NotContain(TeamPermissionKeys.EditCompletionDocTemplates);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Revoke_Override_Removes_A_Role_Default()
|
|
{
|
|
var data = new FakeUsers().Add(
|
|
"u1", "Scheduler", (TeamPermissionKeys.EditCompletionDocTemplates, UserPermissionState.Deny));
|
|
|
|
var result = await Service(data).GetEffectivePermissionsAsync(Caller("u1"), CancellationToken.None);
|
|
|
|
result.Value!.Permissions.Should().NotContain(TeamPermissionKeys.EditCompletionDocTemplates);
|
|
result.Value.Permissions.Should().Contain(TeamPermissionKeys.CreateCompletionDocTemplates);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Admin_Holds_Every_Key_Even_With_A_Revoke_Override()
|
|
{
|
|
var data = new FakeUsers().Add(
|
|
"u1", "Admin", (TeamPermissionKeys.DeleteCompletionDocTemplates, UserPermissionState.Deny));
|
|
|
|
var result = await Service(data).GetEffectivePermissionsAsync(Caller("u1"), CancellationToken.None);
|
|
|
|
result.Value!.Permissions.Should().Equal(TeamPermissionKeys.All);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Keys_Match_What_The_Write_Path_Enforces()
|
|
{
|
|
var data = new FakeUsers().Add(
|
|
"u1", "Dispatcher",
|
|
(TeamPermissionKeys.CreateCompletionDocTemplates, UserPermissionState.Allow),
|
|
(TeamPermissionKeys.CreateSites, UserPermissionState.Deny));
|
|
var policy = new TeamPermissionPolicy();
|
|
var user = await data.GetUserAsync("u1", CancellationToken.None);
|
|
|
|
var result = await Service(data).GetEffectivePermissionsAsync(Caller("u1"), CancellationToken.None);
|
|
|
|
result.Value!.Permissions.Should().Equal(
|
|
TeamPermissionKeys.All.Where(key => policy.IsAllowed(user!.RoleName, key, user.Overrides)));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Reads_Only_The_Caller_Identified_By_The_Token()
|
|
{
|
|
var data = new FakeUsers()
|
|
.Add("u1", "Dispatcher")
|
|
.Add("u2", "Dispatcher", (TeamPermissionKeys.DeleteWorkOrders, UserPermissionState.Allow));
|
|
using var cancellation = new CancellationTokenSource();
|
|
|
|
var result = await Service(data).GetEffectivePermissionsAsync(Caller("u1"), cancellation.Token);
|
|
|
|
result.Value!.Permissions.Should().NotContain(TeamPermissionKeys.DeleteWorkOrders);
|
|
data.RequestedUserIds.Should().Equal("u1");
|
|
data.LastToken.Should().Be(cancellation.Token);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Unauthenticated_Caller_Is_Rejected_Before_Data_Access()
|
|
{
|
|
var data = new FakeUsers().Add("u1", "Admin");
|
|
var anonymous = new ClaimsPrincipal(new ClaimsIdentity(
|
|
new[] { new Claim(ClaimTypes.NameIdentifier, "u1") }));
|
|
|
|
var result = await Service(data).GetEffectivePermissionsAsync(anonymous, CancellationToken.None);
|
|
|
|
result.Status.Should().Be(TeamPermissionResultStatus.Unauthorized);
|
|
data.RequestedUserIds.Should().BeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Token_Without_A_User_Id_Is_Rejected_Before_Data_Access()
|
|
{
|
|
var data = new FakeUsers().Add("u1", "Admin");
|
|
var noId = new ClaimsPrincipal(new ClaimsIdentity(
|
|
new[] { new Claim(ClaimTypes.Role, "Admin") }, "test"));
|
|
|
|
var result = await Service(data).GetEffectivePermissionsAsync(noId, CancellationToken.None);
|
|
|
|
result.Status.Should().Be(TeamPermissionResultStatus.Unauthorized);
|
|
data.RequestedUserIds.Should().BeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Token_For_A_Removed_User_Is_Rejected()
|
|
{
|
|
var data = new FakeUsers();
|
|
|
|
var result = await Service(data).GetEffectivePermissionsAsync(Caller("gone"), CancellationToken.None);
|
|
|
|
result.Status.Should().Be(TeamPermissionResultStatus.Unauthorized);
|
|
result.Value.Should().BeNull();
|
|
}
|
|
|
|
private static TeamPermissionService Service(FakeUsers data) =>
|
|
new(data, new TeamPermissionPolicy());
|
|
|
|
private static ClaimsPrincipal Caller(string userId) =>
|
|
new(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) }, "test"));
|
|
|
|
private sealed class FakeUsers : ITeamPermissionOverrideDataService
|
|
{
|
|
private readonly Dictionary<string, TeamPermissionUserData> _users = new();
|
|
|
|
public List<string> RequestedUserIds { get; } = new();
|
|
public CancellationToken LastToken { get; private set; }
|
|
|
|
public FakeUsers Add(string userId, string role, params (string Key, UserPermissionState State)[] overrides)
|
|
{
|
|
_users[userId] = new TeamPermissionUserData
|
|
{
|
|
UserId = userId,
|
|
RoleName = role,
|
|
Overrides = overrides.ToDictionary(o => o.Key, o => o.State, StringComparer.OrdinalIgnoreCase)
|
|
};
|
|
return this;
|
|
}
|
|
|
|
public Task<TeamPermissionUserData?> GetUserAsync(string userId, CancellationToken cancellationToken)
|
|
{
|
|
RequestedUserIds.Add(userId);
|
|
LastToken = cancellationToken;
|
|
return Task.FromResult(_users.GetValueOrDefault(userId));
|
|
}
|
|
|
|
public Task SetOverrideAsync(string userId, string permissionKey, UserPermissionState state, CancellationToken cancellationToken) =>
|
|
throw new InvalidOperationException("Reading permissions must not write.");
|
|
|
|
public Task SetOverridesAsync(string userId, IReadOnlyDictionary<string, UserPermissionState> overrides, CancellationToken cancellationToken) =>
|
|
throw new InvalidOperationException("Reading permissions must not write.");
|
|
|
|
public Task ClearOverridesAsync(string userId, CancellationToken cancellationToken) =>
|
|
throw new InvalidOperationException("Reading permissions must not write.");
|
|
}
|
|
}
|