From 0dfed0e56e0638c28067a6452edd0bf7db1aa82c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Oct 2026 08:52:03 +0000 Subject: [PATCH 1/2] Initial plan From d49ffc3547cfc36f7e9ab05042cc24e1b4057f0f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Oct 2026 08:58:41 +0000 Subject: [PATCH 2/2] Reject duplicate permission roles per entity during validation Co-authored-by: souvikghosh04 <210500244+souvikghosh04@users.noreply.github.com> --- .../Configurations/RuntimeConfigValidator.cs | 9 ++ .../UnitTests/ConfigValidationUnitTests.cs | 86 ++++++++++++++++++- 2 files changed, 92 insertions(+), 3 deletions(-) diff --git a/src/Core/Configurations/RuntimeConfigValidator.cs b/src/Core/Configurations/RuntimeConfigValidator.cs index 222b15647d..f73109d56c 100644 --- a/src/Core/Configurations/RuntimeConfigValidator.cs +++ b/src/Core/Configurations/RuntimeConfigValidator.cs @@ -1353,9 +1353,18 @@ public void ValidatePermissionsInConfig(RuntimeConfig runtimeConfig) foreach ((string entityName, Entity entity) in runtimeConfig.Entities) { HashSet totalSupportedOperationsFromAllRoles = new(); + HashSet roleNames = new(StringComparer.OrdinalIgnoreCase); foreach (EntityPermission permissionSetting in entity.Permissions) { string roleName = permissionSetting.Role; + if (!roleNames.Add(roleName)) + { + HandleOrRecordException(new DataApiBuilderException( + message: $"Duplicate role '{roleName}' defined for entity '{entityName}'. Role names must be unique per entity.", + statusCode: HttpStatusCode.ServiceUnavailable, + subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError)); + } + EntityAction[] actions = permissionSetting.Actions; List operationsList = new(); foreach (EntityAction action in actions) diff --git a/src/Service.Tests/UnitTests/ConfigValidationUnitTests.cs b/src/Service.Tests/UnitTests/ConfigValidationUnitTests.cs index 522a5d00d5..354b5e8c1b 100644 --- a/src/Service.Tests/UnitTests/ConfigValidationUnitTests.cs +++ b/src/Service.Tests/UnitTests/ConfigValidationUnitTests.cs @@ -37,6 +37,87 @@ namespace Azure.DataApiBuilder.Service.Tests.UnitTests [TestClass] public class ConfigValidationUnitTests { + /// + /// Duplicate role names on an entity are rejected during startup and CLI validation. + /// + [DataTestMethod] + [DataRow("jerry", EntityActionOperation.All, false)] + [DataRow("jerry", EntityActionOperation.Read, false)] + [DataRow("JERRY", EntityActionOperation.All, false)] + [DataRow("anonymous", EntityActionOperation.All, false)] + [DataRow("authenticated", EntityActionOperation.All, false)] + [DataRow("jerry", EntityActionOperation.All, true)] + [DataRow("JERRY", EntityActionOperation.Read, true)] + public void DuplicateRoleNamesAreRejected(string duplicateRole, EntityActionOperation operation, bool isValidateOnly) + { + string role = duplicateRole.ToLowerInvariant(); + RuntimeConfig runtimeConfig = AuthorizationHelpers.InitRuntimeConfig(roleName: role); + Entity entity = runtimeConfig.Entities[AuthorizationHelpers.TEST_ENTITY]; + runtimeConfig = runtimeConfig with + { + Entities = new(new Dictionary + { + [AuthorizationHelpers.TEST_ENTITY] = entity with + { + Permissions = new EntityPermission[] + { + new("anotherRole", new EntityAction[] { new(EntityActionOperation.Read, null, null) }), + new(role, new EntityAction[] { new(EntityActionOperation.All, null, null) }), + new(duplicateRole, new EntityAction[] { new(operation, null, null) }) + } + } + }) + }; + + RuntimeConfigValidator configValidator = InitializeRuntimeConfigValidator(isValidateOnly); + DataApiBuilderException ex; + if (isValidateOnly) + { + configValidator.ValidatePermissionsInConfig(runtimeConfig); + Assert.AreEqual(1, configValidator.ConfigValidationExceptions.Count); + ex = (DataApiBuilderException)configValidator.ConfigValidationExceptions[0]; + } + else + { + ex = Assert.ThrowsException(() => configValidator.ValidatePermissionsInConfig(runtimeConfig)); + } + + Assert.AreEqual($"Duplicate role '{duplicateRole}' defined for entity '{AuthorizationHelpers.TEST_ENTITY}'. Role names must be unique per entity.", ex.Message); + Assert.AreEqual(HttpStatusCode.ServiceUnavailable, ex.StatusCode); + Assert.AreEqual(DataApiBuilderException.SubStatusCodes.ConfigValidationError, ex.SubStatusCode); + } + + /// + /// Distinct roles on an entity and the same role on different entities remain valid. + /// + [TestMethod] + public void UniqueRoleNamesPerEntityAreAllowed() + { + RuntimeConfig runtimeConfig = AuthorizationHelpers.InitRuntimeConfig(roleName: "jerry"); + Entity entity = runtimeConfig.Entities[AuthorizationHelpers.TEST_ENTITY]; + entity = entity with + { + Permissions = new EntityPermission[] + { + entity.Permissions[0], + new("anonymous", new EntityAction[] { new(EntityActionOperation.Read, null, null) }) + } + }; + runtimeConfig = runtimeConfig with + { + Entities = new(new Dictionary + { + [AuthorizationHelpers.TEST_ENTITY] = entity, + ["AnotherEntity"] = entity + }) + }; + Assert.IsTrue(runtimeConfig.TryAddEntityNameToDataSourceName("AnotherEntity")); + + RuntimeConfigValidator configValidator = InitializeRuntimeConfigValidator(); + configValidator.ValidatePermissionsInConfig(runtimeConfig); + Assert.AreEqual(0, configValidator.ConfigValidationExceptions.Count); + } + /// /// Test method to validate that an appropriate exception is thrown when database policy tries to reference a field /// which is not accessible. @@ -3772,12 +3853,12 @@ public void ValidateEmbeddingsOptions_EndpointDisabled_SkipsValidation() configValidator.ValidateEmbeddingsOptions(runtimeConfig); } - private static RuntimeConfigValidator InitializeRuntimeConfigValidator() + private static RuntimeConfigValidator InitializeRuntimeConfigValidator(bool isValidateOnly = false) { MockFileSystem fileSystem = new(); FileSystemRuntimeConfigLoader loader = new(fileSystem); RuntimeConfigProvider provider = new(loader); - return new(provider, fileSystem, new Mock>().Object); + return new(provider, fileSystem, new Mock>().Object, isValidateOnly); } private static Entity GetSampleEntityUsingSourceAndRelationshipMap( @@ -3881,4 +3962,3 @@ private static ForeignKeyDefinition CreateForeignKey(RelationShipPair pair) } } } -