feat(thalos-domain): add provider-aware policy invariants
This commit is contained in:
parent
a0d22d3ea2
commit
0430af9396
@ -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
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
using BuildingBlock.Identity.Contracts.Conventions;
|
||||
|
||||
namespace Thalos.Domain.Contracts;
|
||||
|
||||
/// <summary>
|
||||
@ -5,10 +7,12 @@ namespace Thalos.Domain.Contracts;
|
||||
/// </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);
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
using BuildingBlock.Identity.Contracts.Conventions;
|
||||
|
||||
namespace Thalos.Domain.Contracts;
|
||||
|
||||
/// <summary>
|
||||
@ -5,4 +7,8 @@ namespace Thalos.Domain.Contracts;
|
||||
/// </summary>
|
||||
/// <param name="Token">Token value, if found.</param>
|
||||
/// <param name="ExpiresInSeconds">Token lifetime, if found.</param>
|
||||
public sealed record IdentityTokenData(string? Token, int? ExpiresInSeconds);
|
||||
/// <param name="Provider">Auth provider used in the issuance flow.</param>
|
||||
public sealed record IdentityTokenData(
|
||||
string? Token,
|
||||
int? ExpiresInSeconds,
|
||||
IdentityAuthProvider Provider = IdentityAuthProvider.InternalJwt);
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
using BuildingBlock.Identity.Contracts.Requests;
|
||||
using BuildingBlock.Identity.Contracts.Responses;
|
||||
using BuildingBlock.Identity.Contracts.Conventions;
|
||||
using Thalos.Domain.Contracts;
|
||||
|
||||
namespace Thalos.Domain.Decisions;
|
||||
@ -12,7 +13,11 @@ public sealed class IdentityPolicyDecisionService : IIdentityPolicyDecisionServi
|
||||
/// <inheritdoc />
|
||||
public IdentityPolicyContextRequest BuildPolicyContextRequest(EvaluateIdentityPolicyRequest request)
|
||||
{
|
||||
return new IdentityPolicyContextRequest(request.SubjectId, request.TenantId, request.PermissionCode);
|
||||
return new IdentityPolicyContextRequest(
|
||||
request.SubjectId,
|
||||
request.TenantId,
|
||||
request.PermissionCode,
|
||||
request.Provider);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -22,10 +27,33 @@ public sealed class IdentityPolicyDecisionService : IIdentityPolicyDecisionServi
|
||||
{
|
||||
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,
|
||||
policyContextData.ContextSatisfied && permissionMatched);
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using BuildingBlock.Identity.Contracts.Requests;
|
||||
using BuildingBlock.Identity.Contracts.Conventions;
|
||||
using Thalos.Domain.Contracts;
|
||||
using Thalos.Domain.Decisions;
|
||||
|
||||
@ -10,10 +11,15 @@ public class IdentityPolicyDecisionServiceTests
|
||||
public void Evaluate_WhenPermissionMatchedAndContextSatisfied_ReturnsAllowed()
|
||||
{
|
||||
var service = new IdentityPolicyDecisionService();
|
||||
var request = new EvaluateIdentityPolicyRequest("user-1", "tenant-1", "identity.token.issue");
|
||||
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"]);
|
||||
|
||||
@ -26,8 +32,38 @@ public class IdentityPolicyDecisionServiceTests
|
||||
public void Evaluate_WhenPermissionMissing_ReturnsDenied()
|
||||
{
|
||||
var service = new IdentityPolicyDecisionService();
|
||||
var request = new EvaluateIdentityPolicyRequest("user-1", "tenant-1", "identity.token.issue");
|
||||
var context = new IdentityPolicyContextData(request.SubjectId, request.PermissionCode, true, ["identity.read"]);
|
||||
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);
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user