@@ -905,6 +905,98 @@ 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 readFields = new ( Exclude : new ( ) ) ;
918+ EntityActionFields createUpdateFields = new ( Exclude : new ( ) { "col3" } ) ;
919+ EntityActionFields deleteFields = new ( Exclude : new ( ) ) ;
920+
921+ EntityAction readAction = new ( Action : EntityActionOperation . Read , Fields : null , Policy : new ( null , null ) ) ;
922+ EntityAction createAction = new ( Action : EntityActionOperation . Create , Fields : createUpdateFields , Policy : new ( null , null ) ) ;
923+ EntityAction updateAction = new ( Action : EntityActionOperation . Update , Fields : createUpdateFields , Policy : new ( null , null ) ) ;
924+ EntityAction deleteAction = new ( Action : EntityActionOperation . Delete , Fields : null , Policy : new ( null , null ) ) ;
925+
926+ Azure . DataApiBuilder . Config . ObjectModel . EntityPermission permissionForEntity = new (
927+ Role : AuthorizationHelpers . TEST_ROLE ,
928+ Actions : new EntityAction [ ] { readAction , createAction , updateAction , deleteAction } ) ;
929+
930+ Azure . DataApiBuilder . Config . ObjectModel . Entity sampleEntity = new (
931+ Source : new Azure . DataApiBuilder . Config . ObjectModel . EntitySource ( AuthorizationHelpers . TEST_ENTITY , Azure . DataApiBuilder . Config . ObjectModel . EntitySourceType . Table , null , null ) ,
932+ Fields : null ,
933+ Rest : new ( Array . Empty < SupportedHttpVerb > ( ) ) ,
934+ GraphQL : new ( AuthorizationHelpers . TEST_ENTITY , AuthorizationHelpers . TEST_ENTITY ) ,
935+ Permissions : new Azure . DataApiBuilder . Config . ObjectModel . EntityPermission [ ] { permissionForEntity } ,
936+ Relationships : null ,
937+ Mappings : null ) ;
938+
939+ Azure . DataApiBuilder . Config . ObjectModel . RuntimeConfig runtimeConfig = new (
940+ Schema : "UnitTestSchema" ,
941+ DataSource : new Azure . DataApiBuilder . Config . ObjectModel . DataSource ( Azure . DataApiBuilder . Config . ObjectModel . DatabaseType . MSSQL , "" , new ( ) ) ,
942+ Runtime : new (
943+ Rest : new ( ) ,
944+ GraphQL : new ( ) ,
945+ Mcp : new ( ) ,
946+ Host : new (
947+ Cors : null ,
948+ Authentication : new ( "AppService" , null ) ) ) ,
949+ Entities : new ( new Dictionary < string , Azure . DataApiBuilder . Config . ObjectModel . Entity > { { AuthorizationHelpers . TEST_ENTITY , sampleEntity } } ) ) ;
950+
951+ AuthorizationResolver authZResolver = AuthorizationHelpers . InitAuthorizationResolver ( runtimeConfig ) ;
952+
953+ // Read should allow all columns (no exclusion for read).
954+ Assert . IsTrue ( authZResolver . AreColumnsAllowedForOperation (
955+ AuthorizationHelpers . TEST_ENTITY ,
956+ AuthorizationHelpers . TEST_ROLE ,
957+ operation : EntityActionOperation . Read ,
958+ new List < string > { "col1" , "col3" } ) ) ;
959+
960+ // Create should DENY col3 since it is excluded for create.
961+ Assert . IsFalse ( authZResolver . AreColumnsAllowedForOperation (
962+ AuthorizationHelpers . TEST_ENTITY ,
963+ AuthorizationHelpers . TEST_ROLE ,
964+ operation : EntityActionOperation . Create ,
965+ new List < string > { "col1" , "col3" } ) ) ;
966+
967+ // Update should DENY col3 since it is excluded for update.
968+ Assert . IsFalse ( authZResolver . AreColumnsAllowedForOperation (
969+ AuthorizationHelpers . TEST_ENTITY ,
970+ AuthorizationHelpers . TEST_ROLE ,
971+ operation : EntityActionOperation . Update ,
972+ new List < string > { "col1" , "col3" } ) ) ;
973+
974+ // Round-trip the config through JSON serialization/deserialization (as happens when DAB
975+ // loads a real config file from disk) to rule out any bug specific to the JSON converters
976+ // (e.g. shared/aliased Fields.Exclude HashSet instances across sibling actions).
977+ string json = runtimeConfig . ToJson ( ) ;
978+ Assert . IsTrue ( Azure . DataApiBuilder . Config . RuntimeConfigLoader . TryParseConfig ( json , out Azure . DataApiBuilder . Config . ObjectModel . RuntimeConfig ? roundTrippedConfig ) ) ;
979+ AuthorizationResolver roundTrippedResolver = AuthorizationHelpers . InitAuthorizationResolver ( roundTrippedConfig ! ) ;
980+
981+ Assert . IsTrue ( roundTrippedResolver . AreColumnsAllowedForOperation (
982+ AuthorizationHelpers . TEST_ENTITY ,
983+ AuthorizationHelpers . TEST_ROLE ,
984+ operation : EntityActionOperation . Read ,
985+ new List < string > { "col1" , "col3" } ) ) ;
986+
987+ Assert . IsFalse ( roundTrippedResolver . AreColumnsAllowedForOperation (
988+ AuthorizationHelpers . TEST_ENTITY ,
989+ AuthorizationHelpers . TEST_ROLE ,
990+ operation : EntityActionOperation . Create ,
991+ new List < string > { "col1" , "col3" } ) ) ;
992+
993+ Assert . IsFalse ( roundTrippedResolver . AreColumnsAllowedForOperation (
994+ AuthorizationHelpers . TEST_ENTITY ,
995+ AuthorizationHelpers . TEST_ROLE ,
996+ operation : EntityActionOperation . Update ,
997+ new List < string > { "col1" , "col3" } ) ) ;
998+ }
999+
9081000 /// <summary>
9091001 /// Test that all columns should be excluded if the exclusion contains wildcard character.
9101002 /// </summary>
0 commit comments