feat(thalos-domain): add provider-aware policy invariants
This commit is contained in:
parent
a0d22d3ea2
commit
0430af9396
@ -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
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
using BuildingBlock.Identity.Contracts.Conventions;
|
||||||
|
|
||||||
namespace Thalos.Domain.Contracts;
|
namespace Thalos.Domain.Contracts;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -5,10 +7,12 @@ namespace Thalos.Domain.Contracts;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="SubjectId">Identity subject identifier.</param>
|
/// <param name="SubjectId">Identity subject identifier.</param>
|
||||||
/// <param name="PermissionCode">Permission code being evaluated.</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="ContextSatisfied">Whether contextual constraints are satisfied.</param>
|
||||||
/// <param name="GrantedPermissions">Permissions granted to subject in tenant scope.</param>
|
/// <param name="GrantedPermissions">Permissions granted to subject in tenant scope.</param>
|
||||||
public sealed record IdentityPolicyContextData(
|
public sealed record IdentityPolicyContextData(
|
||||||
string SubjectId,
|
string SubjectId,
|
||||||
string PermissionCode,
|
string PermissionCode,
|
||||||
|
IdentityAuthProvider Provider,
|
||||||
bool ContextSatisfied,
|
bool ContextSatisfied,
|
||||||
IReadOnlyCollection<string> GrantedPermissions);
|
IReadOnlyCollection<string> GrantedPermissions);
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
using BuildingBlock.Identity.Contracts.Conventions;
|
||||||
|
|
||||||
namespace Thalos.Domain.Contracts;
|
namespace Thalos.Domain.Contracts;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -5,4 +7,8 @@ namespace Thalos.Domain.Contracts;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="Token">Token value, if found.</param>
|
/// <param name="Token">Token value, if found.</param>
|
||||||
/// <param name="ExpiresInSeconds">Token lifetime, 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.Requests;
|
||||||
using BuildingBlock.Identity.Contracts.Responses;
|
using BuildingBlock.Identity.Contracts.Responses;
|
||||||
|
using BuildingBlock.Identity.Contracts.Conventions;
|
||||||
using Thalos.Domain.Contracts;
|
using Thalos.Domain.Contracts;
|
||||||
|
|
||||||
namespace Thalos.Domain.Decisions;
|
namespace Thalos.Domain.Decisions;
|
||||||
@ -12,7 +13,11 @@ public sealed class IdentityPolicyDecisionService : IIdentityPolicyDecisionServi
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IdentityPolicyContextRequest BuildPolicyContextRequest(EvaluateIdentityPolicyRequest request)
|
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 />
|
/// <inheritdoc />
|
||||||
@ -22,10 +27,33 @@ public sealed class IdentityPolicyDecisionService : IIdentityPolicyDecisionServi
|
|||||||
{
|
{
|
||||||
var permissionMatched = policyContextData.GrantedPermissions.Any(permission =>
|
var permissionMatched = policyContextData.GrantedPermissions.Any(permission =>
|
||||||
string.Equals(permission, request.PermissionCode, StringComparison.OrdinalIgnoreCase));
|
string.Equals(permission, request.PermissionCode, StringComparison.OrdinalIgnoreCase));
|
||||||
|
var providerSatisfied = IsProviderContextSatisfied(request.Provider, policyContextData);
|
||||||
|
|
||||||
return new EvaluateIdentityPolicyResponse(
|
return new EvaluateIdentityPolicyResponse(
|
||||||
request.SubjectId,
|
request.SubjectId,
|
||||||
request.PermissionCode,
|
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.Requests;
|
||||||
|
using BuildingBlock.Identity.Contracts.Conventions;
|
||||||
using Thalos.Domain.Contracts;
|
using Thalos.Domain.Contracts;
|
||||||
using Thalos.Domain.Decisions;
|
using Thalos.Domain.Decisions;
|
||||||
|
|
||||||
@ -10,10 +11,15 @@ public class IdentityPolicyDecisionServiceTests
|
|||||||
public void Evaluate_WhenPermissionMatchedAndContextSatisfied_ReturnsAllowed()
|
public void Evaluate_WhenPermissionMatchedAndContextSatisfied_ReturnsAllowed()
|
||||||
{
|
{
|
||||||
var service = new IdentityPolicyDecisionService();
|
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(
|
var context = new IdentityPolicyContextData(
|
||||||
request.SubjectId,
|
request.SubjectId,
|
||||||
request.PermissionCode,
|
request.PermissionCode,
|
||||||
|
request.Provider,
|
||||||
true,
|
true,
|
||||||
["identity.token.issue", "identity.policy.evaluate"]);
|
["identity.token.issue", "identity.policy.evaluate"]);
|
||||||
|
|
||||||
@ -26,8 +32,38 @@ public class IdentityPolicyDecisionServiceTests
|
|||||||
public void Evaluate_WhenPermissionMissing_ReturnsDenied()
|
public void Evaluate_WhenPermissionMissing_ReturnsDenied()
|
||||||
{
|
{
|
||||||
var service = new IdentityPolicyDecisionService();
|
var service = new IdentityPolicyDecisionService();
|
||||||
var request = new EvaluateIdentityPolicyRequest("user-1", "tenant-1", "identity.token.issue");
|
var request = new EvaluateIdentityPolicyRequest(
|
||||||
var context = new IdentityPolicyContextData(request.SubjectId, request.PermissionCode, true, ["identity.read"]);
|
"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);
|
var response = service.Evaluate(request, context);
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user