Add Authority Advisory AI and API Lifecycle Configuration

- Introduced AuthorityAdvisoryAiOptions and related classes for managing advisory AI configurations, including remote inference options and tenant-specific settings.
- Added AuthorityApiLifecycleOptions to control API lifecycle settings, including legacy OAuth endpoint configurations.
- Implemented validation and normalization methods for both advisory AI and API lifecycle options to ensure proper configuration.
- Created AuthorityNotificationsOptions and its related classes for managing notification settings, including ack tokens, webhooks, and escalation options.
- Developed IssuerDirectoryClient and related models for interacting with the issuer directory service, including caching mechanisms and HTTP client configurations.
- Added support for dependency injection through ServiceCollectionExtensions for the Issuer Directory Client.
- Updated project file to include necessary package references for the new Issuer Directory Client library.
This commit is contained in:
master
2025-11-02 13:40:38 +02:00
parent 66cb6c4b8a
commit f98cea3bcf
516 changed files with 68157 additions and 24754 deletions

View File

@@ -0,0 +1,123 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using FluentAssertions;
using Xunit;
namespace StellaOps.Attestor.Types.Tests;
public class AttestationGoldenSamplesTests
{
private const string ExpectedSubjectDigest = "d5f5e54d1e1a4c3c7b18961ea7cadb88ec0a93a9f2f40f0e823d9184c83e4d72";
[Fact]
public void EverySampleIsCanonicalAndComplete()
{
var samplesDirectory = Path.Combine(AppContext.BaseDirectory, "samples");
Directory.Exists(samplesDirectory)
.Should()
.BeTrue($"golden samples should be copied to '{samplesDirectory}'");
var sampleFiles = Directory.EnumerateFiles(samplesDirectory, "*.json", SearchOption.TopDirectoryOnly)
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
.ToList();
sampleFiles.Should().NotBeEmpty("golden attestation samples must exist");
foreach (var samplePath in sampleFiles)
{
var json = File.ReadAllText(samplePath);
var node = JsonNode.Parse(json, new JsonNodeOptions { PropertyNameCaseInsensitive = false })
?? throw new InvalidOperationException($"Failed to parse sample '{samplePath}'.");
node.Should().BeOfType<JsonObject>($"sample '{samplePath}' must be a JSON object");
AssertObjectKeysSorted(node.AsObject(), Path.GetFileName(samplePath));
node["_type"]
.Should()
.NotBeNull($"sample '{samplePath}' must declare in-toto type")
.And
.Subject.As<JsonValue>()
.GetValue<string>()
.Should()
.Be("https://in-toto.io/Statement/v1");
var predicateType = node["predicateType"]?.GetValue<string>();
predicateType.Should().NotBeNullOrWhiteSpace($"sample '{samplePath}' must declare predicateType");
predicateType!.Should().MatchRegex(@"^StellaOps\.[A-Za-z]+@1$", "predicate types follow naming convention");
node["predicateVersion"]
?.GetValue<string>()
.Should()
.Be("1.0.0", $"sample '{samplePath}' must lock predicateVersion");
var subjectArray = node["subject"]?.AsArray();
subjectArray.Should().NotBeNullOrEmpty($"sample '{samplePath}' must describe subject digests");
for (var index = 0; index < subjectArray!.Count; index++)
{
var subjectEntry = subjectArray[index] ?? throw new InvalidOperationException($"Null subject entry at index {index} in '{samplePath}'.");
var digest = subjectEntry["digest"]?["sha256"]?.GetValue<string>();
digest.Should().NotBeNullOrWhiteSpace($"sample '{samplePath}' requires subject.digest.sha256");
digest!.Should().Be(ExpectedSubjectDigest, "golden samples share a single canonical digest");
var name = subjectEntry["name"]?.GetValue<string>();
name.Should().NotBeNullOrWhiteSpace($"sample '{samplePath}' requires subject name");
name!.Should().Contain(ExpectedSubjectDigest, "subject name should embed the digest");
AssertObjectKeysSorted(subjectEntry.AsObject(), $"{Path.GetFileName(samplePath)}:subject[{index}]");
AssertCanonicalRecursively(subjectEntry, $"{Path.GetFileName(samplePath)}:subject[{index}]");
}
var predicate = node["predicate"]?.AsObject();
predicate.Should().NotBeNull($"sample '{samplePath}' must include predicate content");
AssertObjectKeysSorted(predicate!, $"{Path.GetFileName(samplePath)}:predicate");
AssertCanonicalRecursively(predicate!, $"{Path.GetFileName(samplePath)}:predicate");
}
}
private static void AssertCanonicalRecursively(JsonNode node, string path)
{
switch (node)
{
case JsonObject obj:
AssertObjectKeysSorted(obj, path);
foreach (var property in obj)
{
property.Value.Should().NotBeNull($"property '{path}.{property.Key}' must not be null");
AssertCanonicalRecursively(property.Value!, $"{path}.{property.Key}");
}
break;
case JsonArray array:
for (var index = 0; index < array.Count; index++)
{
var element = array[index];
element.Should().NotBeNull($"array element '{path}[{index}]' must not be null");
AssertCanonicalRecursively(element!, $"{path}[{index}]");
}
break;
}
}
private static void AssertObjectKeysSorted(JsonObject obj, string path)
{
string? previous = null;
foreach (var property in obj)
{
if (previous is not null)
{
string.CompareOrdinal(previous, property.Key)
.Should()
.BeLessOrEqualTo(0, $"object '{path}' must keep keys in lexicographical order");
}
previous = property.Key;
}
}
}

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
</ItemGroup>
<ItemGroup>
<None Include="..\..\StellaOps.Attestor.Types\samples\**\*.json" LinkBase="samples" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>