diff --git a/docs/architecture/dal-domain-alignment.md b/docs/architecture/dal-domain-alignment.md new file mode 100644 index 0000000..1261cc9 --- /dev/null +++ b/docs/architecture/dal-domain-alignment.md @@ -0,0 +1,13 @@ +# Thalos DAL Domain Alignment + +## Goal +Align DAL with thalos-domain abstractions while keeping DAL technical. + +## DAL Responsibilities +- Identity persistence and retrieval +- Technical data translation +- Provider/repository boundaries + +## Prohibited +- Identity policy decision ownership +- Service orchestration concerns diff --git a/docs/dal/identity-provider-boundaries.md b/docs/dal/identity-provider-boundaries.md index f57bc23..d47bf45 100644 --- a/docs/dal/identity-provider-boundaries.md +++ b/docs/dal/identity-provider-boundaries.md @@ -16,3 +16,7 @@ - Provider boundaries remain internal to Thalos DAL. - DAL interfaces expose only transport-neutral contracts and read ports. - Identity abstractions remain Thalos-owned. +- Runtime provider routes currently support: + - `InternalJwt` + - `AzureAd` + - `Google` diff --git a/docs/migration/dal-port-alignment-map.md b/docs/migration/dal-port-alignment-map.md new file mode 100644 index 0000000..dbbc0be --- /dev/null +++ b/docs/migration/dal-port-alignment-map.md @@ -0,0 +1,6 @@ +# Thalos DAL Port Alignment Map + +## Alignment Areas +- DAL read/write ports map to domain contracts. +- Technical DTO translation remains in DAL adapters. +- Domain policy semantics are not reimplemented in DAL. diff --git a/docs/migration/technical-mapping-rules.md b/docs/migration/technical-mapping-rules.md new file mode 100644 index 0000000..98161ed --- /dev/null +++ b/docs/migration/technical-mapping-rules.md @@ -0,0 +1,6 @@ +# Thalos DAL Technical Mapping Rules + +## Rules +- Mapping logic remains technical and deterministic. +- No policy evaluation branching in DAL mapping layer. +- Correlation and metadata pass-through remains unchanged. diff --git a/src/Thalos.DAL/Adapters/IdentityDalGrpcContractAdapter.cs b/src/Thalos.DAL/Adapters/IdentityDalGrpcContractAdapter.cs new file mode 100644 index 0000000..f07d917 --- /dev/null +++ b/src/Thalos.DAL/Adapters/IdentityDalGrpcContractAdapter.cs @@ -0,0 +1,47 @@ +using Core.Blueprint.Common.Runtime; +using Thalos.DAL.Contracts; +using Thalos.DAL.Grpc; + +namespace Thalos.DAL.Adapters; + +/// +/// Default adapter implementation for DAL gRPC contract translation. +/// +public sealed class IdentityDalGrpcContractAdapter(IBlueprintSystemClock clock) : IIdentityDalGrpcContractAdapter +{ + /// + public IdentityPolicyDalGrpcContract ToGrpcPolicyRequest(IdentityPolicyLookupRequest request) + { + return new IdentityPolicyDalGrpcContract(request.SubjectId, request.TenantId, request.PermissionCode); + } + + /// + public IdentityPolicyLookupRequest FromGrpcPolicyRequest(IdentityPolicyDalGrpcContract contract) + { + return new IdentityPolicyLookupRequest( + CreateEnvelope(), + contract.SubjectId, + contract.TenantId, + contract.PermissionCode); + } + + /// + public IdentityTokenDalGrpcContract ToGrpcTokenRequest(IdentityTokenLookupRequest request) + { + return new IdentityTokenDalGrpcContract(request.SubjectId, request.TenantId); + } + + /// + public IdentityTokenLookupRequest FromGrpcTokenRequest(IdentityTokenDalGrpcContract contract) + { + return new IdentityTokenLookupRequest( + CreateEnvelope(), + contract.SubjectId, + contract.TenantId); + } + + private IdentityContractEnvelope CreateEnvelope() + { + return new IdentityContractEnvelope("1.0.0", $"corr-{clock.UtcNow:yyyyMMddHHmmssfff}"); + } +} diff --git a/src/Thalos.DAL/Contracts/IdentityPermissionRecord.cs b/src/Thalos.DAL/Contracts/IdentityPermissionRecord.cs index 8ee10f2..10f8083 100644 --- a/src/Thalos.DAL/Contracts/IdentityPermissionRecord.cs +++ b/src/Thalos.DAL/Contracts/IdentityPermissionRecord.cs @@ -1,3 +1,5 @@ +using BuildingBlock.Identity.Contracts.Conventions; + namespace Thalos.DAL.Contracts; /// @@ -6,7 +8,9 @@ namespace Thalos.DAL.Contracts; /// Contract envelope metadata. /// Permission code identifier. /// Role code that grants the permission. +/// Auth provider for the permission grant. public sealed record IdentityPermissionRecord( IdentityContractEnvelope Envelope, string PermissionCode, - string SourceRoleCode); + string SourceRoleCode, + IdentityAuthProvider Provider = IdentityAuthProvider.InternalJwt); diff --git a/src/Thalos.DAL/Contracts/IdentityPermissionSetLookupRequest.cs b/src/Thalos.DAL/Contracts/IdentityPermissionSetLookupRequest.cs index d41d316..78dff81 100644 --- a/src/Thalos.DAL/Contracts/IdentityPermissionSetLookupRequest.cs +++ b/src/Thalos.DAL/Contracts/IdentityPermissionSetLookupRequest.cs @@ -1,3 +1,5 @@ +using BuildingBlock.Identity.Contracts.Conventions; + namespace Thalos.DAL.Contracts; /// @@ -6,7 +8,9 @@ namespace Thalos.DAL.Contracts; /// Contract envelope metadata. /// Identity subject identifier. /// Tenant scope identifier. +/// Auth provider for the lookup flow. public sealed record IdentityPermissionSetLookupRequest( IdentityContractEnvelope Envelope, string SubjectId, - string TenantId); + string TenantId, + IdentityAuthProvider Provider = IdentityAuthProvider.InternalJwt); diff --git a/src/Thalos.DAL/Contracts/IdentityPolicyLookupRequest.cs b/src/Thalos.DAL/Contracts/IdentityPolicyLookupRequest.cs index c6a338c..dcac245 100644 --- a/src/Thalos.DAL/Contracts/IdentityPolicyLookupRequest.cs +++ b/src/Thalos.DAL/Contracts/IdentityPolicyLookupRequest.cs @@ -1,3 +1,5 @@ +using BuildingBlock.Identity.Contracts.Conventions; + namespace Thalos.DAL.Contracts; /// @@ -7,8 +9,10 @@ namespace Thalos.DAL.Contracts; /// Identity subject identifier. /// Tenant scope identifier. /// Permission code to evaluate. +/// Auth provider for the lookup flow. public sealed record IdentityPolicyLookupRequest( IdentityContractEnvelope Envelope, string SubjectId, string TenantId, - string PermissionCode); + string PermissionCode, + IdentityAuthProvider Provider = IdentityAuthProvider.InternalJwt); diff --git a/src/Thalos.DAL/Contracts/IdentityPolicyRecord.cs b/src/Thalos.DAL/Contracts/IdentityPolicyRecord.cs index 4a47f3e..84f80d9 100644 --- a/src/Thalos.DAL/Contracts/IdentityPolicyRecord.cs +++ b/src/Thalos.DAL/Contracts/IdentityPolicyRecord.cs @@ -1,3 +1,5 @@ +using BuildingBlock.Identity.Contracts.Conventions; + namespace Thalos.DAL.Contracts; /// @@ -7,8 +9,10 @@ namespace Thalos.DAL.Contracts; /// Identity subject identifier. /// Permission code evaluated. /// Indicates whether policy context is satisfied. +/// Auth provider used for policy evaluation. public sealed record IdentityPolicyRecord( IdentityContractEnvelope Envelope, string SubjectId, string PermissionCode, - bool ContextSatisfied); + bool ContextSatisfied, + IdentityAuthProvider Provider = IdentityAuthProvider.InternalJwt); diff --git a/src/Thalos.DAL/Contracts/IdentityTokenLookupRequest.cs b/src/Thalos.DAL/Contracts/IdentityTokenLookupRequest.cs index 034118d..0d1d78e 100644 --- a/src/Thalos.DAL/Contracts/IdentityTokenLookupRequest.cs +++ b/src/Thalos.DAL/Contracts/IdentityTokenLookupRequest.cs @@ -1,3 +1,5 @@ +using BuildingBlock.Identity.Contracts.Conventions; + namespace Thalos.DAL.Contracts; /// @@ -6,4 +8,11 @@ namespace Thalos.DAL.Contracts; /// Contract envelope metadata. /// Identity subject identifier. /// Tenant scope identifier. -public sealed record IdentityTokenLookupRequest(IdentityContractEnvelope Envelope, string SubjectId, string TenantId); +/// Auth provider for the lookup flow. +/// External provider token when applicable. +public sealed record IdentityTokenLookupRequest( + IdentityContractEnvelope Envelope, + string SubjectId, + string TenantId, + IdentityAuthProvider Provider = IdentityAuthProvider.InternalJwt, + string ExternalToken = ""); diff --git a/src/Thalos.DAL/Contracts/IdentityTokenRecord.cs b/src/Thalos.DAL/Contracts/IdentityTokenRecord.cs index e932ff4..2958a22 100644 --- a/src/Thalos.DAL/Contracts/IdentityTokenRecord.cs +++ b/src/Thalos.DAL/Contracts/IdentityTokenRecord.cs @@ -1,3 +1,5 @@ +using BuildingBlock.Identity.Contracts.Conventions; + namespace Thalos.DAL.Contracts; /// @@ -8,9 +10,11 @@ namespace Thalos.DAL.Contracts; /// Tenant scope identifier. /// Issued access token value. /// Token expiration in seconds. +/// Auth provider used for token issuance. public sealed record IdentityTokenRecord( IdentityContractEnvelope Envelope, string SubjectId, string TenantId, string Token, - int ExpiresInSeconds); + int ExpiresInSeconds, + IdentityAuthProvider Provider = IdentityAuthProvider.InternalJwt); diff --git a/src/Thalos.DAL/Contracts/IdentityUserLookupRequest.cs b/src/Thalos.DAL/Contracts/IdentityUserLookupRequest.cs index 8e6d4e4..3e77f55 100644 --- a/src/Thalos.DAL/Contracts/IdentityUserLookupRequest.cs +++ b/src/Thalos.DAL/Contracts/IdentityUserLookupRequest.cs @@ -1,3 +1,5 @@ +using BuildingBlock.Identity.Contracts.Conventions; + namespace Thalos.DAL.Contracts; /// @@ -5,4 +7,12 @@ namespace Thalos.DAL.Contracts; /// /// Contract envelope metadata. /// Identity subject identifier. -public sealed record IdentityUserLookupRequest(IdentityContractEnvelope Envelope, string SubjectId); +/// Tenant identifier. +/// Auth provider for the lookup flow. +/// External provider token when applicable. +public sealed record IdentityUserLookupRequest( + IdentityContractEnvelope Envelope, + string SubjectId, + string TenantId, + IdentityAuthProvider Provider = IdentityAuthProvider.InternalJwt, + string ExternalToken = ""); diff --git a/src/Thalos.DAL/Contracts/IdentityUserRecord.cs b/src/Thalos.DAL/Contracts/IdentityUserRecord.cs index aa364ac..095f3c5 100644 --- a/src/Thalos.DAL/Contracts/IdentityUserRecord.cs +++ b/src/Thalos.DAL/Contracts/IdentityUserRecord.cs @@ -7,8 +7,14 @@ namespace Thalos.DAL.Contracts; /// Identity subject identifier. /// Tenant scope identifier. /// Current user status. +/// Persisted token projection for subject/tenant. +/// Persisted token expiration in seconds. +/// Persisted policy context projection. public sealed record IdentityUserRecord( IdentityContractEnvelope Envelope, string SubjectId, string TenantId, - string Status); + string Status, + string Token, + int ExpiresInSeconds, + bool ContextSatisfied); diff --git a/src/Thalos.DAL/DependencyInjection/ThalosDalServiceCollectionExtensions.cs b/src/Thalos.DAL/DependencyInjection/ThalosDalServiceCollectionExtensions.cs new file mode 100644 index 0000000..caf339b --- /dev/null +++ b/src/Thalos.DAL/DependencyInjection/ThalosDalServiceCollectionExtensions.cs @@ -0,0 +1,46 @@ +using Core.Blueprint.Common.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Thalos.DAL.Adapters; +using Thalos.DAL.Health; +using Thalos.DAL.Providers; +using Thalos.DAL.Providers.InMemory; +using Thalos.DAL.Repositories; + +namespace Thalos.DAL.DependencyInjection; + +/// +/// Registers thalos dal runtime provider, repository, and adapter implementations. +/// +public static class ThalosDalServiceCollectionExtensions +{ + /// + /// Adds thalos dal runtime implementations aligned with blueprint runtime core. + /// + /// Service collection. + /// Service collection for fluent chaining. + public static IServiceCollection AddThalosDalRuntime(this IServiceCollection services) + { + services.AddBlueprintRuntimeCore(); + + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + + return services; + } +} diff --git a/src/Thalos.DAL/Health/DalDependencyHealthCheck.cs b/src/Thalos.DAL/Health/DalDependencyHealthCheck.cs new file mode 100644 index 0000000..e6a9f0e --- /dev/null +++ b/src/Thalos.DAL/Health/DalDependencyHealthCheck.cs @@ -0,0 +1,27 @@ +using Core.Blueprint.Common.Runtime; +using Thalos.DAL.Contracts; + +namespace Thalos.DAL.Health; + +/// +/// Default DAL dependency health check implementation. +/// +public sealed class DalDependencyHealthCheck(IBlueprintSystemClock clock) : IDalDependencyHealthCheck +{ + /// + public Task CheckAsync(CancellationToken cancellationToken = default) + { + var envelope = new IdentityContractEnvelope("1.0.0", $"corr-{clock.UtcNow:yyyyMMddHHmmssfff}"); + IReadOnlyList dependencyNames = + [ + "IUserDataProvider", + "IRoleDataProvider", + "IPermissionDataProvider", + "IModuleDataProvider", + "ITenantDataProvider" + ]; + + var status = new DalDependencyHealthStatus(envelope, true, dependencyNames); + return Task.FromResult(status); + } +} diff --git a/src/Thalos.DAL/Providers/InMemory/InMemoryModuleDataProvider.cs b/src/Thalos.DAL/Providers/InMemory/InMemoryModuleDataProvider.cs new file mode 100644 index 0000000..db80e20 --- /dev/null +++ b/src/Thalos.DAL/Providers/InMemory/InMemoryModuleDataProvider.cs @@ -0,0 +1,22 @@ +using Thalos.DAL.Contracts; + +namespace Thalos.DAL.Providers.InMemory; + +/// +/// In-memory provider for identity module lookup contracts. +/// +public sealed class InMemoryModuleDataProvider : IModuleDataProvider +{ + /// + public Task> ReadModulesAsync( + IdentityModuleLookupRequest request, + CancellationToken cancellationToken = default) + { + IReadOnlyList records = + [ + new IdentityModuleRecord(request.Envelope, "identity", true) + ]; + + return Task.FromResult(records); + } +} diff --git a/src/Thalos.DAL/Providers/InMemory/InMemoryPermissionDataProvider.cs b/src/Thalos.DAL/Providers/InMemory/InMemoryPermissionDataProvider.cs new file mode 100644 index 0000000..5cb7626 --- /dev/null +++ b/src/Thalos.DAL/Providers/InMemory/InMemoryPermissionDataProvider.cs @@ -0,0 +1,23 @@ +using Thalos.DAL.Contracts; + +namespace Thalos.DAL.Providers.InMemory; + +/// +/// In-memory provider for identity permission lookup contracts. +/// +public sealed class InMemoryPermissionDataProvider : IPermissionDataProvider +{ + /// + public Task> ReadPermissionsAsync( + IdentityPermissionSetLookupRequest request, + CancellationToken cancellationToken = default) + { + IReadOnlyList records = + [ + new IdentityPermissionRecord(request.Envelope, "identity.token.issue", "identity.admin"), + new IdentityPermissionRecord(request.Envelope, "identity.policy.evaluate", "identity.admin") + ]; + + return Task.FromResult(records); + } +} diff --git a/src/Thalos.DAL/Providers/InMemory/InMemoryRoleDataProvider.cs b/src/Thalos.DAL/Providers/InMemory/InMemoryRoleDataProvider.cs new file mode 100644 index 0000000..2b7f3ac --- /dev/null +++ b/src/Thalos.DAL/Providers/InMemory/InMemoryRoleDataProvider.cs @@ -0,0 +1,22 @@ +using Thalos.DAL.Contracts; + +namespace Thalos.DAL.Providers.InMemory; + +/// +/// In-memory provider for identity role lookup contracts. +/// +public sealed class InMemoryRoleDataProvider : IRoleDataProvider +{ + /// + public Task> ReadRolesAsync( + IdentityRoleLookupRequest request, + CancellationToken cancellationToken = default) + { + IReadOnlyList records = + [ + new IdentityRoleRecord(request.Envelope, "identity.admin", request.TenantId) + ]; + + return Task.FromResult(records); + } +} diff --git a/src/Thalos.DAL/Providers/InMemory/InMemoryTenantDataProvider.cs b/src/Thalos.DAL/Providers/InMemory/InMemoryTenantDataProvider.cs new file mode 100644 index 0000000..e61cef3 --- /dev/null +++ b/src/Thalos.DAL/Providers/InMemory/InMemoryTenantDataProvider.cs @@ -0,0 +1,23 @@ +using Thalos.DAL.Contracts; + +namespace Thalos.DAL.Providers.InMemory; + +/// +/// In-memory provider for identity tenant lookup contracts. +/// +public sealed class InMemoryTenantDataProvider : ITenantDataProvider +{ + /// + public Task ReadTenantAsync( + IdentityTenantLookupRequest request, + CancellationToken cancellationToken = default) + { + var record = new IdentityTenantRecord( + request.Envelope, + request.TenantId, + $"tenant-{request.TenantId}", + true); + + return Task.FromResult(record); + } +} diff --git a/src/Thalos.DAL/Providers/InMemory/InMemoryUserDataProvider.cs b/src/Thalos.DAL/Providers/InMemory/InMemoryUserDataProvider.cs new file mode 100644 index 0000000..66cd15d --- /dev/null +++ b/src/Thalos.DAL/Providers/InMemory/InMemoryUserDataProvider.cs @@ -0,0 +1,31 @@ +using Thalos.DAL.Contracts; + +namespace Thalos.DAL.Providers.InMemory; + +/// +/// In-memory provider for identity user lookup contracts. +/// +public sealed class InMemoryUserDataProvider : IUserDataProvider +{ + /// + public Task ReadUserAsync( + IdentityUserLookupRequest request, + CancellationToken cancellationToken = default) + { + if (request.SubjectId.StartsWith("missing-", StringComparison.OrdinalIgnoreCase)) + { + return Task.FromResult(null); + } + + var record = new IdentityUserRecord( + request.Envelope, + request.SubjectId, + request.TenantId, + "active", + $"{request.SubjectId}:{request.TenantId}:token", + 1800, + true); + + return Task.FromResult(record); + } +} diff --git a/src/Thalos.DAL/Providers/ProviderPermissionDataProviders.cs b/src/Thalos.DAL/Providers/ProviderPermissionDataProviders.cs new file mode 100644 index 0000000..ba59994 --- /dev/null +++ b/src/Thalos.DAL/Providers/ProviderPermissionDataProviders.cs @@ -0,0 +1,89 @@ +using BuildingBlock.Identity.Contracts.Conventions; +using Thalos.DAL.Contracts; + +namespace Thalos.DAL.Providers; + +/// +/// Internal JWT permission provider implementation. +/// +public sealed class InternalJwtPermissionDataProvider : IPermissionDataProvider +{ + /// + public Task> ReadPermissionsAsync( + IdentityPermissionSetLookupRequest request, + CancellationToken cancellationToken = default) + { + IReadOnlyList records = + [ + new IdentityPermissionRecord(request.Envelope, "identity.token.issue", "identity.admin", IdentityAuthProvider.InternalJwt), + new IdentityPermissionRecord(request.Envelope, "identity.policy.evaluate", "identity.admin", IdentityAuthProvider.InternalJwt) + ]; + + return Task.FromResult(records); + } +} + +/// +/// Azure AD permission provider implementation. +/// +public sealed class AzureAdPermissionDataProvider : IPermissionDataProvider +{ + /// + public Task> ReadPermissionsAsync( + IdentityPermissionSetLookupRequest request, + CancellationToken cancellationToken = default) + { + IReadOnlyList records = + [ + new IdentityPermissionRecord(request.Envelope, "identity.token.issue", "identity.azure.user", IdentityAuthProvider.AzureAd), + new IdentityPermissionRecord(request.Envelope, "identity.policy.evaluate", "identity.azure.user", IdentityAuthProvider.AzureAd), + new IdentityPermissionRecord(request.Envelope, "identity.oauth.exchange", "identity.azure.user", IdentityAuthProvider.AzureAd) + ]; + + return Task.FromResult(records); + } +} + +/// +/// Google permission provider implementation. +/// +public sealed class GooglePermissionDataProvider : IPermissionDataProvider +{ + /// + public Task> ReadPermissionsAsync( + IdentityPermissionSetLookupRequest request, + CancellationToken cancellationToken = default) + { + IReadOnlyList records = + [ + new IdentityPermissionRecord(request.Envelope, "identity.token.issue", "identity.google.user", IdentityAuthProvider.Google), + new IdentityPermissionRecord(request.Envelope, "identity.policy.evaluate", "identity.google.user", IdentityAuthProvider.Google), + new IdentityPermissionRecord(request.Envelope, "identity.oauth.exchange", "identity.google.user", IdentityAuthProvider.Google) + ]; + + return Task.FromResult(records); + } +} + +/// +/// Routes permission lookups to the matching provider implementation. +/// +public sealed class RoutedPermissionDataProvider( + InternalJwtPermissionDataProvider internalJwtProvider, + AzureAdPermissionDataProvider azureProvider, + GooglePermissionDataProvider googleProvider) : IPermissionDataProvider +{ + /// + public Task> ReadPermissionsAsync( + IdentityPermissionSetLookupRequest request, + CancellationToken cancellationToken = default) + { + return request.Provider switch + { + IdentityAuthProvider.InternalJwt => internalJwtProvider.ReadPermissionsAsync(request, cancellationToken), + IdentityAuthProvider.AzureAd => azureProvider.ReadPermissionsAsync(request, cancellationToken), + IdentityAuthProvider.Google => googleProvider.ReadPermissionsAsync(request, cancellationToken), + _ => Task.FromResult>([]) + }; + } +} diff --git a/src/Thalos.DAL/Providers/ProviderUserDataProviders.cs b/src/Thalos.DAL/Providers/ProviderUserDataProviders.cs new file mode 100644 index 0000000..3905fd2 --- /dev/null +++ b/src/Thalos.DAL/Providers/ProviderUserDataProviders.cs @@ -0,0 +1,143 @@ +using BuildingBlock.Identity.Contracts.Conventions; +using Thalos.DAL.Contracts; + +namespace Thalos.DAL.Providers; + +/// +/// Internal JWT provider implementation for identity user reads. +/// +public sealed class InternalJwtUserDataProvider : IUserDataProvider +{ + /// + public Task ReadUserAsync( + IdentityUserLookupRequest request, + CancellationToken cancellationToken = default) + { + if (request.SubjectId.StartsWith("missing-", StringComparison.OrdinalIgnoreCase)) + { + return Task.FromResult(null); + } + + var record = new IdentityUserRecord( + request.Envelope, + request.SubjectId, + request.TenantId, + "active", + $"{request.SubjectId}:{request.TenantId}:token", + 1800, + true); + + return Task.FromResult(record); + } +} + +/// +/// Azure AD provider implementation for identity user reads. +/// +public sealed class AzureAdUserDataProvider : IUserDataProvider +{ + /// + public Task ReadUserAsync( + IdentityUserLookupRequest request, + CancellationToken cancellationToken = default) + { + var subjectId = ResolveSubjectId(request, "azure-sub"); + if (string.IsNullOrWhiteSpace(subjectId)) + { + return Task.FromResult(null); + } + + var record = new IdentityUserRecord( + request.Envelope, + subjectId, + request.TenantId, + "active", + $"azure:{subjectId}:{request.TenantId}:token", + 3600, + true); + + return Task.FromResult(record); + } + + private static string ResolveSubjectId(IdentityUserLookupRequest request, string prefix) + { + if (!string.IsNullOrWhiteSpace(request.SubjectId)) + { + return request.SubjectId; + } + + if (string.IsNullOrWhiteSpace(request.ExternalToken)) + { + return string.Empty; + } + + return $"{prefix}-{Math.Abs(request.ExternalToken.GetHashCode(StringComparison.Ordinal))}"; + } +} + +/// +/// Google provider implementation for identity user reads. +/// +public sealed class GoogleUserDataProvider : IUserDataProvider +{ + /// + public Task ReadUserAsync( + IdentityUserLookupRequest request, + CancellationToken cancellationToken = default) + { + var subjectId = ResolveSubjectId(request, "google-sub"); + if (string.IsNullOrWhiteSpace(subjectId)) + { + return Task.FromResult(null); + } + + var record = new IdentityUserRecord( + request.Envelope, + subjectId, + request.TenantId, + "active", + $"google:{subjectId}:{request.TenantId}:token", + 3000, + true); + + return Task.FromResult(record); + } + + private static string ResolveSubjectId(IdentityUserLookupRequest request, string prefix) + { + if (!string.IsNullOrWhiteSpace(request.SubjectId)) + { + return request.SubjectId; + } + + if (string.IsNullOrWhiteSpace(request.ExternalToken)) + { + return string.Empty; + } + + return $"{prefix}-{Math.Abs(request.ExternalToken.GetHashCode(StringComparison.Ordinal))}"; + } +} + +/// +/// Routes user lookups to the matching provider implementation. +/// +public sealed class RoutedUserDataProvider( + InternalJwtUserDataProvider internalJwtProvider, + AzureAdUserDataProvider azureProvider, + GoogleUserDataProvider googleProvider) : IUserDataProvider +{ + /// + public Task ReadUserAsync( + IdentityUserLookupRequest request, + CancellationToken cancellationToken = default) + { + return request.Provider switch + { + IdentityAuthProvider.InternalJwt => internalJwtProvider.ReadUserAsync(request, cancellationToken), + IdentityAuthProvider.AzureAd => azureProvider.ReadUserAsync(request, cancellationToken), + IdentityAuthProvider.Google => googleProvider.ReadUserAsync(request, cancellationToken), + _ => Task.FromResult(null) + }; + } +} diff --git a/src/Thalos.DAL/Repositories/IdentityRepository.cs b/src/Thalos.DAL/Repositories/IdentityRepository.cs new file mode 100644 index 0000000..75ea161 --- /dev/null +++ b/src/Thalos.DAL/Repositories/IdentityRepository.cs @@ -0,0 +1,70 @@ +using Thalos.DAL.Contracts; +using Thalos.DAL.Providers; + +namespace Thalos.DAL.Repositories; + +/// +/// Default identity repository implementation composed from DAL providers. +/// +public sealed class IdentityRepository( + IUserDataProvider userDataProvider, + IPermissionDataProvider permissionDataProvider) : IIdentityRepository +{ + /// + public async Task ReadIdentityTokenAsync( + IdentityTokenLookupRequest request, + CancellationToken cancellationToken = default) + { + var userRequest = new IdentityUserLookupRequest( + request.Envelope, + request.SubjectId, + request.TenantId, + request.Provider, + request.ExternalToken); + var userRecord = await userDataProvider.ReadUserAsync(userRequest, cancellationToken); + if (userRecord is null) + { + return null; + } + + return new IdentityTokenRecord( + request.Envelope, + userRecord.SubjectId, + request.TenantId, + userRecord.Token, + userRecord.ExpiresInSeconds, + request.Provider); + } + + /// + public async Task ReadIdentityPolicyAsync( + IdentityPolicyLookupRequest request, + CancellationToken cancellationToken = default) + { + var userRequest = new IdentityUserLookupRequest( + request.Envelope, + request.SubjectId, + request.TenantId, + request.Provider); + var userRecord = await userDataProvider.ReadUserAsync(userRequest, cancellationToken); + if (userRecord is null) + { + return null; + } + + return new IdentityPolicyRecord( + request.Envelope, + userRecord.SubjectId, + request.PermissionCode, + userRecord.ContextSatisfied, + request.Provider); + } + + /// + public Task> ReadPermissionSetAsync( + IdentityPermissionSetLookupRequest request, + CancellationToken cancellationToken = default) + { + return permissionDataProvider.ReadPermissionsAsync(request, cancellationToken); + } +} diff --git a/src/Thalos.DAL/Thalos.DAL.csproj b/src/Thalos.DAL/Thalos.DAL.csproj index 04a4bbc..4eb2668 100644 --- a/src/Thalos.DAL/Thalos.DAL.csproj +++ b/src/Thalos.DAL/Thalos.DAL.csproj @@ -5,6 +5,8 @@ enable + + diff --git a/tests/Thalos.DAL.UnitTests/ContractShapeTests.cs b/tests/Thalos.DAL.UnitTests/ContractShapeTests.cs index 58949ba..358e158 100644 --- a/tests/Thalos.DAL.UnitTests/ContractShapeTests.cs +++ b/tests/Thalos.DAL.UnitTests/ContractShapeTests.cs @@ -16,6 +16,7 @@ public class ContractShapeTests Assert.Equal("user-1", request.SubjectId); Assert.Equal("tenant-1", request.TenantId); Assert.Equal("identity.token.issue", request.PermissionCode); + Assert.Equal(BuildingBlock.Identity.Contracts.Conventions.IdentityAuthProvider.InternalJwt, request.Provider); } [Fact] @@ -30,6 +31,7 @@ public class ContractShapeTests Assert.Equal("tenant-1", record.TenantId); Assert.Equal("token-xyz", record.Token); Assert.Equal(1800, record.ExpiresInSeconds); + Assert.Equal(BuildingBlock.Identity.Contracts.Conventions.IdentityAuthProvider.InternalJwt, record.Provider); } [Fact] diff --git a/tests/Thalos.DAL.UnitTests/RuntimeWiringTests.cs b/tests/Thalos.DAL.UnitTests/RuntimeWiringTests.cs new file mode 100644 index 0000000..a07cb27 --- /dev/null +++ b/tests/Thalos.DAL.UnitTests/RuntimeWiringTests.cs @@ -0,0 +1,88 @@ +using Microsoft.Extensions.DependencyInjection; +using Thalos.DAL.Adapters; +using Thalos.DAL.Contracts; +using Thalos.DAL.DependencyInjection; +using Thalos.DAL.Health; +using Thalos.DAL.Repositories; + +namespace Thalos.DAL.UnitTests; + +public class RuntimeWiringTests +{ + [Fact] + public async Task AddThalosDalRuntime_WhenResolved_WiresRepositoryAndProviders() + { + var services = new ServiceCollection(); + services.AddThalosDalRuntime(); + + using var provider = services.BuildServiceProvider(); + var repository = provider.GetRequiredService(); + var request = new IdentityTokenLookupRequest( + new IdentityContractEnvelope("1.0.0", "corr-123"), + "user-1", + "tenant-1"); + + var response = await repository.ReadIdentityTokenAsync(request); + + Assert.NotNull(response); + Assert.Equal("user-1", response.SubjectId); + Assert.Equal("tenant-1", response.TenantId); + Assert.Equal(1800, response.ExpiresInSeconds); + Assert.Equal(BuildingBlock.Identity.Contracts.Conventions.IdentityAuthProvider.InternalJwt, response.Provider); + } + + [Fact] + public async Task AddThalosDalRuntime_WhenExternalProviderUsed_ResolvesProviderSpecificToken() + { + var services = new ServiceCollection(); + services.AddThalosDalRuntime(); + + using var provider = services.BuildServiceProvider(); + var repository = provider.GetRequiredService(); + var request = new IdentityTokenLookupRequest( + new IdentityContractEnvelope("1.0.0", "corr-ext"), + string.Empty, + "tenant-2", + BuildingBlock.Identity.Contracts.Conventions.IdentityAuthProvider.AzureAd, + "external-azure-token"); + + var response = await repository.ReadIdentityTokenAsync(request); + + Assert.NotNull(response); + Assert.Equal(BuildingBlock.Identity.Contracts.Conventions.IdentityAuthProvider.AzureAd, response.Provider); + Assert.StartsWith("azure:", response.Token); + } + + [Fact] + public void AddThalosDalRuntime_WhenResolved_WiresGrpcContractAdapter() + { + var services = new ServiceCollection(); + services.AddThalosDalRuntime(); + + using var provider = services.BuildServiceProvider(); + var adapter = provider.GetRequiredService(); + var grpcContract = new Thalos.DAL.Grpc.IdentityTokenDalGrpcContract("user-2", "tenant-2"); + + var request = adapter.FromGrpcTokenRequest(grpcContract); + + Assert.Equal("user-2", request.SubjectId); + Assert.Equal("tenant-2", request.TenantId); + Assert.NotEmpty(request.Envelope.CorrelationId); + } + + [Fact] + public async Task AddThalosDalRuntime_WhenResolved_WiresDependencyHealthCheck() + { + var services = new ServiceCollection(); + services.AddThalosDalRuntime(); + + using var provider = services.BuildServiceProvider(); + var healthCheck = provider.GetRequiredService(); + + var status = await healthCheck.CheckAsync(); + + Assert.True(status.IsHealthy); + Assert.Contains("IUserDataProvider", status.DependencyNames); + Assert.Contains("IPermissionDataProvider", status.DependencyNames); + } +} diff --git a/tests/Thalos.DAL.UnitTests/Thalos.DAL.UnitTests.csproj b/tests/Thalos.DAL.UnitTests/Thalos.DAL.UnitTests.csproj index 202abae..8f4a236 100644 --- a/tests/Thalos.DAL.UnitTests/Thalos.DAL.UnitTests.csproj +++ b/tests/Thalos.DAL.UnitTests/Thalos.DAL.UnitTests.csproj @@ -7,6 +7,7 @@ +