528 lines
22 KiB
C#
528 lines
22 KiB
C#
|
|
|
|
using EvidenceLockerProgram = StellaOps.EvidenceLocker.WebService.Program;
|
|
using Microsoft.AspNetCore.Authentication;
|
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Hosting;
|
|
using Microsoft.AspNetCore.Mvc.Testing;
|
|
using Microsoft.AspNetCore.TestHost;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using Npgsql;
|
|
using StellaOps.Auth.Abstractions;
|
|
using StellaOps.Auth.ServerIntegration;
|
|
using StellaOps.EvidenceLocker.Core.Builders;
|
|
using StellaOps.EvidenceLocker.Core.Configuration;
|
|
using StellaOps.EvidenceLocker.Core.Domain;
|
|
using StellaOps.EvidenceLocker.Core.Incident;
|
|
using StellaOps.EvidenceLocker.Core.Repositories;
|
|
using StellaOps.EvidenceLocker.Core.Signing;
|
|
using StellaOps.EvidenceLocker.Core.Storage;
|
|
using StellaOps.EvidenceLocker.Core.Timeline;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Net.Http.Headers;
|
|
using System.Reflection;
|
|
using System.Runtime.Serialization;
|
|
using System.Security.Claims;
|
|
using System.Security.Cryptography;
|
|
using System.Text.Encodings.Web;
|
|
|
|
namespace StellaOps.EvidenceLocker.Tests;
|
|
|
|
public sealed class EvidenceLockerWebApplicationFactory : WebApplicationFactory<EvidenceLockerProgram>
|
|
{
|
|
private readonly string _contentRoot;
|
|
|
|
public EvidenceLockerWebApplicationFactory()
|
|
{
|
|
_contentRoot = Path.Combine(Path.GetTempPath(), "evidence-locker-tests", Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(_contentRoot);
|
|
File.WriteAllText(Path.Combine(_contentRoot, "appsettings.json"), "{}");
|
|
}
|
|
|
|
public TestEvidenceBundleRepository Repository => Services.GetRequiredService<TestEvidenceBundleRepository>();
|
|
public TestEvidenceGateArtifactRepository GateArtifactRepository => Services.GetRequiredService<TestEvidenceGateArtifactRepository>();
|
|
public TestEvidenceObjectStore ObjectStore => Services.GetRequiredService<TestEvidenceObjectStore>();
|
|
|
|
public TestTimelinePublisher TimelinePublisher => Services.GetRequiredService<TestTimelinePublisher>();
|
|
|
|
/// <summary>
|
|
/// Resets all singleton test doubles to prevent accumulated state from
|
|
/// leaking memory across test classes sharing this factory instance.
|
|
/// Call from each test class constructor.
|
|
/// </summary>
|
|
public void ResetTestState()
|
|
{
|
|
Repository.Reset();
|
|
GateArtifactRepository.Reset();
|
|
ObjectStore.Reset();
|
|
TimelinePublisher.Reset();
|
|
}
|
|
|
|
private static SigningKeyMaterialOptions GenerateKeyMaterial()
|
|
{
|
|
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
|
return new SigningKeyMaterialOptions
|
|
{
|
|
EcPrivateKeyPem = ecdsa.ExportECPrivateKeyPem(),
|
|
EcPublicKeyPem = ecdsa.ExportSubjectPublicKeyInfoPem()
|
|
};
|
|
}
|
|
|
|
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
|
{
|
|
builder.UseSetting(WebHostDefaults.ContentRootKey, _contentRoot);
|
|
builder.ConfigureAppConfiguration((context, configurationBuilder) =>
|
|
{
|
|
configurationBuilder.Sources.Clear();
|
|
var keyMaterial = GenerateKeyMaterial();
|
|
configurationBuilder.AddInMemoryCollection(new Dictionary<string, string?>
|
|
{
|
|
["EvidenceLocker:Database:ConnectionString"] = "Host=localhost",
|
|
["EvidenceLocker:Database:ApplyMigrationsAtStartup"] = "false",
|
|
["EvidenceLocker:ObjectStore:Kind"] = "FileSystem",
|
|
["EvidenceLocker:ObjectStore:FileSystem:RootPath"] = ".",
|
|
["EvidenceLocker:Quotas:MaxMaterialCount"] = "4",
|
|
["EvidenceLocker:Quotas:MaxTotalMaterialSizeBytes"] = "1024",
|
|
["EvidenceLocker:Quotas:MaxMetadataEntries"] = "4",
|
|
["EvidenceLocker:Quotas:MaxMetadataKeyLength"] = "32",
|
|
["EvidenceLocker:Quotas:MaxMetadataValueLength"] = "64",
|
|
["EvidenceLocker:Signing:Enabled"] = "true",
|
|
["EvidenceLocker:Signing:Algorithm"] = "ES256",
|
|
["EvidenceLocker:Signing:KeyId"] = "test-key",
|
|
["EvidenceLocker:Signing:PayloadType"] = "application/vnd.stella.test-manifest+json",
|
|
["EvidenceLocker:Signing:KeyMaterial:EcPrivateKeyPem"] = keyMaterial.EcPrivateKeyPem,
|
|
["EvidenceLocker:Signing:KeyMaterial:EcPublicKeyPem"] = keyMaterial.EcPublicKeyPem,
|
|
["EvidenceLocker:Signing:Timestamping:Enabled"] = "true",
|
|
["EvidenceLocker:Signing:Timestamping:Endpoint"] = "https://tsa.example",
|
|
["EvidenceLocker:Signing:Timestamping:HashAlgorithm"] = "SHA256",
|
|
["EvidenceLocker:Incident:Enabled"] = "false",
|
|
["EvidenceLocker:Incident:RetentionExtensionDays"] = "30",
|
|
["EvidenceLocker:Incident:CaptureRequestSnapshot"] = "true",
|
|
["Authority:ResourceServer:Authority"] = "https://authority.localtest.me",
|
|
["Authority:ResourceServer:Audiences:0"] = "api://evidence-locker",
|
|
["Authority:ResourceServer:RequiredTenants:0"] = "tenant-default"
|
|
});
|
|
});
|
|
|
|
builder.ConfigureTestServices(services =>
|
|
{
|
|
services.RemoveAll<IEvidenceBundleRepository>();
|
|
services.RemoveAll<IEvidenceGateArtifactRepository>();
|
|
services.RemoveAll<IEvidenceTimelinePublisher>();
|
|
services.RemoveAll<ITimestampAuthorityClient>();
|
|
services.RemoveAll<IEvidenceObjectStore>();
|
|
services.RemoveAll<IConfigureOptions<AuthenticationOptions>>();
|
|
services.RemoveAll<IPostConfigureOptions<AuthenticationOptions>>();
|
|
services.RemoveAll<IConfigureOptions<JwtBearerOptions>>();
|
|
services.RemoveAll<IPostConfigureOptions<JwtBearerOptions>>();
|
|
|
|
services.AddSingleton<TestEvidenceBundleRepository>();
|
|
services.AddSingleton<IEvidenceBundleRepository>(sp => sp.GetRequiredService<TestEvidenceBundleRepository>());
|
|
services.AddSingleton<TestEvidenceGateArtifactRepository>();
|
|
services.AddSingleton<IEvidenceGateArtifactRepository>(sp => sp.GetRequiredService<TestEvidenceGateArtifactRepository>());
|
|
services.AddSingleton<TestTimelinePublisher>();
|
|
services.AddSingleton<IEvidenceTimelinePublisher>(sp => sp.GetRequiredService<TestTimelinePublisher>());
|
|
services.AddSingleton<ITimestampAuthorityClient, TestTimestampAuthorityClient>();
|
|
services.AddSingleton<TestEvidenceObjectStore>();
|
|
services.AddSingleton<IEvidenceObjectStore>(sp => sp.GetRequiredService<TestEvidenceObjectStore>());
|
|
|
|
services.AddAuthentication(options =>
|
|
{
|
|
options.DefaultAuthenticateScheme = EvidenceLockerTestAuthHandler.SchemeName;
|
|
options.DefaultChallengeScheme = EvidenceLockerTestAuthHandler.SchemeName;
|
|
})
|
|
.AddScheme<AuthenticationSchemeOptions, EvidenceLockerTestAuthHandler>(EvidenceLockerTestAuthHandler.SchemeName, _ => { })
|
|
.AddScheme<AuthenticationSchemeOptions, EvidenceLockerTestAuthHandler>(StellaOpsAuthenticationDefaults.AuthenticationScheme, _ => { });
|
|
|
|
services.PostConfigure<AuthorizationOptions>(options =>
|
|
{
|
|
var allowAllPolicy = new AuthorizationPolicyBuilder()
|
|
.AddAuthenticationSchemes(EvidenceLockerTestAuthHandler.SchemeName)
|
|
.RequireAssertion(_ => true)
|
|
.Build();
|
|
|
|
options.DefaultPolicy = allowAllPolicy;
|
|
options.FallbackPolicy = allowAllPolicy;
|
|
options.AddPolicy(StellaOpsResourceServerPolicies.EvidenceCreate, allowAllPolicy);
|
|
options.AddPolicy(StellaOpsResourceServerPolicies.EvidenceRead, allowAllPolicy);
|
|
options.AddPolicy(StellaOpsResourceServerPolicies.EvidenceHold, allowAllPolicy);
|
|
options.AddPolicy(StellaOpsResourceServerPolicies.ExportViewer, allowAllPolicy);
|
|
options.AddPolicy(StellaOpsResourceServerPolicies.ExportOperator, allowAllPolicy);
|
|
options.AddPolicy(StellaOpsResourceServerPolicies.ExportAdmin, allowAllPolicy);
|
|
});
|
|
});
|
|
}
|
|
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
base.Dispose(disposing);
|
|
if (disposing && Directory.Exists(_contentRoot))
|
|
{
|
|
Directory.Delete(_contentRoot, recursive: true);
|
|
}
|
|
}
|
|
}
|
|
|
|
public sealed class TestTimestampAuthorityClient : ITimestampAuthorityClient
|
|
{
|
|
public Task<TimestampResult?> RequestTimestampAsync(ReadOnlyMemory<byte> signature, string hashAlgorithm, CancellationToken cancellationToken)
|
|
{
|
|
var token = signature.ToArray();
|
|
var result = new TimestampResult(DateTimeOffset.UtcNow, "test-tsa", token);
|
|
return Task.FromResult<TimestampResult?>(result);
|
|
}
|
|
}
|
|
|
|
public sealed class TestTimelinePublisher : IEvidenceTimelinePublisher
|
|
{
|
|
public List<string> PublishedEvents { get; } = new();
|
|
public List<string> IncidentEvents { get; } = new();
|
|
|
|
public void Reset()
|
|
{
|
|
PublishedEvents.Clear();
|
|
IncidentEvents.Clear();
|
|
}
|
|
|
|
public Task PublishBundleSealedAsync(
|
|
EvidenceBundleSignature signature,
|
|
EvidenceBundleManifest manifest,
|
|
string rootHash,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
PublishedEvents.Add($"bundle:{signature.BundleId.Value:D}:{rootHash}");
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task PublishHoldCreatedAsync(EvidenceHold hold, CancellationToken cancellationToken)
|
|
{
|
|
PublishedEvents.Add($"hold:{hold.CaseId}");
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task PublishIncidentModeChangedAsync(IncidentModeChange change, CancellationToken cancellationToken)
|
|
{
|
|
IncidentEvents.Add(change.IsActive ? "enabled" : "disabled");
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
|
|
public sealed class TestEvidenceObjectStore : IEvidenceObjectStore
|
|
{
|
|
private readonly Dictionary<string, byte[]> _objects = new(StringComparer.Ordinal);
|
|
private readonly HashSet<string> _preExisting = new(StringComparer.Ordinal);
|
|
|
|
public IReadOnlyDictionary<string, byte[]> StoredObjects => _objects;
|
|
|
|
public void SeedExisting(string storageKey) => _preExisting.Add(storageKey);
|
|
|
|
public void Reset()
|
|
{
|
|
_objects.Clear();
|
|
_preExisting.Clear();
|
|
}
|
|
|
|
public Task<EvidenceObjectMetadata> StoreAsync(Stream content, EvidenceObjectWriteOptions options, CancellationToken cancellationToken)
|
|
{
|
|
using var memory = new MemoryStream();
|
|
content.CopyTo(memory);
|
|
var bytes = memory.ToArray();
|
|
var storageKey = $"tenants/{options.TenantId.Value:N}/bundles/{options.BundleId.Value:N}/{options.ArtifactName}";
|
|
_objects[storageKey] = bytes;
|
|
_preExisting.Add(storageKey);
|
|
|
|
return Task.FromResult(new EvidenceObjectMetadata(
|
|
storageKey,
|
|
options.ContentType,
|
|
bytes.Length,
|
|
Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant(),
|
|
null,
|
|
DateTimeOffset.UtcNow));
|
|
}
|
|
|
|
public Task<Stream> OpenReadAsync(string storageKey, CancellationToken cancellationToken)
|
|
{
|
|
if (!_objects.TryGetValue(storageKey, out var bytes))
|
|
{
|
|
throw new FileNotFoundException(storageKey);
|
|
}
|
|
|
|
return Task.FromResult<Stream>(new MemoryStream(bytes, writable: false));
|
|
}
|
|
|
|
public Task<bool> ExistsAsync(string storageKey, CancellationToken cancellationToken)
|
|
=> Task.FromResult(_preExisting.Contains(storageKey));
|
|
}
|
|
|
|
public sealed class TestEvidenceBundleRepository : IEvidenceBundleRepository
|
|
{
|
|
private readonly List<EvidenceBundleSignature> _signatures = new();
|
|
private readonly Dictionary<(Guid BundleId, Guid TenantId), EvidenceBundle> _bundles = new();
|
|
|
|
public bool HoldConflict { get; set; }
|
|
|
|
public void Reset()
|
|
{
|
|
_signatures.Clear();
|
|
_bundles.Clear();
|
|
HoldConflict = false;
|
|
}
|
|
|
|
public Task CreateBundleAsync(EvidenceBundle bundle, CancellationToken cancellationToken)
|
|
{
|
|
_bundles[(bundle.Id.Value, bundle.TenantId.Value)] = bundle;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task SetBundleAssemblyAsync(EvidenceBundleId bundleId, TenantId tenantId, EvidenceBundleStatus status, string rootHash, DateTimeOffset updatedAt, CancellationToken cancellationToken)
|
|
{
|
|
UpdateBundle(bundleId, tenantId, bundle => bundle with
|
|
{
|
|
Status = status,
|
|
RootHash = rootHash,
|
|
UpdatedAt = updatedAt
|
|
});
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task MarkBundleSealedAsync(EvidenceBundleId bundleId, TenantId tenantId, EvidenceBundleStatus status, DateTimeOffset sealedAt, CancellationToken cancellationToken)
|
|
{
|
|
UpdateBundle(bundleId, tenantId, bundle => bundle with
|
|
{
|
|
Status = status,
|
|
SealedAt = sealedAt,
|
|
UpdatedAt = sealedAt
|
|
});
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task UpsertSignatureAsync(EvidenceBundleSignature signature, CancellationToken cancellationToken)
|
|
{
|
|
_signatures.RemoveAll(sig => sig.BundleId == signature.BundleId && sig.TenantId == signature.TenantId);
|
|
_signatures.Add(signature);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task<EvidenceBundleDetails?> GetBundleAsync(EvidenceBundleId bundleId, TenantId tenantId, CancellationToken cancellationToken)
|
|
{
|
|
_bundles.TryGetValue((bundleId.Value, tenantId.Value), out var bundle);
|
|
var signature = _signatures.FirstOrDefault(sig => sig.BundleId == bundleId && sig.TenantId == tenantId);
|
|
return Task.FromResult<EvidenceBundleDetails?>(bundle is null ? null : new EvidenceBundleDetails(bundle, signature));
|
|
}
|
|
|
|
public Task<IReadOnlyList<EvidenceBundleDetails>> GetBundlesForReindexAsync(
|
|
TenantId tenantId,
|
|
DateTimeOffset? since,
|
|
DateTimeOffset? cursorUpdatedAt,
|
|
EvidenceBundleId? cursorBundleId,
|
|
int limit,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var filtered = _bundles.Values
|
|
.Where(bundle => bundle.TenantId == tenantId)
|
|
.Where(bundle => !since.HasValue || bundle.UpdatedAt >= since.Value)
|
|
.OrderBy(bundle => bundle.UpdatedAt)
|
|
.ThenBy(bundle => bundle.Id.Value);
|
|
|
|
IEnumerable<EvidenceBundle> paged = filtered;
|
|
if (cursorUpdatedAt.HasValue && cursorBundleId.HasValue)
|
|
{
|
|
paged = filtered.SkipWhile(b =>
|
|
b.UpdatedAt < cursorUpdatedAt.Value ||
|
|
(b.UpdatedAt == cursorUpdatedAt.Value && b.Id.Value <= cursorBundleId.Value.Value));
|
|
}
|
|
|
|
var results = paged
|
|
.Take(limit)
|
|
.Select(bundle =>
|
|
{
|
|
var signature = _signatures.FirstOrDefault(sig => sig.BundleId == bundle.Id && sig.TenantId == tenantId);
|
|
return new EvidenceBundleDetails(bundle, signature);
|
|
})
|
|
.ToList();
|
|
|
|
return Task.FromResult<IReadOnlyList<EvidenceBundleDetails>>(results);
|
|
}
|
|
|
|
public Task<bool> ExistsAsync(EvidenceBundleId bundleId, TenantId tenantId, CancellationToken cancellationToken)
|
|
=> Task.FromResult(_bundles.ContainsKey((bundleId.Value, tenantId.Value)));
|
|
|
|
public Task<EvidenceHold> CreateHoldAsync(EvidenceHold hold, CancellationToken cancellationToken)
|
|
{
|
|
if (HoldConflict)
|
|
{
|
|
throw CreateUniqueViolationException();
|
|
}
|
|
|
|
return Task.FromResult(hold);
|
|
}
|
|
|
|
public Task ExtendBundleRetentionAsync(EvidenceBundleId bundleId, TenantId tenantId, DateTimeOffset? holdExpiresAt, DateTimeOffset processedAt, CancellationToken cancellationToken)
|
|
{
|
|
UpdateBundle(bundleId, tenantId, bundle => bundle with
|
|
{
|
|
ExpiresAt = holdExpiresAt,
|
|
UpdatedAt = processedAt > bundle.UpdatedAt ? processedAt : bundle.UpdatedAt
|
|
});
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task UpdateStorageKeyAsync(EvidenceBundleId bundleId, TenantId tenantId, string storageKey, CancellationToken cancellationToken)
|
|
{
|
|
UpdateBundle(bundleId, tenantId, bundle => bundle with
|
|
{
|
|
StorageKey = storageKey,
|
|
UpdatedAt = DateTimeOffset.UtcNow
|
|
});
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task UpdatePortableStorageKeyAsync(
|
|
EvidenceBundleId bundleId,
|
|
TenantId tenantId,
|
|
string storageKey,
|
|
DateTimeOffset generatedAt,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
UpdateBundle(bundleId, tenantId, bundle => bundle with
|
|
{
|
|
PortableStorageKey = storageKey,
|
|
PortableGeneratedAt = generatedAt,
|
|
UpdatedAt = generatedAt > bundle.UpdatedAt ? generatedAt : bundle.UpdatedAt
|
|
});
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
private void UpdateBundle(EvidenceBundleId bundleId, TenantId tenantId, Func<EvidenceBundle, EvidenceBundle> updater)
|
|
{
|
|
var key = (bundleId.Value, tenantId.Value);
|
|
if (_bundles.TryGetValue(key, out var existing))
|
|
{
|
|
_bundles[key] = updater(existing);
|
|
}
|
|
}
|
|
|
|
#pragma warning disable SYSLIB0050
|
|
private static PostgresException CreateUniqueViolationException()
|
|
{
|
|
var exception = (PostgresException)FormatterServices.GetUninitializedObject(typeof(PostgresException));
|
|
SetStringField(exception, "<SqlState>k__BackingField", PostgresErrorCodes.UniqueViolation);
|
|
SetStringField(exception, "_sqlState", PostgresErrorCodes.UniqueViolation);
|
|
return exception;
|
|
}
|
|
#pragma warning restore SYSLIB0050
|
|
|
|
private static void SetStringField(object target, string fieldName, string value)
|
|
{
|
|
var field = target.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
|
|
field?.SetValue(target, value);
|
|
}
|
|
}
|
|
|
|
public sealed class TestEvidenceGateArtifactRepository : IEvidenceGateArtifactRepository
|
|
{
|
|
private readonly Dictionary<(Guid TenantId, string ArtifactId), EvidenceGateArtifactRecord> _records = new();
|
|
|
|
public void Reset() => _records.Clear();
|
|
|
|
public Task<EvidenceGateArtifactRecord> UpsertAsync(EvidenceGateArtifactRecord record, CancellationToken cancellationToken)
|
|
{
|
|
var stored = record with { UpdatedAt = DateTimeOffset.UtcNow };
|
|
_records[(record.TenantId.Value, record.ArtifactId)] = stored;
|
|
return Task.FromResult(stored);
|
|
}
|
|
|
|
public Task<EvidenceGateArtifactRecord?> GetByArtifactIdAsync(TenantId tenantId, string artifactId, CancellationToken cancellationToken)
|
|
{
|
|
_records.TryGetValue((tenantId.Value, artifactId), out var record);
|
|
return Task.FromResult(record);
|
|
}
|
|
}
|
|
|
|
public sealed class EvidenceLockerTestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
|
|
{
|
|
internal const string SchemeName = "EvidenceLockerTest";
|
|
|
|
public EvidenceLockerTestAuthHandler(
|
|
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
|
ILoggerFactory logger,
|
|
UrlEncoder encoder)
|
|
: base(options, logger, encoder)
|
|
{
|
|
}
|
|
|
|
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
|
{
|
|
if (!Request.Headers.TryGetValue("Authorization", out var rawHeader) ||
|
|
!AuthenticationHeaderValue.TryParse(rawHeader, out var header))
|
|
{
|
|
return Task.FromResult(AuthenticateResult.NoResult());
|
|
}
|
|
|
|
// Accept both "EvidenceLockerTest" and "Bearer" schemes for test flexibility
|
|
if (!string.Equals(header.Scheme, SchemeName, StringComparison.OrdinalIgnoreCase) &&
|
|
!string.Equals(header.Scheme, "Bearer", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return Task.FromResult(AuthenticateResult.NoResult());
|
|
}
|
|
|
|
var claims = new List<Claim>();
|
|
|
|
var subject = Request.Headers.TryGetValue("X-Test-Subject", out var subjectValue)
|
|
? subjectValue.ToString()
|
|
: "subject-test";
|
|
claims.Add(new Claim(StellaOpsClaimTypes.Subject, subject));
|
|
|
|
if (Request.Headers.TryGetValue("X-Test-Client", out var clientValue) &&
|
|
!string.IsNullOrWhiteSpace(clientValue))
|
|
{
|
|
claims.Add(new Claim(StellaOpsClaimTypes.ClientId, clientValue.ToString()!));
|
|
}
|
|
|
|
// Support both X-Test-Tenant and X-Tenant-Id headers
|
|
if (Request.Headers.TryGetValue("X-Test-Tenant", out var tenantValue) &&
|
|
Guid.TryParse(tenantValue, out var tenantId))
|
|
{
|
|
claims.Add(new Claim(StellaOpsClaimTypes.Tenant, tenantId.ToString("D")));
|
|
}
|
|
else if (Request.Headers.TryGetValue("X-Tenant-Id", out var tenantIdValue) &&
|
|
Guid.TryParse(tenantIdValue, out var tenantIdFromHeader))
|
|
{
|
|
claims.Add(new Claim(StellaOpsClaimTypes.Tenant, tenantIdFromHeader.ToString("D")));
|
|
}
|
|
|
|
// Support both X-Test-Scopes and X-Scopes headers
|
|
if (Request.Headers.TryGetValue("X-Test-Scopes", out var scopesValue))
|
|
{
|
|
var scopes = scopesValue
|
|
.ToString()
|
|
.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
|
foreach (var scope in scopes)
|
|
{
|
|
claims.Add(new Claim(StellaOpsClaimTypes.Scope, scope));
|
|
}
|
|
}
|
|
else if (Request.Headers.TryGetValue("X-Scopes", out var xScopesValue))
|
|
{
|
|
var scopes = xScopesValue
|
|
.ToString()
|
|
.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
|
foreach (var scope in scopes)
|
|
{
|
|
claims.Add(new Claim(StellaOpsClaimTypes.Scope, scope));
|
|
}
|
|
}
|
|
|
|
var identity = new ClaimsIdentity(claims, Scheme.Name);
|
|
var principal = new ClaimsPrincipal(identity);
|
|
var ticket = new AuthenticationTicket(principal, Scheme.Name);
|
|
return Task.FromResult(AuthenticateResult.Success(ticket));
|
|
}
|
|
}
|