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
|
LDAP live tests work. See [Provisioning the GwAdmin
|
||||||
group](#provisioning-the-gwadmin-group) below.
|
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
|
## Two bind patterns
|
||||||
|
|
||||||
### 1. Direct bind (simplest)
|
### 1. Direct bind (simplest)
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ public static class DashboardRoles
|
|||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Read-write access: API-key CRUD, settings, any state-changing UI.
|
/// 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>
|
/// </summary>
|
||||||
public const string Admin = "Admin";
|
public const string Admin = "Administrator";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Read-only access: all pages render but write affordances are hidden.
|
/// Read-only access: all pages render but write affordances are hidden.
|
||||||
|
|||||||
@@ -60,7 +60,7 @@
|
|||||||
"RecentSessionLimit": 200,
|
"RecentSessionLimit": 200,
|
||||||
"ShowTagValues": false,
|
"ShowTagValues": false,
|
||||||
"GroupToRole": {
|
"GroupToRole": {
|
||||||
"GwAdmin": "Admin",
|
"GwAdmin": "Administrator",
|
||||||
"GwReader": "Viewer"
|
"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:Events:QueueCapacity", "0", "MxGateway:Events:QueueCapacity must be greater than zero.")]
|
||||||
[InlineData("MxGateway:Protocol:MaxGrpcMessageBytes", "0", "MxGateway:Protocol:MaxGrpcMessageBytes must be between")]
|
[InlineData("MxGateway:Protocol:MaxGrpcMessageBytes", "0", "MxGateway:Protocol:MaxGrpcMessageBytes must be between")]
|
||||||
[InlineData("MxGateway:Authentication:PepperSecretName", "", "MxGateway:Authentication:PepperSecretName is required")]
|
[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)
|
public void Validation_InvalidConfiguration_FailsClearly(string key, string value, string expectedFailure)
|
||||||
{
|
{
|
||||||
OptionsValidationException exception = Assert.Throws<OptionsValidationException>(() =>
|
OptionsValidationException exception = Assert.Throws<OptionsValidationException>(() =>
|
||||||
|
|||||||
@@ -85,6 +85,41 @@ public sealed class DashboardGroupRoleMapperTests
|
|||||||
Assert.Empty(result.Roles);
|
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>
|
/// <summary>
|
||||||
/// Verifies the extracted shared helper is the single source of truth: it
|
/// Verifies the extracted shared helper is the single source of truth: it
|
||||||
/// produces the same roles the mapper does for the same inputs.
|
/// produces the same roles the mapper does for the same inputs.
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ public sealed class GatewayApplicationTests
|
|||||||
[InlineData(
|
[InlineData(
|
||||||
"MxGateway:Dashboard:GroupToRole:GwAdmin",
|
"MxGateway:Dashboard:GroupToRole:GwAdmin",
|
||||||
"BogusRole",
|
"BogusRole",
|
||||||
"MxGateway:Dashboard:GroupToRole['GwAdmin'] must be 'Admin' or 'Viewer'.")]
|
"MxGateway:Dashboard:GroupToRole['GwAdmin'] must be 'Administrator' or 'Viewer'.")]
|
||||||
[InlineData(
|
[InlineData(
|
||||||
"MxGateway:Ldap:AllowInsecure",
|
"MxGateway:Ldap:AllowInsecure",
|
||||||
"false",
|
"false",
|
||||||
|
|||||||
Reference in New Issue
Block a user