fix(auth): OtOpcUa 1.2 review fixes — startup insecure-transport guard + Ldaps in prod overlays, test fidelity, 0.1.1 pin
This commit is contained in:
@@ -59,6 +59,11 @@ public sealed class AuthEndpointsIntegrationTests : IAsyncLifetime
|
||||
["Security:Jwt:SigningKey"] = "test-signing-key-with-at-least-32-bytes-of-utf8-content",
|
||||
["Security:Jwt:Issuer"] = "otopcua-test",
|
||||
["Security:Jwt:Audience"] = "otopcua-test",
|
||||
// GroupToRole baseline bound onto LdapOptions: the production
|
||||
// OtOpcUaGroupRoleMapper resolves "ConfigViewer" from the LDAP group
|
||||
// "ReadOnly". This exercises the real mapper path — the stub no longer
|
||||
// pre-populates roles, so ConfigViewer can only come from the mapper.
|
||||
["Security:Ldap:GroupToRole:ReadOnly"] = "ConfigViewer",
|
||||
}).Build();
|
||||
services.AddOtOpcUaAuth(configuration);
|
||||
services.AddSingleton<ILdapAuthService, StubLdapAuthService>();
|
||||
@@ -187,8 +192,9 @@ public sealed class AuthEndpointsIntegrationTests : IAsyncLifetime
|
||||
[Fact]
|
||||
public async Task Login_merges_db_role_grant_into_claims()
|
||||
{
|
||||
// StubLdapAuthService returns Groups ["ReadOnly"], baseline Roles ["ConfigViewer"].
|
||||
// A system-wide row maps "ReadOnly" → FleetAdmin, so the merged set is both.
|
||||
// StubLdapAuthService returns Groups ["ReadOnly"] with empty Roles (the real production
|
||||
// shape). The mapper resolves the appsettings baseline "ReadOnly" → ConfigViewer, then a
|
||||
// system-wide DB row maps "ReadOnly" → FleetAdmin, so the merged set is both.
|
||||
_roleMappings.Rows.Add(new LdapGroupRoleMapping
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
@@ -214,18 +220,24 @@ public sealed class AuthEndpointsIntegrationTests : IAsyncLifetime
|
||||
roles.ShouldContain("FleetAdmin"); // DB grant merged in
|
||||
}
|
||||
|
||||
/// <summary>When the DB role-map lookup throws, sign-in still succeeds with the appsettings
|
||||
/// baseline roles — a DB hiccup must never block login.</summary>
|
||||
/// <summary>Fail-closed (review I3): when the role mapper throws on the real production path
|
||||
/// (the auth result carries no pre-resolved roles — roles come only from the mapper), sign-in
|
||||
/// still SUCCEEDS but the user is granted ZERO role claims. They are authenticated (can prove
|
||||
/// identity) yet authorized for nothing role-gated until the mapper recovers — the safe
|
||||
/// fail-closed behaviour, not a fail-open with a stale role set.</summary>
|
||||
[Fact]
|
||||
public async Task Login_when_db_role_map_throws_falls_back_to_baseline_roles()
|
||||
public async Task Login_when_role_mapper_throws_signs_in_with_no_role_claims()
|
||||
{
|
||||
// Simulate a mapper fault on the real path. The whole MapAsync throws (the appsettings
|
||||
// baseline is computed inside the mapper, so it does NOT survive the throw): the login
|
||||
// endpoint falls back to result.Roles, which is empty on the real LDAP path.
|
||||
_roleMappings.Throws = true;
|
||||
|
||||
var client = NewClient();
|
||||
var loginResponse = await client.PostAsJsonAsync("/auth/login",
|
||||
new AuthEndpoints.LoginRequest("alice", "valid-password"), Ct);
|
||||
|
||||
// Login proceeds despite the simulated DB outage.
|
||||
// Login proceeds despite the simulated DB outage — authenticated.
|
||||
loginResponse.StatusCode.ShouldBe(HttpStatusCode.NoContent);
|
||||
|
||||
var tokenReq = new HttpRequestMessage(HttpMethod.Post, "/auth/token");
|
||||
@@ -233,9 +245,10 @@ public sealed class AuthEndpointsIntegrationTests : IAsyncLifetime
|
||||
var tokenResp = await client.SendAsync(tokenReq, Ct);
|
||||
tokenResp.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
|
||||
// No role claims at all — fail closed.
|
||||
var payload = await tokenResp.Content.ReadFromJsonAsync<JsonElement>(Ct);
|
||||
var roles = JwtRoleClaims(payload.GetProperty("token").GetString()!);
|
||||
roles.ShouldContain("ConfigViewer"); // baseline still present
|
||||
roles.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
/// <summary>Extracts the "Role" claim values from a JWT's payload segment.</summary>
|
||||
@@ -330,7 +343,11 @@ public sealed class AuthEndpointsIntegrationTests : IAsyncLifetime
|
||||
DisplayName: "Alice User",
|
||||
Username: username,
|
||||
Groups: ["ReadOnly"],
|
||||
Roles: ["ConfigViewer"],
|
||||
// Roles empty — the real production path returns groups, never roles. Role
|
||||
// resolution is the mapper's job (OtOpcUaGroupRoleMapper applies the
|
||||
// GroupToRole baseline). This proves roles flow through the mapper, not via
|
||||
// pre-population of the auth result.
|
||||
Roles: [],
|
||||
Error: null));
|
||||
return Task.FromResult(new LdapAuthResult(
|
||||
Success: false,
|
||||
|
||||
Reference in New Issue
Block a user