diff --git a/docs/migration/policy-behavior-invariants.md b/docs/migration/policy-behavior-invariants.md
index 8c84ef2..1e50ace 100644
--- a/docs/migration/policy-behavior-invariants.md
+++ b/docs/migration/policy-behavior-invariants.md
@@ -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
diff --git a/src/Thalos.Domain/Contracts/IdentityPolicyContextData.cs b/src/Thalos.Domain/Contracts/IdentityPolicyContextData.cs
index 075c63e..b40cf45 100644
--- a/src/Thalos.Domain/Contracts/IdentityPolicyContextData.cs
+++ b/src/Thalos.Domain/Contracts/IdentityPolicyContextData.cs
@@ -1,3 +1,5 @@
+using BuildingBlock.Identity.Contracts.Conventions;
+
namespace Thalos.Domain.Contracts;
///
@@ -5,10 +7,12 @@ namespace Thalos.Domain.Contracts;
///
/// Identity subject identifier.
/// Permission code being evaluated.
+/// Auth provider used in the request flow.
/// Whether contextual constraints are satisfied.
/// Permissions granted to subject in tenant scope.
public sealed record IdentityPolicyContextData(
string SubjectId,
string PermissionCode,
+ IdentityAuthProvider Provider,
bool ContextSatisfied,
IReadOnlyCollection GrantedPermissions);
diff --git a/src/Thalos.Domain/Contracts/IdentityTokenData.cs b/src/Thalos.Domain/Contracts/IdentityTokenData.cs
index 5aaa9b8..f43d312 100644
--- a/src/Thalos.Domain/Contracts/IdentityTokenData.cs
+++ b/src/Thalos.Domain/Contracts/IdentityTokenData.cs
@@ -1,3 +1,5 @@
+using BuildingBlock.Identity.Contracts.Conventions;
+
namespace Thalos.Domain.Contracts;
///
@@ -5,4 +7,8 @@ namespace Thalos.Domain.Contracts;
///
/// Token value, if found.
/// Token lifetime, if found.
-public sealed record IdentityTokenData(string? Token, int? ExpiresInSeconds);
+/// Auth provider used in the issuance flow.
+public sealed record IdentityTokenData(
+ string? Token,
+ int? ExpiresInSeconds,
+ IdentityAuthProvider Provider = IdentityAuthProvider.InternalJwt);
diff --git a/src/Thalos.Domain/Decisions/IdentityPolicyDecisionService.cs b/src/Thalos.Domain/Decisions/IdentityPolicyDecisionService.cs
index 406414f..c382ff6 100644
--- a/src/Thalos.Domain/Decisions/IdentityPolicyDecisionService.cs
+++ b/src/Thalos.Domain/Decisions/IdentityPolicyDecisionService.cs
@@ -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
///
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);
}
///
@@ -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
+ };
}
}
diff --git a/tests/Thalos.Domain.UnitTests/IdentityPolicyDecisionServiceTests.cs b/tests/Thalos.Domain.UnitTests/IdentityPolicyDecisionServiceTests.cs
index 1a692ce..c0bb086 100644
--- a/tests/Thalos.Domain.UnitTests/IdentityPolicyDecisionServiceTests.cs
+++ b/tests/Thalos.Domain.UnitTests/IdentityPolicyDecisionServiceTests.cs
@@ -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);