Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/Core/Configurations/RuntimeConfigValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1353,9 +1353,18 @@ public void ValidatePermissionsInConfig(RuntimeConfig runtimeConfig)
foreach ((string entityName, Entity entity) in runtimeConfig.Entities)
{
HashSet<EntityActionOperation> totalSupportedOperationsFromAllRoles = new();
HashSet<string> 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<EntityActionOperation> operationsList = new();
foreach (EntityAction action in actions)
Expand Down
86 changes: 83 additions & 3 deletions src/Service.Tests/UnitTests/ConfigValidationUnitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,87 @@ namespace Azure.DataApiBuilder.Service.Tests.UnitTests
[TestClass]
public class ConfigValidationUnitTests
{
/// <summary>
/// Duplicate role names on an entity are rejected during startup and CLI validation.
/// </summary>
[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<string, Entity>
{
[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<DataApiBuilderException>(() => 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);
}

/// <summary>
/// Distinct roles on an entity and the same role on different entities remain valid.
/// </summary>
[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<string, Entity>
{
[AuthorizationHelpers.TEST_ENTITY] = entity,
["AnotherEntity"] = entity
})
};
Assert.IsTrue(runtimeConfig.TryAddEntityNameToDataSourceName("AnotherEntity"));

RuntimeConfigValidator configValidator = InitializeRuntimeConfigValidator();
configValidator.ValidatePermissionsInConfig(runtimeConfig);
Assert.AreEqual(0, configValidator.ConfigValidationExceptions.Count);
}

/// <summary>
/// Test method to validate that an appropriate exception is thrown when database policy tries to reference a field
/// which is not accessible.
Expand Down Expand Up @@ -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<ILogger<RuntimeConfigValidator>>().Object);
return new(provider, fileSystem, new Mock<ILogger<RuntimeConfigValidator>>().Object, isValidateOnly);
}

private static Entity GetSampleEntityUsingSourceAndRelationshipMap(
Expand Down Expand Up @@ -3881,4 +3962,3 @@ private static ForeignKeyDefinition CreateForeignKey(RelationShipPair pair)
}
}
}