consolidation of some of the modules, localization fixes, product advisories work, qa work
This commit is contained in:
@@ -1,20 +1,46 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Telemetry.Federation.Security;
|
||||
|
||||
namespace StellaOps.Telemetry.Federation.Consent;
|
||||
|
||||
public sealed class ConsentManager : IConsentManager
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, ConsentEntry> _consents = new();
|
||||
private readonly FederatedTelemetryOptions _options;
|
||||
private readonly IFederationDsseEnvelopeSigner _envelopeSigner;
|
||||
private readonly IFederationDsseEnvelopeVerifier _envelopeVerifier;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public ConsentManager(TimeProvider? timeProvider = null)
|
||||
public ConsentManager(
|
||||
IOptions<FederatedTelemetryOptions> options,
|
||||
IFederationDsseEnvelopeSigner envelopeSigner,
|
||||
IFederationDsseEnvelopeVerifier envelopeVerifier,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
_options = options.Value;
|
||||
_envelopeSigner = envelopeSigner ?? throw new ArgumentNullException(nameof(envelopeSigner));
|
||||
_envelopeVerifier = envelopeVerifier ?? throw new ArgumentNullException(nameof(envelopeVerifier));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public ConsentManager(IOptions<FederatedTelemetryOptions> options, TimeProvider? timeProvider = null)
|
||||
: this(
|
||||
options,
|
||||
new HmacFederationDsseEnvelopeService(options),
|
||||
new HmacFederationDsseEnvelopeService(options),
|
||||
timeProvider)
|
||||
{
|
||||
}
|
||||
|
||||
public ConsentManager(TimeProvider? timeProvider = null)
|
||||
: this(Options.Create(new FederatedTelemetryOptions()), timeProvider)
|
||||
{
|
||||
}
|
||||
|
||||
public Task<ConsentState> GetConsentStateAsync(string tenantId, CancellationToken ct = default)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
@@ -26,7 +52,8 @@ public sealed class ConsentManager : IConsentManager
|
||||
GrantedBy: null,
|
||||
GrantedAt: null,
|
||||
ExpiresAt: null,
|
||||
DsseDigest: null));
|
||||
DsseDigest: null,
|
||||
SignerKeyId: null));
|
||||
}
|
||||
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
@@ -38,7 +65,8 @@ public sealed class ConsentManager : IConsentManager
|
||||
GrantedBy: null,
|
||||
GrantedAt: null,
|
||||
ExpiresAt: null,
|
||||
DsseDigest: null));
|
||||
DsseDigest: null,
|
||||
SignerKeyId: null));
|
||||
}
|
||||
|
||||
return Task.FromResult(new ConsentState(
|
||||
@@ -46,10 +74,11 @@ public sealed class ConsentManager : IConsentManager
|
||||
GrantedBy: entry.GrantedBy,
|
||||
GrantedAt: entry.GrantedAt,
|
||||
ExpiresAt: entry.ExpiresAt,
|
||||
DsseDigest: entry.DsseDigest));
|
||||
DsseDigest: entry.DsseDigest,
|
||||
SignerKeyId: entry.SignerKeyId));
|
||||
}
|
||||
|
||||
public Task<ConsentProof> GrantConsentAsync(
|
||||
public async Task<ConsentProof> GrantConsentAsync(
|
||||
string tenantId,
|
||||
string grantedBy,
|
||||
TimeSpan? ttl = null,
|
||||
@@ -60,28 +89,101 @@ public sealed class ConsentManager : IConsentManager
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var expiresAt = ttl.HasValue ? now + ttl.Value : (DateTimeOffset?)null;
|
||||
|
||||
var payload = JsonSerializer.SerializeToUtf8Bytes(new
|
||||
{
|
||||
tenantId,
|
||||
grantedBy,
|
||||
grantedAt = now,
|
||||
expiresAt,
|
||||
type = "stella.ops/federatedConsent@v1"
|
||||
});
|
||||
|
||||
var digest = ComputeDigest(payload);
|
||||
var envelope = payload; // Placeholder: real DSSE envelope wraps with signature
|
||||
|
||||
var entry = new ConsentEntry(tenantId, grantedBy, now, expiresAt, digest);
|
||||
_consents[tenantId] = entry;
|
||||
|
||||
return Task.FromResult(new ConsentProof(
|
||||
var payload = JsonSerializer.SerializeToUtf8Bytes(new ConsentPayloadDocument(
|
||||
TenantId: tenantId,
|
||||
GrantedBy: grantedBy,
|
||||
GrantedAt: now,
|
||||
ExpiresAt: expiresAt,
|
||||
DsseDigest: digest,
|
||||
Envelope: envelope));
|
||||
Type: _options.ConsentPredicateType));
|
||||
|
||||
FederationDsseEnvelopeSignResult signResult;
|
||||
try
|
||||
{
|
||||
signResult = await _envelopeSigner
|
||||
.SignAsync(_options.ConsentPredicateType, payload, ct)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (FederationSignatureException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new FederationSignatureException(
|
||||
"federation.dsse.sign_failed",
|
||||
"Consent proof could not be DSSE-signed.",
|
||||
ex);
|
||||
}
|
||||
|
||||
var entry = new ConsentEntry(tenantId, grantedBy, now, expiresAt, signResult.EnvelopeDigest, signResult.SignerKeyId);
|
||||
_consents[tenantId] = entry;
|
||||
|
||||
return new ConsentProof(
|
||||
TenantId: tenantId,
|
||||
GrantedBy: grantedBy,
|
||||
GrantedAt: now,
|
||||
ExpiresAt: expiresAt,
|
||||
DsseDigest: signResult.EnvelopeDigest,
|
||||
Envelope: signResult.Envelope,
|
||||
SignerKeyId: signResult.SignerKeyId);
|
||||
}
|
||||
|
||||
public async Task<bool> VerifyProofAsync(ConsentProof proof, CancellationToken ct = default)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
ArgumentNullException.ThrowIfNull(proof);
|
||||
|
||||
var envelopeDigest = HmacFederationDsseEnvelopeService.ComputeDigest(proof.Envelope);
|
||||
if (!string.Equals(envelopeDigest, proof.DsseDigest, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var verifyResult = await _envelopeVerifier
|
||||
.VerifyAsync(proof.Envelope, _options.ConsentPredicateType, ct)
|
||||
.ConfigureAwait(false);
|
||||
if (!verifyResult.IsValid || verifyResult.Payload is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ConsentPayloadDocument? payload;
|
||||
try
|
||||
{
|
||||
payload = JsonSerializer.Deserialize<ConsentPayloadDocument>(verifyResult.Payload);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (payload is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.Equals(payload.TenantId, proof.TenantId, StringComparison.Ordinal) ||
|
||||
!string.Equals(payload.GrantedBy, proof.GrantedBy, StringComparison.Ordinal) ||
|
||||
payload.GrantedAt != proof.GrantedAt ||
|
||||
payload.ExpiresAt != proof.ExpiresAt ||
|
||||
!string.Equals(payload.Type, _options.ConsentPredicateType, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(proof.SignerKeyId) &&
|
||||
!string.Equals(proof.SignerKeyId, verifyResult.SignerKeyId, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
if (payload.ExpiresAt.HasValue && now >= payload.ExpiresAt.Value)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public Task RevokeConsentAsync(string tenantId, string revokedBy, CancellationToken ct = default)
|
||||
@@ -91,16 +193,18 @@ public sealed class ConsentManager : IConsentManager
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static string ComputeDigest(byte[] payload)
|
||||
{
|
||||
var hash = SHA256.HashData(payload);
|
||||
return $"sha256:{Convert.ToHexStringLower(hash)}";
|
||||
}
|
||||
|
||||
private sealed record ConsentEntry(
|
||||
string TenantId,
|
||||
string GrantedBy,
|
||||
DateTimeOffset GrantedAt,
|
||||
DateTimeOffset? ExpiresAt,
|
||||
string DsseDigest);
|
||||
string DsseDigest,
|
||||
string SignerKeyId);
|
||||
|
||||
private sealed record ConsentPayloadDocument(
|
||||
[property: JsonPropertyName("tenantId")] string TenantId,
|
||||
[property: JsonPropertyName("grantedBy")] string GrantedBy,
|
||||
[property: JsonPropertyName("grantedAt")] DateTimeOffset GrantedAt,
|
||||
[property: JsonPropertyName("expiresAt")] DateTimeOffset? ExpiresAt,
|
||||
[property: JsonPropertyName("type")] string Type);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ public interface IConsentManager
|
||||
{
|
||||
Task<ConsentState> GetConsentStateAsync(string tenantId, CancellationToken ct = default);
|
||||
Task<ConsentProof> GrantConsentAsync(string tenantId, string grantedBy, TimeSpan? ttl = null, CancellationToken ct = default);
|
||||
Task<bool> VerifyProofAsync(ConsentProof proof, CancellationToken ct = default);
|
||||
Task RevokeConsentAsync(string tenantId, string revokedBy, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
@@ -12,7 +13,8 @@ public sealed record ConsentState(
|
||||
string? GrantedBy,
|
||||
DateTimeOffset? GrantedAt,
|
||||
DateTimeOffset? ExpiresAt,
|
||||
string? DsseDigest);
|
||||
string? DsseDigest,
|
||||
string? SignerKeyId = null);
|
||||
|
||||
public sealed record ConsentProof(
|
||||
string TenantId,
|
||||
@@ -20,4 +22,5 @@ public sealed record ConsentProof(
|
||||
DateTimeOffset GrantedAt,
|
||||
DateTimeOffset? ExpiresAt,
|
||||
string DsseDigest,
|
||||
byte[] Envelope);
|
||||
byte[] Envelope,
|
||||
string? SignerKeyId = null);
|
||||
|
||||
Reference in New Issue
Block a user