feat(auth)!: MxGateway canonical dashboard roles — Admin→Administrator (Task 1.7)
Standardize the dashboard role VALUE on the canonical six: Admin→Administrator (Viewer unchanged). Pure value rename via DashboardRoles.Admin constant + appsettings GroupToRole; the GatewayOptionsValidator allowed-set/message track the constant so they now require 'Administrator' or 'Viewer'. Enforcement is unchanged — Administrator authorizes exactly what Admin did. Dashboard roles are derived at login from LDAP groups via GroupToRole and are never persisted to the SQLite auth store, so no DB migration/seed change. UNTOUCHED: the separate gRPC API-key scope GatewayScopes.Admin = "admin" (lowercase) and every "admin" scope literal — a distinct data-plane system.
This commit is contained in:
@@ -67,6 +67,12 @@ GLAuth config — it must be provisioned before dashboard authn or the
|
||||
LDAP live tests work. See [Provisioning the GwAdmin
|
||||
group](#provisioning-the-gwadmin-group) below.
|
||||
|
||||
> **Dashboard role value (Task 1.7):** the LDAP `GwAdmin` group now maps to
|
||||
> the canonical dashboard role **`Administrator`** (was `Admin`); `GwReader`
|
||||
> maps to `Viewer`. This is a pure value rename via
|
||||
> `MxGateway:Dashboard:GroupToRole` — same operations are authorized. (This
|
||||
> dashboard role is distinct from the lowercase gRPC `admin` *API-key scope*.)
|
||||
|
||||
## Two bind patterns
|
||||
|
||||
### 1. Direct bind (simplest)
|
||||
|
||||
@@ -8,8 +8,10 @@ public static class DashboardRoles
|
||||
{
|
||||
/// <summary>
|
||||
/// Read-write access: API-key CRUD, settings, any state-changing UI.
|
||||
/// Canonical role value (Task 1.7); formerly <c>"Admin"</c> — pure value
|
||||
/// rename, the operations this role authorizes are unchanged.
|
||||
/// </summary>
|
||||
public const string Admin = "Admin";
|
||||
public const string Admin = "Administrator";
|
||||
|
||||
/// <summary>
|
||||
/// Read-only access: all pages render but write affordances are hidden.
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
"RecentSessionLimit": 200,
|
||||
"ShowTagValues": false,
|
||||
"GroupToRole": {
|
||||
"GwAdmin": "Admin",
|
||||
"GwAdmin": "Administrator",
|
||||
"GwReader": "Viewer"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -87,7 +87,7 @@ public sealed class GatewayOptionsTests
|
||||
[InlineData("MxGateway:Events:QueueCapacity", "0", "MxGateway:Events:QueueCapacity must be greater than zero.")]
|
||||
[InlineData("MxGateway:Protocol:MaxGrpcMessageBytes", "0", "MxGateway:Protocol:MaxGrpcMessageBytes must be between")]
|
||||
[InlineData("MxGateway:Authentication:PepperSecretName", "", "MxGateway:Authentication:PepperSecretName is required")]
|
||||
[InlineData("MxGateway:Dashboard:GroupToRole:GwAdmin", "Sysadmin", "MxGateway:Dashboard:GroupToRole['GwAdmin'] must be 'Admin' or 'Viewer'.")]
|
||||
[InlineData("MxGateway:Dashboard:GroupToRole:GwAdmin", "Sysadmin", "MxGateway:Dashboard:GroupToRole['GwAdmin'] must be 'Administrator' or 'Viewer'.")]
|
||||
public void Validation_InvalidConfiguration_FailsClearly(string key, string value, string expectedFailure)
|
||||
{
|
||||
OptionsValidationException exception = Assert.Throws<OptionsValidationException>(() =>
|
||||
|
||||
@@ -85,6 +85,41 @@ public sealed class DashboardGroupRoleMapperTests
|
||||
Assert.Empty(result.Roles);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Task 1.7 (canonical roles): an LDAP user in the admin group must resolve
|
||||
/// to the canonical role value <c>"Administrator"</c> (not the legacy
|
||||
/// <c>"Admin"</c>), and the reader group to <c>"Viewer"</c>. Asserted with
|
||||
/// string LITERALS — independent of <see cref="DashboardRoles"/> — so a
|
||||
/// regression on the constant's value is caught here.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task MapAsync_AdminGroup_ResolvesToCanonicalAdministratorValue()
|
||||
{
|
||||
DashboardGroupRoleMapper mapper = CreateMapper(StandardMapping());
|
||||
|
||||
GroupRoleMapping<string> adminResult = await mapper.MapAsync(["GwAdmin"], CancellationToken.None);
|
||||
GroupRoleMapping<string> readerResult = await mapper.MapAsync(["GwReader"], CancellationToken.None);
|
||||
|
||||
Assert.Equal("Administrator", Assert.Single(adminResult.Roles));
|
||||
Assert.Equal("Viewer", Assert.Single(readerResult.Roles));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Task 1.7: the canonical admin value (<c>"Administrator"</c>) passes the
|
||||
/// admin-only gate while a <c>"Viewer"</c> fails it — asserted with literals,
|
||||
/// proving enforcement is bound to the new value and the legacy <c>"Admin"</c>
|
||||
/// string is no longer what authorizes admin actions.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AdminOnly_AuthorizesCanonicalAdministratorButNotViewer()
|
||||
{
|
||||
IReadOnlyList<string> adminGate = DashboardAuthorizationRequirement.AdminOnly.RequiredRoles;
|
||||
|
||||
Assert.Contains("Administrator", adminGate);
|
||||
Assert.DoesNotContain("Admin", adminGate);
|
||||
Assert.DoesNotContain("Viewer", adminGate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the extracted shared helper is the single source of truth: it
|
||||
/// produces the same roles the mapper does for the same inputs.
|
||||
|
||||
@@ -197,7 +197,7 @@ public sealed class GatewayApplicationTests
|
||||
[InlineData(
|
||||
"MxGateway:Dashboard:GroupToRole:GwAdmin",
|
||||
"BogusRole",
|
||||
"MxGateway:Dashboard:GroupToRole['GwAdmin'] must be 'Admin' or 'Viewer'.")]
|
||||
"MxGateway:Dashboard:GroupToRole['GwAdmin'] must be 'Administrator' or 'Viewer'.")]
|
||||
[InlineData(
|
||||
"MxGateway:Ldap:AllowInsecure",
|
||||
"false",
|
||||
|
||||
Reference in New Issue
Block a user