Skip to content

Commit 20d95f7

Browse files
committed
Add test_role_with_excluded_fields_on_mutation to MsSql config generator
The MCP create_record/update_record column-exclusion integration tests passed locally but failed in CI. CI regenerates src/Service.Tests/dab-config.MsSql.json from config-generators/mssql-commands.txt; the role existed only in the checked-in JSON, so the regenerated CI config lacked it and inheritance from the 'authenticated' role (no field exclusions) allowed writes to excluded columns. Add the role to mssql-commands.txt for Book (exclude publisher_id) and WebsiteUser (exclude username) on create/update. Add AuthorizationResolver unit tests covering multi-action-per-role column exclusion and config parsing.
1 parent 41a573a commit 20d95f7

3 files changed

Lines changed: 217 additions & 0 deletions

File tree

config-generators/mssql-commands.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@ update Book --config "dab-config.MsSql.json" --permissions "test_role_with_exclu
123123
update Book --config "dab-config.MsSql.json" --permissions "test_role_with_excluded_fields:read" --fields.exclude "publisher_id"
124124
update Book --config "dab-config.MsSql.json" --permissions "test_role_with_policy_excluded_fields:create,update,delete"
125125
update Book --config "dab-config.MsSql.json" --permissions "test_role_with_policy_excluded_fields:read" --fields.exclude "publisher_id" --policy-database "@item.title ne 'Test'"
126+
update Book --config "dab-config.MsSql.json" --permissions "test_role_with_excluded_fields_on_mutation:read,delete"
127+
update Book --config "dab-config.MsSql.json" --permissions "test_role_with_excluded_fields_on_mutation:create" --fields.exclude "publisher_id"
128+
update Book --config "dab-config.MsSql.json" --permissions "test_role_with_excluded_fields_on_mutation:update" --fields.exclude "publisher_id"
126129
update Book --config "dab-config.MsSql.json" --permissions "role_multiple_create_policy_tester:read" --policy-database "@item.publisher_id ne 1234"
127130
update Book --config "dab-config.MsSql.json" --permissions "role_multiple_create_policy_tester:create" --policy-database "@item.title ne 'Test'"
128131
update Book --config "dab-config.MsSql.json" --permissions "role_multiple_create_policy_tester:update,delete"
@@ -142,6 +145,9 @@ update BookWebsitePlacement --config "dab-config.MsSql.json" --permissions "auth
142145
update BookWebsitePlacement --config "dab-config.MsSql.json" --permissions "authenticated:delete" --fields.include "*" --policy-database "@claims.userId eq @item.id"
143146
update Author --config "dab-config.MsSql.json" --permissions "authenticated:create,read,update,delete" --rest true --graphql true
144147
update WebsiteUser --config "dab-config.MsSql.json" --permissions "authenticated:create,read,delete,update" --rest false --graphql "websiteUser:websiteUsers"
148+
update WebsiteUser --config "dab-config.MsSql.json" --permissions "test_role_with_excluded_fields_on_mutation:read,delete"
149+
update WebsiteUser --config "dab-config.MsSql.json" --permissions "test_role_with_excluded_fields_on_mutation:create" --fields.exclude "username"
150+
update WebsiteUser --config "dab-config.MsSql.json" --permissions "test_role_with_excluded_fields_on_mutation:update" --fields.exclude "username"
145151
update WebsiteUser -c "dab-config.MsSql.json" --relationship reviews --target.entity Review --cardinality many --relationship.fields "id:websiteuser_id"
146152
update WebsiteUser_MM --config "dab-config.MsSql.json" --source website_users_mm --permissions "authenticated:*" --relationship reviews --relationship.fields "id:websiteuser_id" --target.entity Review_MM --cardinality many
147153
update Revenue --config "dab-config.MsSql.json" --permissions "database_policy_tester:create" --policy-database "@item.revenue gt 1000"

