Files
git.stella-ops.org/src/__Libraries/StellaOps.IssuerDirectory.Client/IssuerDirectoryClient.cs
master 2eb6852d34
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Add unit tests for SBOM ingestion and transformation
- Implement `SbomIngestServiceCollectionExtensionsTests` to verify the SBOM ingestion pipeline exports snapshots correctly.
- Create `SbomIngestTransformerTests` to ensure the transformation produces expected nodes and edges, including deduplication of license nodes and normalization of timestamps.
- Add `SbomSnapshotExporterTests` to test the export functionality for manifest, adjacency, nodes, and edges.
- Introduce `VexOverlayTransformerTests` to validate the transformation of VEX nodes and edges.
- Set up project file for the test project with necessary dependencies and configurations.
- Include JSON fixture files for testing purposes.
2025-11-04 07:49:39 +02:00

212 lines
8.2 KiB
C#

using System;
using System.Globalization;
using System.Net.Http;
using System.Net.Http.Json;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace StellaOps.IssuerDirectory.Client;
internal sealed class IssuerDirectoryClient : IIssuerDirectoryClient
{
private readonly HttpClient _httpClient;
private readonly IMemoryCache _cache;
private readonly IssuerDirectoryClientOptions _options;
private readonly ILogger<IssuerDirectoryClient> _logger;
public IssuerDirectoryClient(
HttpClient httpClient,
IMemoryCache cache,
IOptions<IssuerDirectoryClientOptions> options,
ILogger<IssuerDirectoryClient> logger)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_cache = cache ?? throw new ArgumentNullException(nameof(cache));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
ArgumentNullException.ThrowIfNull(options);
_options = options.Value;
_options.Validate();
}
public async ValueTask<IReadOnlyList<IssuerKeyModel>> GetIssuerKeysAsync(
string tenantId,
string issuerId,
bool includeGlobal,
CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
ArgumentException.ThrowIfNullOrWhiteSpace(issuerId);
tenantId = tenantId.Trim();
issuerId = issuerId.Trim();
var cacheKey = CacheKey("keys", tenantId, issuerId, includeGlobal.ToString(CultureInfo.InvariantCulture));
if (_cache.TryGetValue(cacheKey, out IReadOnlyList<IssuerKeyModel>? cached) && cached is not null)
{
return cached;
}
var requestUri = $"issuer-directory/issuers/{Uri.EscapeDataString(issuerId)}/keys?includeGlobal={includeGlobal.ToString().ToLowerInvariant()}";
using var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
request.Headers.TryAddWithoutValidation(_options.TenantHeader, tenantId);
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning(
"Issuer Directory key lookup failed for {IssuerId} (tenant={TenantId}) {StatusCode}",
issuerId,
tenantId,
response.StatusCode);
response.EnsureSuccessStatusCode();
}
var payload = await response.Content.ReadFromJsonAsync<List<IssuerKeyModel>>(cancellationToken: cancellationToken)
.ConfigureAwait(false);
IReadOnlyList<IssuerKeyModel> result = payload?.ToArray() ?? Array.Empty<IssuerKeyModel>();
_cache.Set(cacheKey, result, _options.Cache.Keys);
return result;
}
public async ValueTask<IssuerTrustResponseModel> GetIssuerTrustAsync(
string tenantId,
string issuerId,
bool includeGlobal,
CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
ArgumentException.ThrowIfNullOrWhiteSpace(issuerId);
tenantId = tenantId.Trim();
issuerId = issuerId.Trim();
var cacheKey = CacheKey("trust", tenantId, issuerId, includeGlobal.ToString(CultureInfo.InvariantCulture));
if (_cache.TryGetValue(cacheKey, out IssuerTrustResponseModel? cached) && cached is not null)
{
return cached;
}
var requestUri = $"issuer-directory/issuers/{Uri.EscapeDataString(issuerId)}/trust?includeGlobal={includeGlobal.ToString().ToLowerInvariant()}";
using var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
request.Headers.TryAddWithoutValidation(_options.TenantHeader, tenantId);
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning(
"Issuer Directory trust lookup failed for {IssuerId} (tenant={TenantId}) {StatusCode}",
issuerId,
tenantId,
response.StatusCode);
response.EnsureSuccessStatusCode();
}
var payload = await response.Content.ReadFromJsonAsync<IssuerTrustResponseModel>(cancellationToken: cancellationToken)
.ConfigureAwait(false) ?? new IssuerTrustResponseModel(null, null, 0m);
_cache.Set(cacheKey, payload, _options.Cache.Trust);
return payload;
}
public async ValueTask<IssuerTrustResponseModel> SetIssuerTrustAsync(
string tenantId,
string issuerId,
decimal weight,
string? reason,
CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
ArgumentException.ThrowIfNullOrWhiteSpace(issuerId);
var normalizedTenant = tenantId.Trim();
var normalizedReason = string.IsNullOrWhiteSpace(reason) ? null : reason.Trim();
var requestUri = $"issuer-directory/issuers/{Uri.EscapeDataString(issuerId)}/trust";
using var request = new HttpRequestMessage(HttpMethod.Put, requestUri)
{
Content = JsonContent.Create(new IssuerTrustSetRequestModel(weight, normalizedReason))
};
request.Headers.TryAddWithoutValidation(_options.TenantHeader, normalizedTenant);
if (!string.IsNullOrWhiteSpace(normalizedReason))
{
request.Headers.TryAddWithoutValidation(_options.AuditReasonHeader, normalizedReason);
}
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning(
"Issuer Directory trust update failed for {IssuerId} (tenant={TenantId}) {StatusCode}",
issuerId,
normalizedTenant,
response.StatusCode);
response.EnsureSuccessStatusCode();
}
InvalidateTrustCache(normalizedTenant, issuerId);
var payload = await response.Content.ReadFromJsonAsync<IssuerTrustResponseModel>(cancellationToken: cancellationToken)
.ConfigureAwait(false) ?? new IssuerTrustResponseModel(null, null, 0m);
return payload;
}
public async ValueTask DeleteIssuerTrustAsync(
string tenantId,
string issuerId,
string? reason,
CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
ArgumentException.ThrowIfNullOrWhiteSpace(issuerId);
var normalizedTenant = tenantId.Trim();
var normalizedReason = string.IsNullOrWhiteSpace(reason) ? null : reason.Trim();
var requestUri = $"issuer-directory/issuers/{Uri.EscapeDataString(issuerId)}/trust";
using var request = new HttpRequestMessage(HttpMethod.Delete, requestUri);
request.Headers.TryAddWithoutValidation(_options.TenantHeader, normalizedTenant);
if (!string.IsNullOrWhiteSpace(normalizedReason))
{
request.Headers.TryAddWithoutValidation(_options.AuditReasonHeader, normalizedReason);
}
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning(
"Issuer Directory trust delete failed for {IssuerId} (tenant={TenantId}) {StatusCode}",
issuerId,
normalizedTenant,
response.StatusCode);
response.EnsureSuccessStatusCode();
}
InvalidateTrustCache(normalizedTenant, issuerId);
}
private static string CacheKey(string prefix, params string[] parts)
{
if (parts is null || parts.Length == 0)
{
return prefix;
}
var segments = new string[1 + parts.Length];
segments[0] = prefix;
Array.Copy(parts, 0, segments, 1, parts.Length);
return string.Join('|', segments);
}
private void InvalidateTrustCache(string tenantId, string issuerId)
{
_cache.Remove(CacheKey("trust", tenantId, issuerId, bool.FalseString));
_cache.Remove(CacheKey("trust", tenantId, issuerId, bool.TrueString));
}
}