merge(thalos-domain): integrate domain policy baseline
This commit is contained in:
commit
ba834a4c4d
6
.gitignore
vendored
6
.gitignore
vendored
@ -1,2 +1,8 @@
|
|||||||
.tasks/
|
.tasks/
|
||||||
.agile/
|
.agile/
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
TestResults/
|
||||||
|
.vs/
|
||||||
|
*.user
|
||||||
|
*.suo
|
||||||
|
|||||||
8
Thalos.Domain.slnx
Normal file
8
Thalos.Domain.slnx
Normal 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>
|
||||||
@ -3,6 +3,9 @@
|
|||||||
## Invariants
|
## Invariants
|
||||||
- Equivalent policy inputs produce equivalent policy decisions.
|
- Equivalent policy inputs produce equivalent policy decisions.
|
||||||
- Token decision fallback behavior remains stable until explicitly revised.
|
- 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.
|
- Service transport contracts remain stable during domain extraction.
|
||||||
|
|
||||||
## Validation Approach
|
## Validation Approach
|
||||||
|
|||||||
18
src/Thalos.Domain/Contracts/IdentityPolicyContextData.cs
Normal file
18
src/Thalos.Domain/Contracts/IdentityPolicyContextData.cs
Normal 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);
|
||||||
14
src/Thalos.Domain/Contracts/IdentityTokenData.cs
Normal file
14
src/Thalos.Domain/Contracts/IdentityTokenData.cs
Normal 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);
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
namespace Thalos.Domain.Conventions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marker type for thalos-domain package discovery.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ThalosDomainPackageContract
|
||||||
|
{
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
15
src/Thalos.Domain/Decisions/IIdentityTokenDecisionService.cs
Normal file
15
src/Thalos.Domain/Decisions/IIdentityTokenDecisionService.cs
Normal 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);
|
||||||
|
}
|
||||||
59
src/Thalos.Domain/Decisions/IdentityPolicyDecisionService.cs
Normal file
59
src/Thalos.Domain/Decisions/IdentityPolicyDecisionService.cs
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/Thalos.Domain/Decisions/IdentityTokenDecisionService.cs
Normal file
21
src/Thalos.Domain/Decisions/IdentityTokenDecisionService.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/Thalos.Domain/Thalos.Domain.csproj
Normal file
10
src/Thalos.Domain/Thalos.Domain.csproj
Normal 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>
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
tests/Thalos.Domain.UnitTests/Thalos.Domain.UnitTests.csproj
Normal file
20
tests/Thalos.Domain.UnitTests/Thalos.Domain.UnitTests.csproj
Normal 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>
|
||||||
Loading…
Reference in New Issue
Block a user