From 607ff41af2a74a718e222dbbbadcebcde4e97752 Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Mon, 19 Oct 2026 16:35:22 -0700 Subject: [PATCH 1/5] Add logging for policy incompleteness --- .../Authorization/AuthorizationResolver.cs | 91 ++++++++++++++++++- 1 file changed, 89 insertions(+), 2 deletions(-) diff --git a/src/Core/Authorization/AuthorizationResolver.cs b/src/Core/Authorization/AuthorizationResolver.cs index 1b76401a0e..8569e355ea 100644 --- a/src/Core/Authorization/AuthorizationResolver.cs +++ b/src/Core/Authorization/AuthorizationResolver.cs @@ -4,6 +4,8 @@ using System.Globalization; using System.Net; using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; using System.Text.Json; using System.Text.RegularExpressions; using Azure.DataApiBuilder.Auth; @@ -16,6 +18,7 @@ using Azure.DataApiBuilder.Service.Exceptions; using HotChocolate.Resolvers; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using Microsoft.IdentityModel.JsonWebTokens; @@ -29,6 +32,7 @@ public class AuthorizationResolver : IAuthorizationResolver { private readonly RuntimeConfigProvider _runtimeConfigProvider; private readonly IMetadataProviderFactory _metadataProviderFactory; + private readonly ILogger? _logger; public const string WILDCARD = "*"; public const string CLAIM_PREFIX = "@claims."; public const string FIELD_PREFIX = "@item."; @@ -49,10 +53,12 @@ public class AuthorizationResolver : IAuthorizationResolver public AuthorizationResolver( RuntimeConfigProvider runtimeConfigProvider, IMetadataProviderFactory metadataProviderFactory, - HotReloadEventHandler? handler = null) + HotReloadEventHandler? handler = null, + ILogger? logger = null) { _metadataProviderFactory = metadataProviderFactory; _runtimeConfigProvider = runtimeConfigProvider; + _logger = logger; handler?.Subscribe(DabConfigEvents.AUTHZ_RESOLVER_ON_CONFIG_CHANGED, OnConfigChanged); if (runtimeConfigProvider.TryGetConfig(out RuntimeConfig? runtimeConfig)) @@ -216,7 +222,17 @@ public ResolvedDatabasePolicy ResolveDBPolicy(string entityName, string roleName return ResolvedDatabasePolicy.Empty; } - return GetPolicyWithClaimValues(dBpolicyWithClaimTypes, GetAllAuthenticatedUserClaims(httpContext)); + ResolvedDatabasePolicy resolvedPolicy = GetPolicyWithClaimValues(dBpolicyWithClaimTypes, GetAllAuthenticatedUserClaims(httpContext)); + + // A configured (non-empty) database policy that compiles to an empty predicate is a + // silent fail-open of row/column level authorization: the query would run unfiltered. + // Emit an audit signal and, outside development mode, fail the request. + if (string.IsNullOrWhiteSpace(resolvedPolicy.Policy)) + { + AuditDatabasePolicyFailOpen(entityName, roleName, operation, dBpolicyWithClaimTypes); + } + + return resolvedPolicy; } /// @@ -794,6 +810,77 @@ private static ResolvedDatabasePolicy GetPolicyWithClaimValues(string policy, Di return new ResolvedDatabasePolicy(processedPolicy, claimValues); } + /// + /// Emits a structured audit signal when a configured (non-empty) database authorization + /// policy resolves to an empty predicate, which silently removes the intended row/column + /// level filter (fail-open). In development mode this is logged as a warning so the request + /// can proceed for debugging; in all other host modes it is logged as an error and the + /// request is failed to prevent returning unfiltered data. + /// + /// Entity targeted by the request. + /// Role resolved from the client role header. + /// Operation the policy applies to. + /// The non-empty policy text configured in runtime config. + /// Thrown outside development mode to fail the request. + private void AuditDatabasePolicyFailOpen( + string entityName, + string roleName, + EntityActionOperation operation, + string configuredPolicy) + { + const string reason = "Configured database authorization policy compiled to an empty predicate; " + + "the request would run without the intended row/column level filter (fail-open)."; + string predicateHash = ComputePredicateHash(configuredPolicy); + bool isDevelopmentMode = _runtimeConfigProvider.TryGetConfig(out RuntimeConfig? runtimeConfig) + && runtimeConfig.IsDevelopmentMode(); + + // Structured audit event: entity, role, operation, policy text, reason, predicateHash, dbPolicyApplied. + const string auditMessageTemplate = "Database authorization policy fail-open detected. " + + "Entity: {Entity}, Role: {Role}, Operation: {Operation}, PolicyText: {PolicyText}, " + + "Reason: {Reason}, PredicateHash: {PredicateHash}, DbPolicyApplied: {DbPolicyApplied}"; + + if (isDevelopmentMode) + { + _logger?.LogWarning( + auditMessageTemplate, + entityName, + roleName, + operation, + configuredPolicy, + reason, + predicateHash, + false); + return; + } + + _logger?.LogError( + auditMessageTemplate, + entityName, + roleName, + operation, + configuredPolicy, + reason, + predicateHash, + false); + + throw new DataApiBuilderException( + message: DataApiBuilderException.AUTHORIZATION_FAILURE, + statusCode: HttpStatusCode.Forbidden, + subStatusCode: DataApiBuilderException.SubStatusCodes.DatabasePolicyFailure); + } + + /// + /// Produces a stable, non-reversible fingerprint of a policy's text so audit signals can be + /// correlated across requests without logging claim values or other sensitive data. + /// + /// Configured policy text to hash. + /// Uppercase hexadecimal SHA-256 hash of the policy text. + private static string ComputePredicateHash(string policyText) + { + byte[] hash = SHA256.HashData(Encoding.UTF8.GetBytes(policyText)); + return Convert.ToHexString(hash); + } + /// /// Helper function used to retrieve the claim value for the given claim type from the user's claims. /// From 03a89d3c321a6bf0c38c1786a491c3b2ce24f209 Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Mon, 19 Oct 2026 17:44:43 -0700 Subject: [PATCH 2/5] Add test --- .../Authorization/AuthorizationHelpers.cs | 6 +- .../AuthorizationResolverUnitTests.cs | 87 +++++++++++++++++++ 2 files changed, 90 insertions(+), 3 deletions(-) diff --git a/src/Service.Tests/Authorization/AuthorizationHelpers.cs b/src/Service.Tests/Authorization/AuthorizationHelpers.cs index bdd5630a50..8170c068bb 100644 --- a/src/Service.Tests/Authorization/AuthorizationHelpers.cs +++ b/src/Service.Tests/Authorization/AuthorizationHelpers.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO.Abstractions.TestingHelpers; +using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Config; using Azure.DataApiBuilder.Config.DatabasePrimitives; using Azure.DataApiBuilder.Config.ObjectModel; @@ -33,7 +34,7 @@ public static class AuthorizationHelpers /// /// Configuration with Authorization metadata /// AuthorizationResolver object - public static AuthorizationResolver InitAuthorizationResolver(RuntimeConfig runtimeConfig) + public static AuthorizationResolver InitAuthorizationResolver(RuntimeConfig runtimeConfig, ILogger? logger = null) { MockFileSystem fileSystem = new(); fileSystem.AddFile(FileSystemRuntimeConfigLoader.DEFAULT_CONFIG_FILE_NAME, new(runtimeConfig.ToJson())); @@ -41,7 +42,6 @@ public static AuthorizationResolver InitAuthorizationResolver(RuntimeConfig runt RuntimeConfigProvider runtimeConfigProvider = TestHelper.GetRuntimeConfigProvider(loader); Mock metadataProvider = new(); - Mock> logger = new(); SourceDefinition sampleTable = CreateSampleTable(); metadataProvider.Setup(x => x.GetSourceDefinition(TEST_ENTITY)).Returns(sampleTable); metadataProvider.Setup(x => x.GetDatabaseType()).Returns(DatabaseType.MSSQL); @@ -58,7 +58,7 @@ public static AuthorizationResolver InitAuthorizationResolver(RuntimeConfig runt string dataSourceName = runtimeConfigProvider.GetConfig().DefaultDataSourceName; metadataProviderFactory.Setup(x => x.GetMetadataProvider(dataSourceName)).Returns(metadataProvider.Object); - return new AuthorizationResolver(runtimeConfigProvider, metadataProviderFactory.Object); + return new AuthorizationResolver(runtimeConfigProvider, metadataProviderFactory.Object, logger: logger); } /// diff --git a/src/Service.Tests/Authorization/AuthorizationResolverUnitTests.cs b/src/Service.Tests/Authorization/AuthorizationResolverUnitTests.cs index 81be4e2441..b01d095fa1 100644 --- a/src/Service.Tests/Authorization/AuthorizationResolverUnitTests.cs +++ b/src/Service.Tests/Authorization/AuthorizationResolverUnitTests.cs @@ -8,6 +8,7 @@ using System.Net; using System.Security.Claims; using System.Security.Cryptography; +using System.Text; using System.Text.Json; using System.Threading.Tasks; using Azure.DataApiBuilder.Auth; @@ -17,6 +18,7 @@ using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.Tests.Authentication.Helpers; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; @@ -34,6 +36,91 @@ public class AuthorizationResolverUnitTests private const string TEST_AUTHENTICATION_TYPE = "TestAuth"; private const string TEST_CLAIMTYPE_NAME = "TestName"; + /// Verifies empty resolved policies emit an audit log and fail closed only in production. + [DataTestMethod] + [DataRow(HostMode.Development, EntityActionOperation.Read, "@item.", DisplayName = "Empty Predicate in Development read logs a warning")] + [DataRow(HostMode.Development, EntityActionOperation.Update, "@item.", DisplayName = "Empty Predicate in Development update logs a warning")] + [DataRow(HostMode.Development, EntityActionOperation.Delete, "@item.", DisplayName = "Empty Predicate in Development delete logs a warning")] + [DataRow(HostMode.Production, EntityActionOperation.Read, "@item.", DisplayName = "Empty Predicate in Production read logs an error and fails")] + [DataRow(HostMode.Production, EntityActionOperation.Update, "@item.", DisplayName = "Empty Predicate in Production update logs an error and fails")] + [DataRow(HostMode.Production, EntityActionOperation.Delete, "@item.", DisplayName = "Empty Predicate in Production delete logs an error and fails")] + [DataRow(HostMode.Development, EntityActionOperation.Read, "@item. @item.", DisplayName = "Empty Predicate in Development whitespace predicate logs a warning")] + [DataRow(HostMode.Production, EntityActionOperation.Read, "@item. @item.", DisplayName = "Empty Predicate in Production whitespace predicate logs an error and fails")] + public void ResolveDBPolicy_EmptyPredicate_AuditsAccordingToHostMode( + HostMode hostMode, EntityActionOperation operation, string configuredPolicy) + { + RuntimeConfig config = AuthorizationHelpers.InitRuntimeConfig( + roleName: TEST_ROLE, operation: operation, databasePolicy: configuredPolicy); + config = config with + { + Runtime = config.Runtime! with { Host = config.Runtime.Host! with { Mode = hostMode } } + }; + Mock> logger = new(); + AuthorizationResolver resolver = AuthorizationHelpers.InitAuthorizationResolver(config, logger.Object); + DefaultHttpContext context = new(); + context.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER] = TEST_ROLE; + + if (hostMode == HostMode.Development) + { + ResolvedDatabasePolicy resolved = resolver.ResolveDBPolicy(TEST_ENTITY, TEST_ROLE, operation, context); + Assert.IsTrue(string.IsNullOrWhiteSpace(resolved.Policy)); + Assert.AreEqual(0, resolved.ClaimValues.Count); + } + else + { + DataApiBuilderException exception = Assert.ThrowsException(() => + resolver.ResolveDBPolicy(TEST_ENTITY, TEST_ROLE, operation, context)); + Assert.AreEqual(HttpStatusCode.Forbidden, exception.StatusCode); + Assert.AreEqual(DataApiBuilderException.SubStatusCodes.DatabasePolicyFailure, exception.SubStatusCode); + Assert.AreEqual(DataApiBuilderException.AUTHORIZATION_FAILURE, exception.Message); + } + + var log = logger.Invocations.Single(invocation => invocation.Method.Name == nameof(ILogger.Log)); + Assert.AreEqual(hostMode == HostMode.Development ? LogLevel.Warning : LogLevel.Error, log.Arguments[0]); + Dictionary audit = ((IEnumerable>)log.Arguments[2]) + .ToDictionary(property => property.Key, property => property.Value); + Assert.AreEqual(TEST_ENTITY, audit["Entity"]); + Assert.AreEqual(TEST_ROLE, audit["Role"]); + Assert.AreEqual(operation, audit["Operation"]); + Assert.AreEqual(configuredPolicy, audit["PolicyText"]); + Assert.AreEqual(false, audit["DbPolicyApplied"]); + Assert.AreEqual( + Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(configuredPolicy))), + audit["PredicateHash"]); + StringAssert.Contains((string)audit["Reason"]!, "compiled to an empty predicate"); + } + + /// Verifies valid or unconfigured policies do not produce false fail-open audit logs. + [DataTestMethod] + [DataRow(HostMode.Development, null, "", DisplayName = "ResolveDBPolicy Development missing policy does not log")] + [DataRow(HostMode.Production, null, "", DisplayName = "ResolveDBPolicy Production missing policy does not log")] + [DataRow(HostMode.Development, "", "", DisplayName = "ResolveDBPolicy Development empty policy does not log")] + [DataRow(HostMode.Production, "", "", DisplayName = "ResolveDBPolicy Production empty policy does not log")] + [DataRow(HostMode.Development, " ", "", DisplayName = "ResolveDBPolicy Development whitespace configuration does not log")] + [DataRow(HostMode.Production, " ", "", DisplayName = "ResolveDBPolicy Production whitespace configuration does not log")] + [DataRow(HostMode.Development, "@item.col1 eq 1", "col1 eq 1", DisplayName = "ResolveDBPolicy Development valid policy does not log")] + [DataRow(HostMode.Production, "@item.col1 eq 1", "col1 eq 1", DisplayName = "ResolveDBPolicy Production valid policy does not log")] + public void ResolveDBPolicy_ValidOrUnconfiguredPolicy_DoesNotAudit( + HostMode hostMode, string? configuredPolicy, string expectedPolicy) + { + RuntimeConfig config = AuthorizationHelpers.InitRuntimeConfig( + roleName: TEST_ROLE, operation: EntityActionOperation.Read, databasePolicy: configuredPolicy); + config = config with + { + Runtime = config.Runtime! with { Host = config.Runtime.Host! with { Mode = hostMode } } + }; + Mock> logger = new(); + AuthorizationResolver resolver = AuthorizationHelpers.InitAuthorizationResolver(config, logger.Object); + DefaultHttpContext context = new(); + context.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER] = TEST_ROLE; + + ResolvedDatabasePolicy resolved = resolver.ResolveDBPolicy(TEST_ENTITY, TEST_ROLE, EntityActionOperation.Read, context); + + Assert.AreEqual(expectedPolicy, resolved.Policy); + Assert.AreEqual(0, resolved.ClaimValues.Count); + logger.VerifyNoOtherCalls(); + } + [TestMethod] public void GetRolesForOperation_NullEntityNameThrows() { From 0f595dc9b9de0dea63adbde13104d1400a587e6a Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Fri, 23 Oct 2026 13:31:28 -0700 Subject: [PATCH 3/5] Address comments --- src/Auth/IAuthorizationResolver.cs | 3 + .../Authorization/AuthorizationResolver.cs | 2 +- .../Resolvers/AuthorizationPolicyHelpers.cs | 9 ++ .../DatabasePolicyClaimBindingUnitTests.cs | 103 ++++++++++++++++++ 4 files changed, 116 insertions(+), 1 deletion(-) diff --git a/src/Auth/IAuthorizationResolver.cs b/src/Auth/IAuthorizationResolver.cs index c990611f9a..6499eb0632 100644 --- a/src/Auth/IAuthorizationResolver.cs +++ b/src/Auth/IAuthorizationResolver.cs @@ -82,6 +82,9 @@ public interface IAuthorizationResolver /// The policy text and typed claim values to bind to it. public ResolvedDatabasePolicy ResolveDBPolicy(string entityName, string roleName, EntityActionOperation operation, HttpContext httpContext); + /// Audits a configured policy that produced no predicate and rejects the request outside development mode. + public void AuditDatabasePolicyFailOpen(string entityName, string roleName, EntityActionOperation operation, string configuredPolicy); + /// /// Get list of roles defined for entity within runtime configuration.. This is applicable for GraphQL when creating authorization /// directive on Object type. diff --git a/src/Core/Authorization/AuthorizationResolver.cs b/src/Core/Authorization/AuthorizationResolver.cs index 8569e355ea..4c9fa1d3c6 100644 --- a/src/Core/Authorization/AuthorizationResolver.cs +++ b/src/Core/Authorization/AuthorizationResolver.cs @@ -822,7 +822,7 @@ private static ResolvedDatabasePolicy GetPolicyWithClaimValues(string policy, Di /// Operation the policy applies to. /// The non-empty policy text configured in runtime config. /// Thrown outside development mode to fail the request. - private void AuditDatabasePolicyFailOpen( + public void AuditDatabasePolicyFailOpen( string entityName, string roleName, EntityActionOperation operation, diff --git a/src/Core/Resolvers/AuthorizationPolicyHelpers.cs b/src/Core/Resolvers/AuthorizationPolicyHelpers.cs index 408b0f30d4..4428beb381 100644 --- a/src/Core/Resolvers/AuthorizationPolicyHelpers.cs +++ b/src/Core/Resolvers/AuthorizationPolicyHelpers.cs @@ -189,6 +189,15 @@ private static List ProcessFilter( resourcePath: (entityDBObject is not null) ? $"{entityName}.{entityDBObject.FullName}" : entityName, sqlMetadataProvider); + if (filterClause is null && !string.IsNullOrWhiteSpace(dbQueryPolicy.Policy)) + { + string configuredPolicy = authorizationResolver.GetDBPolicyForRequest(entityName, clientRoleHeader, elementalOperation); + if (!string.IsNullOrWhiteSpace(configuredPolicy)) + { + authorizationResolver.AuditDatabasePolicyFailOpen(entityName, clientRoleHeader, elementalOperation, configuredPolicy); + } + } + postProcessCallback(filterClause, elementalOperation); } diff --git a/src/Service.Tests/UnitTests/DatabasePolicyClaimBindingUnitTests.cs b/src/Service.Tests/UnitTests/DatabasePolicyClaimBindingUnitTests.cs index fb423de2c7..a6374df986 100644 --- a/src/Service.Tests/UnitTests/DatabasePolicyClaimBindingUnitTests.cs +++ b/src/Service.Tests/UnitTests/DatabasePolicyClaimBindingUnitTests.cs @@ -5,7 +5,10 @@ using System.Collections.Generic; using System.Data; using System.Linq; +using System.Net; using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Config.DatabasePrimitives; using Azure.DataApiBuilder.Config.ObjectModel; @@ -16,6 +19,7 @@ using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.Tests.Authorization; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.OData.UriParser; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -34,6 +38,105 @@ public class DatabasePolicyClaimBindingUnitTests private const string ROLE_NAME = AuthorizationHelpers.TEST_ROLE; private const EntityActionOperation OPERATION = EntityActionOperation.Read; + /// Verifies a configured policy with no parsed filter is audited and rejected in production. + [DataTestMethod] + [DataRow(HostMode.Development, "@item.&$top=1", DisplayName = "Development empty filter with top logs a warning")] + [DataRow(HostMode.Production, "@item.&$top=1", DisplayName = "Production empty filter with top logs an error and fails")] + [DataRow(HostMode.Development, "@item.&$skip=1", DisplayName = "Development empty filter with skip logs a warning")] + [DataRow(HostMode.Production, "@item.&$skip=1", DisplayName = "Production empty filter with skip logs an error and fails")] + [DataRow(HostMode.Development, "@item.&$orderby=id", DisplayName = "Development empty filter with orderby logs a warning")] + [DataRow(HostMode.Production, "@item.&$orderby=id", DisplayName = "Production empty filter with orderby logs an error and fails")] + [DataRow(HostMode.Development, "@item.&$select=id", DisplayName = "Development empty filter with select logs a warning")] + [DataRow(HostMode.Production, "@item.&$select=id", DisplayName = "Production empty filter with select logs an error and fails")] + [DataRow(HostMode.Development, "@item.&$top=1&$skip=1", DisplayName = "Development empty filter with multiple options logs a warning")] + [DataRow(HostMode.Production, "@item.&$top=1&$skip=1", DisplayName = "Production empty filter with multiple options logs an error and fails")] + public void ProcessAuthorizationPolicies_NullParsedPolicy_AuditsAccordingToHostMode(HostMode hostMode, string policy) + { + RuntimeConfig config = AuthorizationHelpers.InitRuntimeConfig( + entityName: ENTITY_NAME, roleName: ROLE_NAME, operation: OPERATION, databasePolicy: policy); + config = config with + { + Runtime = config.Runtime! with { Host = config.Runtime.Host! with { Mode = hostMode } } + }; + Mock> logger = new(); + AuthorizationResolver resolver = AuthorizationHelpers.InitAuthorizationResolver(config, logger.Object); + DefaultHttpContext context = new(); + context.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER] = ROLE_NAME; + Mock metadataProvider = CreateMetadataProvider(); + TestSqlQueryStructure structure = new(metadataProvider.Object, resolver); + + ResolvedDatabasePolicy resolvedPolicy = resolver.ResolveDBPolicy(ENTITY_NAME, ROLE_NAME, OPERATION, context); + Assert.IsFalse(string.IsNullOrWhiteSpace(resolvedPolicy.Policy)); + Assert.IsNull(AuthorizationPolicyHelpers.GetDBPolicyClauseForQueryStructure( + resolvedPolicy, + ENTITY_NAME, + $"{ENTITY_NAME}.{metadataProvider.Object.EntityToDatabaseObject[ENTITY_NAME].FullName}", + metadataProvider.Object)); + + if (hostMode == HostMode.Production) + { + DataApiBuilderException exception = Assert.ThrowsException(() => + AuthorizationPolicyHelpers.ProcessAuthorizationPolicies(OPERATION, structure, context, resolver, metadataProvider.Object)); + Assert.AreEqual(HttpStatusCode.Forbidden, exception.StatusCode); + Assert.AreEqual(DataApiBuilderException.SubStatusCodes.DatabasePolicyFailure, exception.SubStatusCode); + } + else + { + AuthorizationPolicyHelpers.ProcessAuthorizationPolicies(OPERATION, structure, context, resolver, metadataProvider.Object); + } + + var log = logger.Invocations.Single(invocation => invocation.Method.Name == nameof(ILogger.Log)); + Assert.AreEqual(hostMode == HostMode.Development ? LogLevel.Warning : LogLevel.Error, log.Arguments[0]); + Dictionary audit = ((IEnumerable>)log.Arguments[2]) + .ToDictionary(property => property.Key, property => property.Value); + Assert.AreEqual(ENTITY_NAME, audit["Entity"]); + Assert.AreEqual(ROLE_NAME, audit["Role"]); + Assert.AreEqual(OPERATION, audit["Operation"]); + Assert.AreEqual(policy, audit["PolicyText"]); + Assert.AreEqual(false, audit["DbPolicyApplied"]); + Assert.AreEqual(Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(policy))), audit["PredicateHash"]); + Assert.IsFalse(string.IsNullOrWhiteSpace((string)audit["Reason"]!)); + AssertParameterValues(structure); + } + + /// Verifies unconfigured and valid policies remain unaffected by the parsed-filter guard. + [DataTestMethod] + [DataRow(HostMode.Development, null, DisplayName = "Development missing policy is allowed")] + [DataRow(HostMode.Production, null, DisplayName = "Production missing policy is allowed")] + [DataRow(HostMode.Development, "", DisplayName = "Development empty policy is allowed")] + [DataRow(HostMode.Production, "", DisplayName = "Production empty policy is allowed")] + [DataRow(HostMode.Development, "@item.intCol eq 1", DisplayName = "Development valid policy is applied")] + [DataRow(HostMode.Production, "@item.intCol eq 1", DisplayName = "Production valid policy is applied")] + public void ProcessAuthorizationPolicies_UnconfiguredOrValidPolicy_DoesNotAudit(HostMode hostMode, string policy) + { + RuntimeConfig config = AuthorizationHelpers.InitRuntimeConfig( + entityName: ENTITY_NAME, roleName: ROLE_NAME, operation: OPERATION, databasePolicy: policy); + config = config with + { + Runtime = config.Runtime! with { Host = config.Runtime.Host! with { Mode = hostMode } } + }; + Mock> logger = new(); + AuthorizationResolver resolver = AuthorizationHelpers.InitAuthorizationResolver(config, logger.Object); + DefaultHttpContext context = new(); + context.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER] = ROLE_NAME; + Mock metadataProvider = CreateMetadataProvider(); + TestSqlQueryStructure structure = new(metadataProvider.Object, resolver); + + AuthorizationPolicyHelpers.ProcessAuthorizationPolicies(OPERATION, structure, context, resolver, metadataProvider.Object); + + logger.VerifyNoOtherCalls(); + if (!string.IsNullOrEmpty(policy)) + { + Assert.AreEqual("([intCol] = @param0)", structure.GetDbPolicyForOperation(OPERATION)); + AssertParameterValues(structure, 1); + } + else + { + Assert.IsTrue(string.IsNullOrEmpty(structure.GetDbPolicyForOperation(OPERATION))); + AssertParameterValues(structure); + } + } + /// /// Verifies that encoded and literal syntax remains claim data throughout the complete /// SQL and Cosmos policy pipelines. From 850615d3489b1cc5aa7c0123bd1633c5ede56170 Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Fri, 23 Oct 2026 13:33:52 -0700 Subject: [PATCH 4/5] Remove unnecessary section --- src/Service.Tests/Authorization/AuthorizationHelpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service.Tests/Authorization/AuthorizationHelpers.cs b/src/Service.Tests/Authorization/AuthorizationHelpers.cs index 8170c068bb..f84fcf346e 100644 --- a/src/Service.Tests/Authorization/AuthorizationHelpers.cs +++ b/src/Service.Tests/Authorization/AuthorizationHelpers.cs @@ -58,7 +58,7 @@ public static AuthorizationResolver InitAuthorizationResolver(RuntimeConfig runt string dataSourceName = runtimeConfigProvider.GetConfig().DefaultDataSourceName; metadataProviderFactory.Setup(x => x.GetMetadataProvider(dataSourceName)).Returns(metadataProvider.Object); - return new AuthorizationResolver(runtimeConfigProvider, metadataProviderFactory.Object, logger: logger); + return new AuthorizationResolver(runtimeConfigProvider, metadataProviderFactory.Object, logger); } /// From 83d50e5025e0a32debcbaf694b62fe817aebe281 Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Mon, 26 Oct 2026 16:29:25 -0700 Subject: [PATCH 5/5] Fix error --- src/Service.Tests/Authorization/AuthorizationHelpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service.Tests/Authorization/AuthorizationHelpers.cs b/src/Service.Tests/Authorization/AuthorizationHelpers.cs index f84fcf346e..8170c068bb 100644 --- a/src/Service.Tests/Authorization/AuthorizationHelpers.cs +++ b/src/Service.Tests/Authorization/AuthorizationHelpers.cs @@ -58,7 +58,7 @@ public static AuthorizationResolver InitAuthorizationResolver(RuntimeConfig runt string dataSourceName = runtimeConfigProvider.GetConfig().DefaultDataSourceName; metadataProviderFactory.Setup(x => x.GetMetadataProvider(dataSourceName)).Returns(metadataProvider.Object); - return new AuthorizationResolver(runtimeConfigProvider, metadataProviderFactory.Object, logger); + return new AuthorizationResolver(runtimeConfigProvider, metadataProviderFactory.Object, logger: logger); } ///