src/Service.Tests/Authorization/AuthorizationResolverUnitTests.cs

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -905,6 +905,163 @@ public void WildcardColumnInclusionWithExplicitExclusion()
905905
excludedColumns));
906906
}
907907

908+
/// <summary>
909+
/// Reproduces the real-world Book entity scenario: a single role with MULTIPLE actions
910+
/// (read with no field restriction, create/update excluding a column, delete with no
911+
/// field restriction) all defined in one EntityPermission.Actions array - unlike
912+
/// AuthorizationHelpers.InitRuntimeConfig which only ever builds a single action per role.
913+
/// </summary>
914+
[TestMethod("Multiple actions per role - column exclusion on create/update only")]
915+
public void MultipleActionsPerRole_ColumnExclusionOnCreateAndUpdate()
916+
{
917+
EntityActionFields createUpdateFields = new(Exclude: new() { "col3" });
918+
EntityAction readAction = new(Action: EntityActionOperation.Read, Fields: null, Policy: new(null, null));
919+
EntityAction createAction = new(Action: EntityActionOperation.Create, Fields: createUpdateFields, Policy: new(null, null));
920+
EntityAction updateAction = new(Action: EntityActionOperation.Update, Fields: createUpdateFields, Policy: new(null, null));
921+
EntityAction deleteAction = new(Action: EntityActionOperation.Delete, Fields: null, Policy: new(null, null));
922+
923+
Azure.DataApiBuilder.Config.ObjectModel.EntityPermission permissionForEntity = new(
924+
Role: AuthorizationHelpers.TEST_ROLE,
925+
Actions: new EntityAction[] { readAction, createAction, updateAction, deleteAction });
926+
927+
Azure.DataApiBuilder.Config.ObjectModel.Entity sampleEntity = new(
928+
Source: new Azure.DataApiBuilder.Config.ObjectModel.EntitySource(AuthorizationHelpers.TEST_ENTITY, Azure.DataApiBuilder.Config.ObjectModel.EntitySourceType.Table, null, null),
929+
Fields: null,
930+
Rest: new(Array.Empty<SupportedHttpVerb>()),
931+
GraphQL: new(AuthorizationHelpers.TEST_ENTITY, AuthorizationHelpers.TEST_ENTITY),
932+
Permissions: new Azure.DataApiBuilder.Config.ObjectModel.EntityPermission[] { permissionForEntity },
933+
Relationships: null,
934+
Mappings: null);
935+
936+
Azure.DataApiBuilder.Config.ObjectModel.RuntimeConfig runtimeConfig = new(
937+
Schema: "UnitTestSchema",
938+
DataSource: new Azure.DataApiBuilder.Config.ObjectModel.DataSource(Azure.DataApiBuilder.Config.ObjectModel.DatabaseType.MSSQL, "", new()),
939+
Runtime: new(
940+
Rest: new(),
941+
GraphQL: new(),
942+
Mcp: new(),
943+
Host: new(
944+
Cors: null,
945+
Authentication: new("AppService", null))),
946+
Entities: new(new Dictionary<string, Azure.DataApiBuilder.Config.ObjectModel.Entity> { { AuthorizationHelpers.TEST_ENTITY, sampleEntity } }));
947+
948+
AuthorizationResolver authZResolver = AuthorizationHelpers.InitAuthorizationResolver(runtimeConfig);
949+
950+
// Read should allow all columns (no exclusion for read).
951+
Assert.IsTrue(authZResolver.AreColumnsAllowedForOperation(
952+
AuthorizationHelpers.TEST_ENTITY,
953+
AuthorizationHelpers.TEST_ROLE,
954+
operation: EntityActionOperation.Read,
955+
new List<string> { "col1", "col3" }));
956+
957+
// Create should DENY col3 since it is excluded for create.
958+
Assert.IsFalse(authZResolver.AreColumnsAllowedForOperation(
959+
AuthorizationHelpers.TEST_ENTITY,
960+
AuthorizationHelpers.TEST_ROLE,
961+
operation: EntityActionOperation.Create,
962+
new List<string> { "col1", "col3" }));
963+
964+
// Update should DENY col3 since it is excluded for update.
965+
Assert.IsFalse(authZResolver.AreColumnsAllowedForOperation(
966+
AuthorizationHelpers.TEST_ENTITY,
967+
AuthorizationHelpers.TEST_ROLE,
968+
operation: EntityActionOperation.Update,
969+
new List<string> { "col1", "col3" }));
970+
971+
// Round-trip the config through JSON serialization/deserialization (as happens when DAB
972+
// loads a real config file from disk) to rule out any bug specific to the JSON converters
973+
// (e.g. shared/aliased Fields.Exclude HashSet instances across sibling actions).
974+
string json = runtimeConfig.ToJson();
975+
Assert.IsTrue(Azure.DataApiBuilder.Config.RuntimeConfigLoader.TryParseConfig(json, out Azure.DataApiBuilder.Config.ObjectModel.RuntimeConfig? roundTrippedConfig));
976+
AuthorizationResolver roundTrippedResolver = AuthorizationHelpers.InitAuthorizationResolver(roundTrippedConfig!);
977+
978+
Assert.IsTrue(roundTrippedResolver.AreColumnsAllowedForOperation(
979+
AuthorizationHelpers.TEST_ENTITY,
980+
AuthorizationHelpers.TEST_ROLE,
981+
operation: EntityActionOperation.Read,
982+
new List<string> { "col1", "col3" }));
983+
984+
Assert.IsFalse(roundTrippedResolver.AreColumnsAllowedForOperation(
985+
AuthorizationHelpers.TEST_ENTITY,
986+
AuthorizationHelpers.TEST_ROLE,
987+
operation: EntityActionOperation.Create,
988+
new List<string> { "col1", "col3" }));
989+
990+
Assert.IsFalse(roundTrippedResolver.AreColumnsAllowedForOperation(
991+
AuthorizationHelpers.TEST_ENTITY,
992+
AuthorizationHelpers.TEST_ROLE,
993+
operation: EntityActionOperation.Update,
994+
new List<string> { "col1", "col3" }));
995+
}
996+
997+
/// <summary>
998+
/// Parses the EXACT real-world Book entity config structure from JSON (entity-level "fields"
999+
/// array listing only id/title, plus a multi-action role that excludes publisher_id for
1000+
/// create/update) and asserts that Fields.Exclude survives deserialization for each action.
1001+
/// This isolates whether the bug is in the JSON config converters (EntityActionConverter /
1002+
/// EntityActionFields) independent of any database/metadata provider.
1003+
/// </summary>
1004+
[TestMethod("Real config JSON: Fields.Exclude survives parse for multi-action role")]
1005+
public void RealConfigJson_FieldsExcludeSurvivesParse()
1006+
{
1007+
string configJson = @"{
1008+
""$schema"": ""https://github.com/Azure/data-api-builder/releases/download/v0.10.23/dab.draft.schema.json"",
1009+
""data-source"": {
1010+
""database-type"": ""mssql"",
1011+
""connection-string"": ""Server=localhost;Database=master;""
1012+
},
1013+
""runtime"": {
1014+
""rest"": { ""enabled"": true, ""path"": ""/api"", ""request-body-strict"": true },
1015+
""graphql"": { ""enabled"": true, ""path"": ""/graphql"" },
1016+
""host"": { ""mode"": ""development"", ""authentication"": { ""provider"": ""StaticWebApps"" } }
1017+
},
1018+
""entities"": {
1019+
""Book"": {
1020+
""source"": { ""object"": ""books"", ""type"": ""table"" },
1021+
""fields"": [
1022+
{ ""name"": ""id"", ""alias"": ""id"", ""primary-key"": false },
1023+
{ ""name"": ""title"", ""alias"": ""title"", ""primary-key"": false }
1024+
],
1025+
""permissions"": [
1026+
{
1027+
""role"": ""test_role_with_excluded_fields_on_mutation"",
1028+
""actions"": [
1029+
{ ""action"": ""read"" },
1030+
{ ""action"": ""create"", ""fields"": { ""exclude"": [ ""publisher_id"" ] } },
1031+
{ ""action"": ""update"", ""fields"": { ""exclude"": [ ""publisher_id"" ] } },
1032+
{ ""action"": ""delete"" }
1033+
]
1034+
}
1035+
]
1036+
}
1037+
}
1038+
}";
1039+
1040+
Assert.IsTrue(
1041+
Azure.DataApiBuilder.Config.RuntimeConfigLoader.TryParseConfig(configJson, out Azure.DataApiBuilder.Config.ObjectModel.RuntimeConfig? config),
1042+
"Config should parse successfully.");
1043+
Assert.IsNotNull(config);
1044+
1045+
Azure.DataApiBuilder.Config.ObjectModel.Entity bookEntity = config!.Entities["Book"];
1046+
Azure.DataApiBuilder.Config.ObjectModel.EntityPermission permission =
1047+
bookEntity.Permissions.Single(p => p.Role == "test_role_with_excluded_fields_on_mutation");
1048+
1049+
Azure.DataApiBuilder.Config.ObjectModel.EntityAction createAction =
1050+
permission.Actions.Single(a => a.Action == EntityActionOperation.Create);
1051+
Azure.DataApiBuilder.Config.ObjectModel.EntityAction updateAction =
1052+
permission.Actions.Single(a => a.Action == EntityActionOperation.Update);
1053+
1054+
Assert.IsNotNull(createAction.Fields, "Create action Fields should not be null after parsing.");
1055+
Assert.IsNotNull(updateAction.Fields, "Update action Fields should not be null after parsing.");
1056+
1057+
Assert.IsTrue(
1058+
createAction.Fields!.Exclude.Contains("publisher_id"),
1059+
$"Create action Exclude should contain publisher_id. Actual: [{string.Join(",", createAction.Fields.Exclude)}]");
1060+
Assert.IsTrue(
1061+
updateAction.Fields!.Exclude.Contains("publisher_id"),
1062+
$"Update action Exclude should contain publisher_id. Actual: [{string.Join(",", updateAction.Fields.Exclude)}]");
1063+
}
1064+
9081065
/// <summary>
9091066
/// Test that all columns should be excluded if the exclusion contains wildcard character.
9101067
/// </summary>

