mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 06:03:12 +00:00
feat(permissions): add team member permission policy foundation (SH-327)
This commit is contained in:
parent
d204536215
commit
d6c3cd2e24
10 changed files with 4529 additions and 0 deletions
|
|
@ -225,6 +225,32 @@ namespace Data.SeaHavenIndustries
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey(c => c.AreaId)
|
.HasForeignKey(c => c.AreaId)
|
||||||
.OnDelete(DeleteBehavior.Restrict);
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
// Per-person team permission overrides. Composite primary key
|
||||||
|
// (UserId + PermissionKey) plus an explicitly named unique composite
|
||||||
|
// index; missing rows behave as Unset.
|
||||||
|
builder.Entity<UserPermissionOverride>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasKey(o => new { o.UserId, o.PermissionKey });
|
||||||
|
|
||||||
|
entity.Property(o => o.UserId)
|
||||||
|
.HasMaxLength(450);
|
||||||
|
|
||||||
|
entity.Property(o => o.PermissionKey)
|
||||||
|
.HasMaxLength(64);
|
||||||
|
|
||||||
|
entity.Property(o => o.State)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
entity.HasOne(o => o.User)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(o => o.UserId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
entity.HasIndex(o => new { o.UserId, o.PermissionKey })
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("IX_UserPermissionOverrides_UserId_PermissionKey");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
public DbSet<Category> Categories { get; set; }
|
public DbSet<Category> Categories { get; set; }
|
||||||
public DbSet<Locations> Locations { get; set; }
|
public DbSet<Locations> Locations { get; set; }
|
||||||
|
|
@ -279,6 +305,7 @@ namespace Data.SeaHavenIndustries
|
||||||
public DbSet<Department> Departments { get; set; }
|
public DbSet<Department> Departments { get; set; }
|
||||||
public DbSet<JobTitle> JobTitles { get; set; }
|
public DbSet<JobTitle> JobTitles { get; set; }
|
||||||
public DbSet<Region> Regions { get; set; }
|
public DbSet<Region> Regions { get; set; }
|
||||||
|
public DbSet<UserPermissionOverride> UserPermissionOverrides { get; set; }
|
||||||
|
|
||||||
public override int SaveChanges()
|
public override int SaveChanges()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
18
Data.SeaHavenIndustries/Auth/UserPermissionOverride.cs
Normal file
18
Data.SeaHavenIndustries/Auth/UserPermissionOverride.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
using Data.SeaHavenIndustries.Enums;
|
||||||
|
|
||||||
|
namespace Data.SeaHavenIndustries
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Per-person team permission override, keyed by user and canonical
|
||||||
|
/// permission key. Explicit Allow/Deny rows take precedence over role defaults;
|
||||||
|
/// <see cref="UserPermissionState.Unset"/> (and a missing row) falls back to
|
||||||
|
/// the role default.
|
||||||
|
/// </summary>
|
||||||
|
public class UserPermissionOverride
|
||||||
|
{
|
||||||
|
public string UserId { get; set; } = null!;
|
||||||
|
public string PermissionKey { get; set; } = null!;
|
||||||
|
public UserPermissionState State { get; set; } = UserPermissionState.Unset;
|
||||||
|
public virtual ApplicationUser? User { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
13
Data.SeaHavenIndustries/Enums/PermissionEnums.cs
Normal file
13
Data.SeaHavenIndustries/Enums/PermissionEnums.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
namespace Data.SeaHavenIndustries.Enums
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Tri-state state of a per-person team permission override.
|
||||||
|
/// A missing row behaves the same as <see cref="Unset"/>.
|
||||||
|
/// </summary>
|
||||||
|
public enum UserPermissionState
|
||||||
|
{
|
||||||
|
Unset = 0,
|
||||||
|
Allow = 1,
|
||||||
|
Deny = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
3966
Data.SeaHavenIndustries/Migrations/20260916201532_SH327_UserPermissionOverrides.Designer.cs
generated
Normal file
3966
Data.SeaHavenIndustries/Migrations/20260916201532_SH327_UserPermissionOverrides.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,46 @@
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Data.SeaHavenIndustries.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class SH327_UserPermissionOverrides : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "UserPermissionOverrides",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
UserId = table.Column<string>(type: "nvarchar(450)", maxLength: 450, nullable: false),
|
||||||
|
PermissionKey = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||||
|
State = table.Column<int>(type: "int", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_UserPermissionOverrides", x => new { x.UserId, x.PermissionKey });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_UserPermissionOverrides_AspNetUsers_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "AspNetUsers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_UserPermissionOverrides_UserId_PermissionKey",
|
||||||
|
table: "UserPermissionOverrides",
|
||||||
|
columns: new[] { "UserId", "PermissionKey" },
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "UserPermissionOverrides");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2072,6 +2072,28 @@ namespace Data.SeaHavenIndustries.Migrations
|
||||||
b.ToTable("Trades");
|
b.ToTable("Trades");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Data.SeaHavenIndustries.UserPermissionOverride", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasMaxLength(450)
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("PermissionKey")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
|
b.Property<int>("State")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "PermissionKey");
|
||||||
|
|
||||||
|
b.HasIndex("UserId", "PermissionKey")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("IX_UserPermissionOverrides_UserId_PermissionKey");
|
||||||
|
|
||||||
|
b.ToTable("UserPermissionOverrides");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Data.SeaHavenIndustries.Vendor", b =>
|
modelBuilder.Entity("Data.SeaHavenIndustries.Vendor", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
|
|
@ -3616,6 +3638,17 @@ namespace Data.SeaHavenIndustries.Migrations
|
||||||
b.Navigation("POC");
|
b.Navigation("POC");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Data.SeaHavenIndustries.UserPermissionOverride", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Data.SeaHavenIndustries.ApplicationUser", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Data.SeaHavenIndustries.Vendor", b =>
|
modelBuilder.Entity("Data.SeaHavenIndustries.Vendor", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Data.SeaHavenIndustries.VendorCompany", "Company")
|
b.HasOne("Data.SeaHavenIndustries.VendorCompany", "Company")
|
||||||
|
|
|
||||||
248
SeaHaven.Services.Tests/TeamPermissionPolicyTests.cs
Normal file
248
SeaHaven.Services.Tests/TeamPermissionPolicyTests.cs
Normal file
|
|
@ -0,0 +1,248 @@
|
||||||
|
using Data.SeaHavenIndustries.Enums;
|
||||||
|
using FluentAssertions;
|
||||||
|
using SeaHaven.Services.Constants;
|
||||||
|
using SeaHaven.Services.Implementation;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace SeaHaven.Services.Tests;
|
||||||
|
|
||||||
|
public class TeamPermissionPolicyTests
|
||||||
|
{
|
||||||
|
private static readonly string[] DispatcherKeys =
|
||||||
|
{
|
||||||
|
TeamPermissionKeys.CreateVendors,
|
||||||
|
TeamPermissionKeys.EditVendors,
|
||||||
|
TeamPermissionKeys.DeactivateVendors,
|
||||||
|
TeamPermissionKeys.CreateSites,
|
||||||
|
TeamPermissionKeys.EditSites,
|
||||||
|
TeamPermissionKeys.CreateWorkOrders,
|
||||||
|
TeamPermissionKeys.EditOthersWorkOrders,
|
||||||
|
TeamPermissionKeys.CancelWorkOrders,
|
||||||
|
TeamPermissionKeys.RequestUplifts,
|
||||||
|
TeamPermissionKeys.AutoApproveUplifts
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly string[] SchedulerOnlyKeys =
|
||||||
|
{
|
||||||
|
TeamPermissionKeys.DeleteSites,
|
||||||
|
TeamPermissionKeys.CreateServices,
|
||||||
|
TeamPermissionKeys.EditServices,
|
||||||
|
TeamPermissionKeys.CreateCompletionDocTemplates,
|
||||||
|
TeamPermissionKeys.EditCompletionDocTemplates,
|
||||||
|
TeamPermissionKeys.ViewAllDispatchersOnDashboard
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly string[] AdminOnlyKeys =
|
||||||
|
{
|
||||||
|
TeamPermissionKeys.ManageTeamMembers,
|
||||||
|
TeamPermissionKeys.ChangeTeamMemberRole,
|
||||||
|
TeamPermissionKeys.DeactivateServices,
|
||||||
|
TeamPermissionKeys.DeleteCompletionDocTemplates,
|
||||||
|
TeamPermissionKeys.DeleteWorkOrders,
|
||||||
|
TeamPermissionKeys.ReviewUplifts
|
||||||
|
};
|
||||||
|
|
||||||
|
private static IReadOnlyDictionary<string, UserPermissionState> Overrides(
|
||||||
|
string key, UserPermissionState state) =>
|
||||||
|
new Dictionary<string, UserPermissionState>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
[key] = state
|
||||||
|
};
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Registry_Exposes_Exactly_The_22_Prototype_Keys()
|
||||||
|
{
|
||||||
|
TeamPermissionKeys.All.Should().HaveCount(22);
|
||||||
|
TeamPermissionKeys.KnownKeys.Should().HaveCount(22);
|
||||||
|
TeamPermissionKeys.All.Should().OnlyHaveUniqueItems();
|
||||||
|
TeamPermissionKeys.All.Should().OnlyContain(key => !string.IsNullOrWhiteSpace(key));
|
||||||
|
TeamPermissionKeys.All.Should().BeEquivalentTo(new[]
|
||||||
|
{
|
||||||
|
"manageTeamMembers",
|
||||||
|
"changeTeamMemberRole",
|
||||||
|
"createVendors",
|
||||||
|
"editVendors",
|
||||||
|
"deactivateVendors",
|
||||||
|
"createSites",
|
||||||
|
"editSites",
|
||||||
|
"deleteSites",
|
||||||
|
"createServices",
|
||||||
|
"editServices",
|
||||||
|
"deactivateServices",
|
||||||
|
"createCompletionDocTemplates",
|
||||||
|
"editCompletionDocTemplates",
|
||||||
|
"deleteCompletionDocTemplates",
|
||||||
|
"createWorkOrders",
|
||||||
|
"editOthersWorkOrders",
|
||||||
|
"cancelWorkOrders",
|
||||||
|
"deleteWorkOrders",
|
||||||
|
"requestUplifts",
|
||||||
|
"autoApproveUplifts",
|
||||||
|
"reviewUplifts",
|
||||||
|
"viewAllDispatchersOnDashboard"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Registry_IsKnown_Is_Case_Insensitive_And_Rejects_Unknown_Keys()
|
||||||
|
{
|
||||||
|
TeamPermissionKeys.IsKnown("CREATEVENDORS").Should().BeTrue();
|
||||||
|
TeamPermissionKeys.IsKnown("autoApproveUplifts").Should().BeTrue();
|
||||||
|
TeamPermissionKeys.IsKnown("nope.unknown").Should().BeFalse();
|
||||||
|
TeamPermissionKeys.IsKnown("").Should().BeFalse();
|
||||||
|
TeamPermissionKeys.IsKnown(null).Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Dispatcher_Gets_Exactly_Prototype_Defaults()
|
||||||
|
{
|
||||||
|
var policy = new TeamPermissionPolicy();
|
||||||
|
|
||||||
|
foreach (var key in TeamPermissionKeys.All)
|
||||||
|
{
|
||||||
|
var expected = DispatcherKeys.Contains(key, StringComparer.Ordinal);
|
||||||
|
policy.IsAllowed("Dispatcher", key).Should().Be(expected,
|
||||||
|
$"Dispatcher default for '{key}' should be {expected}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Scheduler_Gets_Exactly_Prototype_Defaults()
|
||||||
|
{
|
||||||
|
var policy = new TeamPermissionPolicy();
|
||||||
|
|
||||||
|
foreach (var key in TeamPermissionKeys.All)
|
||||||
|
{
|
||||||
|
var expected = SchedulerOnlyKeys.Contains(key, StringComparer.Ordinal)
|
||||||
|
|| DispatcherKeys.Contains(key, StringComparer.Ordinal)
|
||||||
|
&& key is not TeamPermissionKeys.RequestUplifts
|
||||||
|
&& key is not TeamPermissionKeys.AutoApproveUplifts;
|
||||||
|
|
||||||
|
policy.IsAllowed("Scheduler", key).Should().Be(expected,
|
||||||
|
$"Scheduler default for '{key}' should be {expected}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Admin_Has_Effective_Access_To_Every_Key()
|
||||||
|
{
|
||||||
|
var policy = new TeamPermissionPolicy();
|
||||||
|
|
||||||
|
foreach (var key in TeamPermissionKeys.All)
|
||||||
|
policy.IsAllowed("Admin", key).Should().BeTrue($"Admin should hold '{key}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Role_Recognition_Is_Case_Insensitive()
|
||||||
|
{
|
||||||
|
var policy = new TeamPermissionPolicy();
|
||||||
|
|
||||||
|
policy.IsAllowed("dIsPaTcHeR", TeamPermissionKeys.CreateVendors).Should().BeTrue();
|
||||||
|
policy.IsAllowed("SCHEDULER", TeamPermissionKeys.DeleteSites).Should().BeTrue();
|
||||||
|
policy.IsAllowed("admin", TeamPermissionKeys.ReviewUplifts).Should().BeTrue();
|
||||||
|
|
||||||
|
policy.IsAllowed("dIsPaTcHeR", TeamPermissionKeys.DeleteSites).Should().BeFalse();
|
||||||
|
policy.IsAllowed("SCHEDULER", TeamPermissionKeys.AutoApproveUplifts).Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Manager")]
|
||||||
|
[InlineData("OfficeAdmin")]
|
||||||
|
[InlineData("")]
|
||||||
|
[InlineData(null)]
|
||||||
|
public void Unknown_And_Legacy_Roles_Receive_No_Permissions(string? role)
|
||||||
|
{
|
||||||
|
var policy = new TeamPermissionPolicy();
|
||||||
|
|
||||||
|
foreach (var key in TeamPermissionKeys.All)
|
||||||
|
policy.IsAllowed(role, key).Should().BeFalse(
|
||||||
|
$"unknown/legacy role '{role}' must not hold '{key}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Unknown_Role_Gains_Nothing_Even_With_Allow_Overrides()
|
||||||
|
{
|
||||||
|
var policy = new TeamPermissionPolicy();
|
||||||
|
|
||||||
|
foreach (var key in TeamPermissionKeys.All)
|
||||||
|
policy.IsAllowed("Manager", key, Overrides(key, UserPermissionState.Allow))
|
||||||
|
.Should().BeFalse($"an unknown role must not be elevated via '{key}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Allow_Override_Grants_Key_Outside_Role_Default()
|
||||||
|
{
|
||||||
|
var policy = new TeamPermissionPolicy();
|
||||||
|
|
||||||
|
policy.IsAllowed("Scheduler", TeamPermissionKeys.RequestUplifts)
|
||||||
|
.Should().BeFalse();
|
||||||
|
policy.IsAllowed(
|
||||||
|
"Scheduler",
|
||||||
|
TeamPermissionKeys.RequestUplifts,
|
||||||
|
Overrides(TeamPermissionKeys.RequestUplifts, UserPermissionState.Allow))
|
||||||
|
.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Deny_Override_Removes_Key_Inside_Role_Default()
|
||||||
|
{
|
||||||
|
var policy = new TeamPermissionPolicy();
|
||||||
|
|
||||||
|
policy.IsAllowed("Dispatcher", TeamPermissionKeys.CreateVendors)
|
||||||
|
.Should().BeTrue();
|
||||||
|
policy.IsAllowed(
|
||||||
|
"Dispatcher",
|
||||||
|
TeamPermissionKeys.CreateVendors,
|
||||||
|
Overrides(TeamPermissionKeys.CreateVendors, UserPermissionState.Deny))
|
||||||
|
.Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Unset_Override_And_Missing_Row_Fall_Back_To_Role_Default()
|
||||||
|
{
|
||||||
|
var policy = new TeamPermissionPolicy();
|
||||||
|
|
||||||
|
policy.IsAllowed(
|
||||||
|
"Dispatcher",
|
||||||
|
TeamPermissionKeys.CreateWorkOrders,
|
||||||
|
Overrides(TeamPermissionKeys.CreateWorkOrders, UserPermissionState.Unset))
|
||||||
|
.Should().BeTrue();
|
||||||
|
|
||||||
|
policy.IsAllowed(
|
||||||
|
"Scheduler",
|
||||||
|
TeamPermissionKeys.RequestUplifts,
|
||||||
|
Overrides(TeamPermissionKeys.RequestUplifts, UserPermissionState.Unset))
|
||||||
|
.Should().BeFalse();
|
||||||
|
|
||||||
|
policy.IsAllowed(
|
||||||
|
"Scheduler",
|
||||||
|
TeamPermissionKeys.RequestUplifts,
|
||||||
|
new Dictionary<string, UserPermissionState>())
|
||||||
|
.Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Admin_Is_Immune_To_Overrides()
|
||||||
|
{
|
||||||
|
var policy = new TeamPermissionPolicy();
|
||||||
|
|
||||||
|
foreach (var key in TeamPermissionKeys.All)
|
||||||
|
{
|
||||||
|
policy.IsAllowed("Admin", key, Overrides(key, UserPermissionState.Deny))
|
||||||
|
.Should().BeTrue($"Admin must keep '{key}' despite a Deny override");
|
||||||
|
policy.IsAllowed("ADMIN", key, Overrides(key, UserPermissionState.Unset))
|
||||||
|
.Should().BeTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Unknown_Permission_Key_Is_Denied_For_Every_Role()
|
||||||
|
{
|
||||||
|
var policy = new TeamPermissionPolicy();
|
||||||
|
|
||||||
|
policy.IsAllowed("Admin", "nope.unknown").Should().BeFalse();
|
||||||
|
policy.IsAllowed("Dispatcher", "nope.unknown").Should().BeFalse();
|
||||||
|
policy.IsAllowed("Admin", "nope.unknown", Overrides("nope.unknown", UserPermissionState.Allow))
|
||||||
|
.Should().BeFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
67
SeaHaven.Services/Constants/TeamPermissionKeys.cs
Normal file
67
SeaHaven.Services/Constants/TeamPermissionKeys.cs
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
namespace SeaHaven.Services.Constants
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Canonical, immutable registry of team-members permission keys.
|
||||||
|
/// Exactly the 22 approved prototype keys; stored overrides must reference
|
||||||
|
/// these keys. Keys are stable identifiers and must never be renamed.
|
||||||
|
/// </summary>
|
||||||
|
public static class TeamPermissionKeys
|
||||||
|
{
|
||||||
|
public const string ManageTeamMembers = "manageTeamMembers";
|
||||||
|
public const string ChangeTeamMemberRole = "changeTeamMemberRole";
|
||||||
|
public const string CreateVendors = "createVendors";
|
||||||
|
public const string EditVendors = "editVendors";
|
||||||
|
public const string DeactivateVendors = "deactivateVendors";
|
||||||
|
public const string CreateSites = "createSites";
|
||||||
|
public const string EditSites = "editSites";
|
||||||
|
public const string DeleteSites = "deleteSites";
|
||||||
|
public const string CreateServices = "createServices";
|
||||||
|
public const string EditServices = "editServices";
|
||||||
|
public const string DeactivateServices = "deactivateServices";
|
||||||
|
public const string CreateCompletionDocTemplates = "createCompletionDocTemplates";
|
||||||
|
public const string EditCompletionDocTemplates = "editCompletionDocTemplates";
|
||||||
|
public const string DeleteCompletionDocTemplates = "deleteCompletionDocTemplates";
|
||||||
|
public const string CreateWorkOrders = "createWorkOrders";
|
||||||
|
public const string EditOthersWorkOrders = "editOthersWorkOrders";
|
||||||
|
public const string CancelWorkOrders = "cancelWorkOrders";
|
||||||
|
public const string DeleteWorkOrders = "deleteWorkOrders";
|
||||||
|
public const string RequestUplifts = "requestUplifts";
|
||||||
|
public const string AutoApproveUplifts = "autoApproveUplifts";
|
||||||
|
public const string ReviewUplifts = "reviewUplifts";
|
||||||
|
public const string ViewAllDispatchersOnDashboard = "viewAllDispatchersOnDashboard";
|
||||||
|
|
||||||
|
private static readonly IReadOnlyList<string> AllKeys = Array.AsReadOnly(new[]
|
||||||
|
{
|
||||||
|
ManageTeamMembers,
|
||||||
|
ChangeTeamMemberRole,
|
||||||
|
CreateVendors,
|
||||||
|
EditVendors,
|
||||||
|
DeactivateVendors,
|
||||||
|
CreateSites,
|
||||||
|
EditSites,
|
||||||
|
DeleteSites,
|
||||||
|
CreateServices,
|
||||||
|
EditServices,
|
||||||
|
DeactivateServices,
|
||||||
|
CreateCompletionDocTemplates,
|
||||||
|
EditCompletionDocTemplates,
|
||||||
|
DeleteCompletionDocTemplates,
|
||||||
|
CreateWorkOrders,
|
||||||
|
EditOthersWorkOrders,
|
||||||
|
CancelWorkOrders,
|
||||||
|
DeleteWorkOrders,
|
||||||
|
RequestUplifts,
|
||||||
|
AutoApproveUplifts,
|
||||||
|
ReviewUplifts,
|
||||||
|
ViewAllDispatchersOnDashboard
|
||||||
|
});
|
||||||
|
|
||||||
|
public static IReadOnlyList<string> All => AllKeys;
|
||||||
|
|
||||||
|
public static IReadOnlySet<string> KnownKeys { get; } =
|
||||||
|
new HashSet<string>(AllKeys, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public static bool IsKnown(string? permissionKey) =>
|
||||||
|
!string.IsNullOrWhiteSpace(permissionKey) && KnownKeys.Contains(permissionKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
88
SeaHaven.Services/Implementation/TeamPermissionPolicy.cs
Normal file
88
SeaHaven.Services/Implementation/TeamPermissionPolicy.cs
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
using Data.SeaHavenIndustries.Enums;
|
||||||
|
using SeaHaven.Services.Constants;
|
||||||
|
using SeaHaven.Services.Interfaces;
|
||||||
|
|
||||||
|
namespace SeaHaven.Services.Implementation
|
||||||
|
{
|
||||||
|
public sealed class TeamPermissionPolicy : ITeamPermissionPolicy
|
||||||
|
{
|
||||||
|
private static readonly StringComparer KeyComparer = StringComparer.OrdinalIgnoreCase;
|
||||||
|
|
||||||
|
private static readonly HashSet<string> DispatcherDefaults = new(KeyComparer)
|
||||||
|
{
|
||||||
|
TeamPermissionKeys.CreateVendors,
|
||||||
|
TeamPermissionKeys.EditVendors,
|
||||||
|
TeamPermissionKeys.DeactivateVendors,
|
||||||
|
TeamPermissionKeys.CreateSites,
|
||||||
|
TeamPermissionKeys.EditSites,
|
||||||
|
TeamPermissionKeys.CreateWorkOrders,
|
||||||
|
TeamPermissionKeys.EditOthersWorkOrders,
|
||||||
|
TeamPermissionKeys.CancelWorkOrders,
|
||||||
|
TeamPermissionKeys.RequestUplifts,
|
||||||
|
TeamPermissionKeys.AutoApproveUplifts
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly HashSet<string> SchedulerDefaults = new(
|
||||||
|
DispatcherDefaults
|
||||||
|
.Except(new[]
|
||||||
|
{
|
||||||
|
TeamPermissionKeys.RequestUplifts,
|
||||||
|
TeamPermissionKeys.AutoApproveUplifts
|
||||||
|
}, KeyComparer)
|
||||||
|
.Concat(new[]
|
||||||
|
{
|
||||||
|
TeamPermissionKeys.DeleteSites,
|
||||||
|
TeamPermissionKeys.CreateServices,
|
||||||
|
TeamPermissionKeys.EditServices,
|
||||||
|
TeamPermissionKeys.CreateCompletionDocTemplates,
|
||||||
|
TeamPermissionKeys.EditCompletionDocTemplates,
|
||||||
|
TeamPermissionKeys.ViewAllDispatchersOnDashboard
|
||||||
|
}), KeyComparer);
|
||||||
|
|
||||||
|
public bool IsAllowed(
|
||||||
|
string? roleName,
|
||||||
|
string permissionKey,
|
||||||
|
IReadOnlyDictionary<string, UserPermissionState>? overrides = null)
|
||||||
|
{
|
||||||
|
if (!TeamPermissionKeys.IsKnown(permissionKey))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (IsAdmin(roleName))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var defaults = ResolveRoleDefaults(roleName);
|
||||||
|
if (defaults is null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (overrides is not null
|
||||||
|
&& overrides.TryGetValue(permissionKey, out var state))
|
||||||
|
{
|
||||||
|
return state switch
|
||||||
|
{
|
||||||
|
UserPermissionState.Allow => true,
|
||||||
|
UserPermissionState.Deny => false,
|
||||||
|
_ => defaults.Contains(permissionKey)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaults.Contains(permissionKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsAdmin(string? roleName) =>
|
||||||
|
string.Equals(roleName, "Admin", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private static IReadOnlySet<string>? ResolveRoleDefaults(string? roleName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(roleName))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (roleName.Equals("Dispatcher", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return DispatcherDefaults;
|
||||||
|
|
||||||
|
if (roleName.Equals("Scheduler", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return SchedulerDefaults;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
SeaHaven.Services/Interfaces/ITeamPermissionPolicy.cs
Normal file
23
SeaHaven.Services/Interfaces/ITeamPermissionPolicy.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
using Data.SeaHavenIndustries.Enums;
|
||||||
|
|
||||||
|
namespace SeaHaven.Services.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Pure evaluator for team-members permissions. Applies explicit
|
||||||
|
/// per-person overrides on top of role defaults; Admin has effective access
|
||||||
|
/// to every canonical key regardless of stored values. Unknown or legacy
|
||||||
|
/// roles receive no permissions.
|
||||||
|
/// </summary>
|
||||||
|
public interface ITeamPermissionPolicy
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Resolves whether a user in <paramref name="roleName"/> holds
|
||||||
|
/// <paramref name="permissionKey"/>. <paramref name="overrides"/> is the
|
||||||
|
/// person's stored override set (missing key behaves as Unset).
|
||||||
|
/// </summary>
|
||||||
|
bool IsAllowed(
|
||||||
|
string? roleName,
|
||||||
|
string permissionKey,
|
||||||
|
IReadOnlyDictionary<string, UserPermissionState>? overrides = null);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue