merge(thalos-domain): integrate domain policy baseline

This commit is contained in:
José René White Enciso 2026-02-25 14:17:54 -06:00
commit ba834a4c4d
14 changed files with 306 additions and 0 deletions

6
.gitignore vendored
View File

@ -1,2 +1,8 @@
.tasks/
.agile/
bin/
obj/
TestResults/
.vs/
*.user
*.suo

8
Thalos.Domain.slnx Normal file
View File

@ -0,0 +1,8 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/Thalos.Domain/Thalos.Domain.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/Thalos.Domain.UnitTests/Thalos.Domain.UnitTests.csproj" />
</Folder>
</Solution>

View File

@ -3,6 +3,9 @@
## Invariants
- Equivalent policy inputs produce equivalent policy decisions.
- Token decision fallback behavior remains stable until explicitly revised.
- Provider semantics are explicit:
- `InternalJwt`: standard identity permission evaluation.
- `AzureAd` and `Google`: policy permission must remain within `identity.*` scope.
- Service transport contracts remain stable during domain extraction.
## Validation Approach

View File

@ -0,0 +1,18 @@
using BuildingBlock.Identity.Contracts.Conventions;
namespace Thalos.Domain.Contracts;
/// <summary>
/// Domain input describing policy context and granted permissions for evaluation.
/// </summary>
/// <param name="SubjectId">Identity subject identifier.</param>
/// <param name="PermissionCode">Permission code being evaluated.</param>
/// <param name="Provider">Auth provider used in the request flow.</param>
/// <param name="ContextSatisfied">Whether contextual constraints are satisfied.</param>
/// <param name="GrantedPermissions">Permissions granted to subject in tenant scope.</param>
public sealed record IdentityPolicyContextData(
string SubjectId,
string PermissionCode,
IdentityAuthProvider Provider,
bool ContextSatisfied,
IReadOnlyCollection<string> GrantedPermissions);

View File

@ -0,0 +1,14 @@
using BuildingBlock.Identity.Contracts.Conventions;
namespace Thalos.Domain.Contracts;
/// <summary>
/// Domain input describing issued token projection from technical persistence.
/// </summary>
/// <param name="Token">Token value, if found.</param>
/// <param name="ExpiresInSeconds">Token lifetime, if found.</param>
/// <param name="Provider">Auth provider used in the issuance flow.</param>
public sealed record IdentityTokenData(
string? Token,
int? ExpiresInSeconds,
IdentityAuthProvider Provider = IdentityAuthProvider.InternalJwt);

View File

@ -0,0 +1,8 @@
namespace Thalos.Domain.Conventions;
/// <summary>
/// Marker type for thalos-domain package discovery.
/// </summary>
public sealed class ThalosDomainPackageContract
{
}

View File

@ -0,0 +1,23 @@
using BuildingBlock.Identity.Contracts.Requests;
using BuildingBlock.Identity.Contracts.Responses;
using Thalos.Domain.Contracts;
namespace Thalos.Domain.Decisions;
/// <summary>
/// Defines domain decision boundary for identity policy workflows.
/// </summary>
public interface IIdentityPolicyDecisionService
{
/// <summary>
/// Builds policy context request from policy evaluation request.
/// </summary>
IdentityPolicyContextRequest BuildPolicyContextRequest(EvaluateIdentityPolicyRequest request);
/// <summary>
/// Evaluates policy response from contextual data.
/// </summary>
EvaluateIdentityPolicyResponse Evaluate(
EvaluateIdentityPolicyRequest request,
IdentityPolicyContextData policyContextData);
}

View File

@ -0,0 +1,15 @@
using BuildingBlock.Identity.Contracts.Responses;
using Thalos.Domain.Contracts;
namespace Thalos.Domain.Decisions;
/// <summary>
/// Defines domain decision boundary for identity token issuance semantics.
/// </summary>
public interface IIdentityTokenDecisionService
{
/// <summary>
/// Builds token response from technical token data using domain fallback policy.
/// </summary>
IssueIdentityTokenResponse BuildIssuedTokenResponse(IdentityTokenData tokenData);
}

View File

@ -0,0 +1,59 @@
using BuildingBlock.Identity.Contracts.Requests;
using BuildingBlock.Identity.Contracts.Responses;
using BuildingBlock.Identity.Contracts.Conventions;
using Thalos.Domain.Contracts;
namespace Thalos.Domain.Decisions;
/// <summary>
/// Default domain implementation for identity policy decision workflows.
/// </summary>
public sealed class IdentityPolicyDecisionService : IIdentityPolicyDecisionService
{
/// <inheritdoc />
public IdentityPolicyContextRequest BuildPolicyContextRequest(EvaluateIdentityPolicyRequest request)
{
return new IdentityPolicyContextRequest(
request.SubjectId,
request.TenantId,
request.PermissionCode,
request.Provider);
}
/// <inheritdoc />
public EvaluateIdentityPolicyResponse Evaluate(
EvaluateIdentityPolicyRequest request,
IdentityPolicyContextData policyContextData)
{
var permissionMatched = policyContextData.GrantedPermissions.Any(permission =>
string.Equals(permission, request.PermissionCode, StringComparison.OrdinalIgnoreCase));
var providerSatisfied = IsProviderContextSatisfied(request.Provider, policyContextData);
return new EvaluateIdentityPolicyResponse(
request.SubjectId,
request.PermissionCode,
providerSatisfied && permissionMatched);
}
private static bool IsProviderContextSatisfied(
IdentityAuthProvider provider,
IdentityPolicyContextData policyContextData)
{
if (!policyContextData.ContextSatisfied)
{
return false;
}
return provider switch
{
IdentityAuthProvider.InternalJwt => true,
IdentityAuthProvider.AzureAd => policyContextData.PermissionCode.StartsWith(
"identity.",
StringComparison.OrdinalIgnoreCase),
IdentityAuthProvider.Google => policyContextData.PermissionCode.StartsWith(
"identity.",
StringComparison.OrdinalIgnoreCase),
_ => false
};
}
}

View File

@ -0,0 +1,21 @@
using BuildingBlock.Identity.Contracts.Responses;
using Thalos.Domain.Contracts;
namespace Thalos.Domain.Decisions;
/// <summary>
/// Default domain implementation for token issuance fallback semantics.
/// </summary>
public sealed class IdentityTokenDecisionService : IIdentityTokenDecisionService
{
/// <inheritdoc />
public IssueIdentityTokenResponse BuildIssuedTokenResponse(IdentityTokenData tokenData)
{
if (string.IsNullOrWhiteSpace(tokenData.Token) || !tokenData.ExpiresInSeconds.HasValue)
{
return new IssueIdentityTokenResponse(string.Empty, 0);
}
return new IssueIdentityTokenResponse(tokenData.Token, tokenData.ExpiresInSeconds.Value);
}
}

View File

@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\building-block-identity\src\BuildingBlock.Identity.Contracts\BuildingBlock.Identity.Contracts.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,72 @@
using BuildingBlock.Identity.Contracts.Requests;
using BuildingBlock.Identity.Contracts.Conventions;
using Thalos.Domain.Contracts;
using Thalos.Domain.Decisions;
namespace Thalos.Domain.UnitTests;
public class IdentityPolicyDecisionServiceTests
{
[Fact]
public void Evaluate_WhenPermissionMatchedAndContextSatisfied_ReturnsAllowed()
{
var service = new IdentityPolicyDecisionService();
var request = new EvaluateIdentityPolicyRequest(
"user-1",
"tenant-1",
"identity.token.issue",
IdentityAuthProvider.InternalJwt);
var context = new IdentityPolicyContextData(
request.SubjectId,
request.PermissionCode,
request.Provider,
true,
["identity.token.issue", "identity.policy.evaluate"]);
var response = service.Evaluate(request, context);
Assert.True(response.IsAllowed);
}
[Fact]
public void Evaluate_WhenPermissionMissing_ReturnsDenied()
{
var service = new IdentityPolicyDecisionService();
var request = new EvaluateIdentityPolicyRequest(
"user-1",
"tenant-1",
"identity.token.issue",
IdentityAuthProvider.InternalJwt);
var context = new IdentityPolicyContextData(
request.SubjectId,
request.PermissionCode,
request.Provider,
true,
["identity.read"]);
var response = service.Evaluate(request, context);
Assert.False(response.IsAllowed);
}
[Fact]
public void Evaluate_WhenProviderIsExternalAndPermissionPrefixInvalid_ReturnsDenied()
{
var service = new IdentityPolicyDecisionService();
var request = new EvaluateIdentityPolicyRequest(
"user-2",
"tenant-2",
"catalog.read",
IdentityAuthProvider.AzureAd);
var context = new IdentityPolicyContextData(
request.SubjectId,
request.PermissionCode,
request.Provider,
true,
["catalog.read"]);
var response = service.Evaluate(request, context);
Assert.False(response.IsAllowed);
}
}

View File

@ -0,0 +1,29 @@
using Thalos.Domain.Contracts;
using Thalos.Domain.Decisions;
namespace Thalos.Domain.UnitTests;
public class IdentityTokenDecisionServiceTests
{
[Fact]
public void BuildIssuedTokenResponse_WhenTokenMissing_ReturnsFallbackShape()
{
var service = new IdentityTokenDecisionService();
var response = service.BuildIssuedTokenResponse(new IdentityTokenData(null, null));
Assert.Equal(string.Empty, response.Token);
Assert.Equal(0, response.ExpiresInSeconds);
}
[Fact]
public void BuildIssuedTokenResponse_WhenTokenExists_ReturnsIssuedToken()
{
var service = new IdentityTokenDecisionService();
var response = service.BuildIssuedTokenResponse(new IdentityTokenData("token-123", 1800));
Assert.Equal("token-123", response.Token);
Assert.Equal(1800, response.ExpiresInSeconds);
}
}

View File

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Thalos.Domain\Thalos.Domain.csproj" />
</ItemGroup>
</Project>