b71fbae36e
The rig pointed at the shared 10.100.0.35 GLAuth whose serviceaccount password
was rotated (SEC-36), so central login had been failing ('Authentication service
is misconfigured') and a TEMP DisableLogin workaround was pending. Central nodes
now point at the local redundant pair (scadaproj/infra/glauth-redundant,
host.docker.internal:3893 + FallbackServers :3894), where the dev bind password
is correct — live-gated on the redeployed rig: login OK, primary-kill failover,
sticky preference (bind-count proven), walk-back on backup-kill.
AuthFlowTests factory bound as cn=admin for search-then-bind, but the current
directory grants the search capability only to serviceaccount (admin searches
return 50 Insufficient access) — stale since the GLAuth config evolved; the test
had been skipping on the closed port and failed once anything answered :3893.
Now binds as serviceaccount; AuthFlowTests 5/5 against the pair.
156 lines
5.4 KiB
C#
156 lines
5.4 KiB
C#
using System.Net;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using ZB.MOM.WW.ScadaBridge.CentralUI.Auth;
|
|
using ZB.MOM.WW.ScadaBridge.Security;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.IntegrationTests;
|
|
|
|
/// <summary>
|
|
/// WP-22: Auth flow integration tests.
|
|
/// Tests that require a running LDAP server are marked with Integration trait.
|
|
/// </summary>
|
|
public class AuthFlowTests : IClassFixture<ScadaBridgeWebApplicationFactory>
|
|
{
|
|
private readonly ScadaBridgeWebApplicationFactory _factory;
|
|
|
|
public AuthFlowTests(ScadaBridgeWebApplicationFactory factory)
|
|
{
|
|
_factory = factory;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task LoginEndpoint_WithEmptyCredentials_RedirectsToLoginWithError()
|
|
{
|
|
var client = _factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions
|
|
{
|
|
AllowAutoRedirect = false
|
|
});
|
|
|
|
var content = new FormUrlEncodedContent(new[]
|
|
{
|
|
new KeyValuePair<string, string>("username", ""),
|
|
new KeyValuePair<string, string>("password", "")
|
|
});
|
|
|
|
var response = await client.PostAsync("/auth/login", content);
|
|
|
|
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
|
|
var location = response.Headers.Location?.ToString() ?? "";
|
|
Assert.Contains("/login", location);
|
|
Assert.Contains("error", location, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task LogoutEndpoint_ClearsCookieAndRedirects()
|
|
{
|
|
var client = _factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions
|
|
{
|
|
AllowAutoRedirect = false
|
|
});
|
|
|
|
var response = await client.PostAsync("/auth/logout", null);
|
|
|
|
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
|
|
var location = response.Headers.Location?.ToString() ?? "";
|
|
Assert.Contains("/login", location);
|
|
}
|
|
|
|
[Fact]
|
|
public void JwtTokenService_GenerateAndValidate_RoundTrips()
|
|
{
|
|
using var scope = _factory.Services.CreateScope();
|
|
var jwtService = scope.ServiceProvider.GetRequiredService<JwtTokenService>();
|
|
|
|
var token = jwtService.GenerateToken(
|
|
displayName: "Test User",
|
|
username: "testuser",
|
|
roles: new[] { "Administrator", "Designer" },
|
|
permittedSiteIds: null);
|
|
|
|
Assert.NotNull(token);
|
|
|
|
var principal = jwtService.ValidateToken(token);
|
|
Assert.NotNull(principal);
|
|
|
|
var displayName = principal!.FindFirst(JwtTokenService.DisplayNameClaimType)?.Value;
|
|
var username = principal.FindFirst(JwtTokenService.UsernameClaimType)?.Value;
|
|
var roles = principal.FindAll(JwtTokenService.RoleClaimType).Select(c => c.Value).ToList();
|
|
|
|
Assert.Equal("Test User", displayName);
|
|
Assert.Equal("testuser", username);
|
|
Assert.Contains("Administrator", roles);
|
|
Assert.Contains("Designer", roles);
|
|
}
|
|
|
|
[Fact]
|
|
public void JwtTokenService_WithSiteScopes_IncludesSiteIdClaims()
|
|
{
|
|
using var scope = _factory.Services.CreateScope();
|
|
var jwtService = scope.ServiceProvider.GetRequiredService<JwtTokenService>();
|
|
|
|
var token = jwtService.GenerateToken(
|
|
displayName: "Deployer",
|
|
username: "deployer1",
|
|
roles: new[] { "Deployer" },
|
|
permittedSiteIds: new[] { "1", "3" });
|
|
|
|
var principal = jwtService.ValidateToken(token);
|
|
Assert.NotNull(principal);
|
|
|
|
var siteIds = principal!.FindAll(JwtTokenService.SiteIdClaimType).Select(c => c.Value).ToList();
|
|
Assert.Contains("1", siteIds);
|
|
Assert.Contains("3", siteIds);
|
|
}
|
|
|
|
[Trait("Category", "Integration")]
|
|
[Fact]
|
|
public async Task LoginEndpoint_WithValidLdapCredentials_SetsCookieAndRedirects()
|
|
{
|
|
// Requires a local GLAuth on localhost:3893 — the redundant test pair:
|
|
// cd ~/Desktop/scadaproj/infra/glauth-redundant && docker compose up -d
|
|
// (baseDN dc=zb,dc=local, user passwords "password"; the old infra/docker-compose.yml
|
|
// glauth service no longer exists — LDAP moved out of that stack.)
|
|
if (!await IsLdapAvailableAsync())
|
|
{
|
|
// Skip gracefully if GLAuth not running — not a test failure
|
|
return;
|
|
}
|
|
|
|
var client = _factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions
|
|
{
|
|
AllowAutoRedirect = false
|
|
});
|
|
|
|
var content = new FormUrlEncodedContent(new[]
|
|
{
|
|
new KeyValuePair<string, string>("username", "admin"),
|
|
new KeyValuePair<string, string>("password", "password")
|
|
});
|
|
|
|
var response = await client.PostAsync("/auth/login", content);
|
|
|
|
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
|
|
var location = response.Headers.Location?.ToString() ?? "";
|
|
Assert.Equal("/", location);
|
|
|
|
// Verify auth cookie was set
|
|
var setCookieHeader = response.Headers.GetValues("Set-Cookie").FirstOrDefault();
|
|
Assert.NotNull(setCookieHeader);
|
|
Assert.Contains("ZB.MOM.WW.ScadaBridge.Auth", setCookieHeader);
|
|
}
|
|
|
|
private static async Task<bool> IsLdapAvailableAsync()
|
|
{
|
|
try
|
|
{
|
|
using var tcp = new System.Net.Sockets.TcpClient();
|
|
await tcp.ConnectAsync("localhost", 3893).WaitAsync(TimeSpan.FromSeconds(2));
|
|
return true;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|