src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -909,6 +909,33 @@
909909
}
910910
]
911911
},
912+
{
913+
Role: test_role_with_excluded_fields_on_mutation,
914+
Actions: [
915+
{
916+
Action: Update,
917+
Fields: {
918+
Exclude: [
919+
publisher_id
920+
]
921+
}
922+
},
923+
{
924+
Action: Create,
925+
Fields: {
926+
Exclude: [
927+
publisher_id
928+
]
929+
}
930+
},
931+
{
932+
Action: Read
933+
},
934+
{
935+
Action: Delete
936+
}
937+
]
938+
},
912939
{
913940
Role: role_multiple_create_policy_tester,
914941
Actions: [
@@ -1592,6 +1619,33 @@
15921619
Action: Update
15931620
}
15941621
]
1622+
},
1623+
{
1624+
Role: test_role_with_excluded_fields_on_mutation,
1625+
Actions: [
1626+
{
1627+
Action: Update,
1628+
Fields: {
1629+
Exclude: [
1630+
username
1631+
]
1632+
}
1633+
},
1634+
{
1635+
Action: Create,
1636+
Fields: {
1637+
Exclude: [
1638+
username
1639+
]
1640+
}
1641+
},
1642+
{
1643+
Action: Read
1644+
},
1645+
{
1646+
Action: Delete
1647+
}
1648+
]
15951649
}
15961650
],
15971651
Relationships: {

0 commit comments

Comments
 (0)