Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- 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.
212 lines
8.2 KiB
C#
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));
|
|
}
|
|
|
|
}
|