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/
|
||||
.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
|
||||
- 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
|
||||
|
||||
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