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:
@@ -0,0 +1,120 @@
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user