193 lines
7.1 KiB
C#
193 lines
7.1 KiB
C#
using FluentAssertions;
|
|
using System.Diagnostics;
|
|
using System.Text.RegularExpressions;
|
|
using Xunit;
|
|
using YamlDotNet.RepresentationModel;
|
|
|
|
namespace BluejayInfraLint.Tests;
|
|
|
|
[Trait("Category", "AuthentikOidc")]
|
|
public sealed class AuthentikOidcClientRegistrationTests
|
|
{
|
|
private static readonly IReadOnlyList<ServiceClientExpectation> ExpectedClients =
|
|
[
|
|
new("library", "fc-library"),
|
|
new("retail", "fc-retail"),
|
|
new("telephony", "telephony"),
|
|
new("knowledge", "knowledge"),
|
|
new("llmbridge", "fc-llm-bridge"),
|
|
new("mysql", "fc-mysql"),
|
|
new("php", "fc-php"),
|
|
new("signage", "fc-signage"),
|
|
new("media", "fc-media"),
|
|
new("dms", "fc-dms"),
|
|
new("pimanager", "fc-pimanager"),
|
|
new("distribution", "fc-distribution"),
|
|
new("dns", "fc-dns"),
|
|
new("print", "fc-print"),
|
|
new("aistation", "fc-aistation"),
|
|
new("irc", "irc"),
|
|
new("ttsreader", "fc-ttsreader"),
|
|
new("chat", "fc-chat"),
|
|
new("intranet", "intranet"),
|
|
new("remotedesktop", "fc-desktop"),
|
|
new("provisioning", "fc-provisioning"),
|
|
new("scoreboards", "fc-scoreboard"),
|
|
new("mndot", "fc-mndot"),
|
|
new("kiosk", "fc-system"),
|
|
new("mike-bundle", "fc-mike-bundle"),
|
|
new("messageboard", "fc-messageboard"),
|
|
new("menuboard", "fc-menuboard"),
|
|
new("presentations", "fc-presentations"),
|
|
new("segmentdisplay", "fc-segmentdisplay"),
|
|
new("signalcontrol", "fc-signalcontrol"),
|
|
new("worldbuilder", "fc-worldbuilder"),
|
|
new("audit", "fc-audit"),
|
|
new("licensing", "fc-licensing"),
|
|
];
|
|
|
|
public static TheoryData<string, string> ExpectedClientRows()
|
|
{
|
|
var data = new TheoryData<string, string>();
|
|
foreach (var client in ExpectedClients)
|
|
{
|
|
data.Add(client.Slug, client.Namespace);
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(ExpectedClientRows))]
|
|
public void OidcClientManifest_MatchesOnePasswordOperatorContract(string slug, string targetNamespace)
|
|
{
|
|
var manifest = LoadClientManifest(slug);
|
|
manifest.Scalar("apiVersion").Should().Be("onepassword.com/v1");
|
|
manifest.Scalar("kind").Should().Be("OnePasswordItem");
|
|
manifest.Scalar("metadata", "name").Should().Be($"{slug}-oidc-client");
|
|
manifest.Scalar("metadata", "namespace").Should().Be(targetNamespace);
|
|
manifest.Scalar("metadata", "labels", "app.kubernetes.io/component")
|
|
.Should().Be("authentik-oidc-client");
|
|
manifest.Scalar("metadata", "labels", "flowercore.io/authentik-client-slug")
|
|
.Should().Be(slug);
|
|
manifest.Scalar("metadata", "annotations", "flowercore.io/expected-fields")
|
|
.Should().Be("client_id,client_secret,issuer_url");
|
|
manifest.Scalar("spec", "itemPath")
|
|
.Should().Be($"vaults/IAmWorkin/items/{slug}-oidc-client");
|
|
}
|
|
|
|
[Fact]
|
|
public void AuthentikKustomization_ReferencesEveryClientManifest()
|
|
{
|
|
var kustomizationPath = Path.Combine(BluejayRoot(), "apps", "authentik", "kustomization.yaml");
|
|
var text = File.ReadAllText(kustomizationPath);
|
|
|
|
foreach (var client in ExpectedClients)
|
|
{
|
|
text.Should().Contain($"clients/{client.Slug}-oidc-client.yaml");
|
|
}
|
|
|
|
Regex.Matches(text, @"clients/[-a-z0-9]+-oidc-client\.yaml")
|
|
.Select(match => match.Value)
|
|
.Distinct(StringComparer.Ordinal)
|
|
.Should()
|
|
.HaveCount(ExpectedClients.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void BulkClientScript_HasDryRunDefaultAndRequiredClaimPayloads()
|
|
{
|
|
var scriptPath = Path.Combine(BluejayRoot(), "scripts", "authentik-bulk-client-create.py");
|
|
var script = File.ReadAllText(scriptPath);
|
|
|
|
script.Should().Contain("--apply");
|
|
script.Should().Contain("Dry-run only");
|
|
script.Should().Contain("AUTHENTIK_TOKEN");
|
|
script.Should().Contain("client_secrets_json");
|
|
script.Should().Contain("scope-offline_access");
|
|
script.Should().Contain("authorization_code");
|
|
script.Should().Contain("refresh_token");
|
|
|
|
foreach (var claim in new[] { "fc:roles", "fc:tenant", "fc:svc", "fc:scope", "fc:mfa", "flowercore_actor_id" })
|
|
{
|
|
script.Should().Contain(claim);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void BulkClientScript_DryRunGeneratesAllServicesWithoutSecrets()
|
|
{
|
|
var scriptPath = Path.Combine(BluejayRoot(), "scripts", "authentik-bulk-client-create.py");
|
|
var startInfo = new ProcessStartInfo
|
|
{
|
|
FileName = "python",
|
|
WorkingDirectory = BluejayRoot(),
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
};
|
|
startInfo.ArgumentList.Add(scriptPath);
|
|
startInfo.ArgumentList.Add("--print-json");
|
|
|
|
using var process = Process.Start(startInfo)
|
|
?? throw new InvalidOperationException("Could not start python dry-run.");
|
|
var stdout = process.StandardOutput.ReadToEnd();
|
|
var stderr = process.StandardError.ReadToEnd();
|
|
process.WaitForExit(15000).Should().BeTrue(stderr);
|
|
process.ExitCode.Should().Be(0, stderr);
|
|
|
|
foreach (var client in ExpectedClients)
|
|
{
|
|
stdout.Should().Contain($"\"service\": \"{client.Slug}\"");
|
|
stdout.Should().Contain($"\"namespace\": \"{client.Namespace}\"");
|
|
}
|
|
|
|
stdout.Should().Contain("\"client_secret\": \"<redacted>\"");
|
|
stdout.Should().NotMatchRegex("\"client_secret\"\\s*:\\s*\"(?!<redacted>)[^\"]+\"");
|
|
}
|
|
|
|
[Fact]
|
|
public void ClientManifests_DoNotContainInlineSecretMaterial()
|
|
{
|
|
foreach (var client in ExpectedClients)
|
|
{
|
|
var path = ClientManifestPath(client.Slug);
|
|
var text = File.ReadAllText(path);
|
|
|
|
text.Should().NotContain("client_secret:");
|
|
text.Should().NotContain("password:");
|
|
text.Should().NotContain("secret:");
|
|
text.Should().Contain($"IAmWorkin/items/{client.Slug}-oidc-client");
|
|
}
|
|
}
|
|
|
|
private static YamlMappingNode LoadClientManifest(string slug)
|
|
{
|
|
using var reader = File.OpenText(ClientManifestPath(slug));
|
|
var stream = new YamlStream();
|
|
stream.Load(reader);
|
|
return stream.Documents[0].RootNode.Should().BeOfType<YamlMappingNode>().Subject;
|
|
}
|
|
|
|
private static string ClientManifestPath(string slug) =>
|
|
Path.Combine(BluejayRoot(), "apps", "authentik", "clients", $"{slug}-oidc-client.yaml");
|
|
|
|
private static string BluejayRoot()
|
|
{
|
|
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
|
while (current is not null)
|
|
{
|
|
if (Directory.Exists(Path.Combine(current.FullName, "apps"))
|
|
&& File.Exists(Path.Combine(current.FullName, "README.md")))
|
|
{
|
|
return current.FullName;
|
|
}
|
|
|
|
current = current.Parent;
|
|
}
|
|
|
|
throw new DirectoryNotFoundException("Could not find bluejay-infra root.");
|
|
}
|
|
|
|
private sealed record ServiceClientExpectation(string Slug, string Namespace);
|
|
}
|