@@ -377,42 +377,102 @@ pub fn validate_l7_policies(data_json: &serde_json::Value) -> (Vec<String>, Vec<
377377 }
378378 }
379379
380- // Validate query matchers (same rules as allow query matchers)
380+ // Validate query matchers — mirrors allow-side validation exactly
381381 if let Some ( query) = deny_rule. get ( "query" ) . filter ( |v| !v. is_null ( ) ) {
382- if let Some ( query_obj) = query. as_object ( ) {
383- for ( param, matcher) in query_obj {
384- if let Some ( glob_str) = matcher. as_str ( ) {
385- if let Some ( warning) = check_glob_syntax ( glob_str) {
386- warnings. push ( format ! (
387- "{deny_loc}.query.{param}: {warning}"
388- ) ) ;
389- }
390- continue ;
382+ let Some ( query_obj) = query. as_object ( ) else {
383+ errors. push ( format ! (
384+ "{deny_loc}.query: expected map of query matchers"
385+ ) ) ;
386+ continue ;
387+ } ;
388+
389+ for ( param, matcher) in query_obj {
390+ if let Some ( glob_str) = matcher. as_str ( ) {
391+ if let Some ( warning) = check_glob_syntax ( glob_str) {
392+ warnings
393+ . push ( format ! ( "{deny_loc}.query.{param}: {warning}" ) ) ;
391394 }
392- if let Some ( matcher_obj) = matcher. as_object ( ) {
393- let has_any = matcher_obj. get ( "any" ) . is_some ( ) ;
394- let has_glob = matcher_obj. get ( "glob" ) . is_some ( ) ;
395- let has_unknown =
396- matcher_obj. keys ( ) . any ( |k| k != "any" && k != "glob" ) ;
397- if has_unknown {
398- errors. push ( format ! (
399- "{deny_loc}.query.{param}: unknown matcher keys; only `glob` or `any` are supported"
400- ) ) ;
401- } else if has_glob && has_any {
402- errors. push ( format ! (
403- "{deny_loc}.query.{param}: matcher cannot specify both `glob` and `any`"
404- ) ) ;
405- } else if !has_glob && !has_any {
395+ continue ;
396+ }
397+
398+ let Some ( matcher_obj) = matcher. as_object ( ) else {
399+ errors. push ( format ! (
400+ "{deny_loc}.query.{param}: expected string glob or object with `any`"
401+ ) ) ;
402+ continue ;
403+ } ;
404+
405+ let has_any = matcher_obj. get ( "any" ) . is_some ( ) ;
406+ let has_glob = matcher_obj. get ( "glob" ) . is_some ( ) ;
407+ let has_unknown =
408+ matcher_obj. keys ( ) . any ( |k| k != "any" && k != "glob" ) ;
409+ if has_unknown {
410+ errors. push ( format ! (
411+ "{deny_loc}.query.{param}: unknown matcher keys; only `glob` or `any` are supported"
412+ ) ) ;
413+ continue ;
414+ }
415+
416+ if has_glob && has_any {
417+ errors. push ( format ! (
418+ "{deny_loc}.query.{param}: matcher cannot specify both `glob` and `any`"
419+ ) ) ;
420+ continue ;
421+ }
422+
423+ if !has_glob && !has_any {
424+ errors. push ( format ! (
425+ "{deny_loc}.query.{param}: object matcher requires `glob` string or non-empty `any` list"
426+ ) ) ;
427+ continue ;
428+ }
429+
430+ if has_glob {
431+ match matcher_obj. get ( "glob" ) . and_then ( |v| v. as_str ( ) ) {
432+ None => {
406433 errors. push ( format ! (
407- "{deny_loc}.query.{param}: object matcher requires ` glob` string or non-empty `any` list "
434+ "{deny_loc}.query.{param}.glob: expected glob string"
408435 ) ) ;
409436 }
437+ Some ( g) => {
438+ if let Some ( warning) = check_glob_syntax ( g) {
439+ warnings. push ( format ! (
440+ "{deny_loc}.query.{param}.glob: {warning}"
441+ ) ) ;
442+ }
443+ }
444+ }
445+ continue ;
446+ }
447+
448+ let any = matcher_obj. get ( "any" ) . and_then ( |v| v. as_array ( ) ) ;
449+ let Some ( any) = any else {
450+ errors. push ( format ! (
451+ "{deny_loc}.query.{param}.any: expected array of glob strings"
452+ ) ) ;
453+ continue ;
454+ } ;
455+
456+ if any. is_empty ( ) {
457+ errors. push ( format ! (
458+ "{deny_loc}.query.{param}.any: list must not be empty"
459+ ) ) ;
460+ continue ;
461+ }
462+
463+ if any. iter ( ) . any ( |v| v. as_str ( ) . is_none ( ) ) {
464+ errors. push ( format ! (
465+ "{deny_loc}.query.{param}.any: all values must be strings"
466+ ) ) ;
467+ }
468+
469+ for item in any. iter ( ) . filter_map ( |v| v. as_str ( ) ) {
470+ if let Some ( warning) = check_glob_syntax ( item) {
471+ warnings. push ( format ! (
472+ "{deny_loc}.query.{param}.any: {warning}"
473+ ) ) ;
410474 }
411475 }
412- } else {
413- errors. push ( format ! (
414- "{deny_loc}.query: expected map of query matchers"
415- ) ) ;
416476 }
417477 }
418478
@@ -1269,4 +1329,193 @@ mod tests {
12691329 "valid query matcher shapes should not error: {errors:?}"
12701330 ) ;
12711331 }
1332+
1333+ // --- Deny rules validation tests ---
1334+
1335+ #[ test]
1336+ fn validate_deny_rules_require_protocol ( ) {
1337+ let data = serde_json:: json!( {
1338+ "network_policies" : {
1339+ "test" : {
1340+ "endpoints" : [ {
1341+ "host" : "api.example.com" ,
1342+ "port" : 443 ,
1343+ "deny_rules" : [ { "method" : "POST" , "path" : "/admin" } ]
1344+ } ] ,
1345+ "binaries" : [ ]
1346+ }
1347+ }
1348+ } ) ;
1349+ let ( errors, _) = validate_l7_policies ( & data) ;
1350+ assert ! (
1351+ errors
1352+ . iter( )
1353+ . any( |e| e. contains( "deny_rules require protocol" ) ) ,
1354+ "should require protocol for deny_rules: {errors:?}"
1355+ ) ;
1356+ }
1357+
1358+ #[ test]
1359+ fn validate_deny_rules_require_allow_base ( ) {
1360+ let data = serde_json:: json!( {
1361+ "network_policies" : {
1362+ "test" : {
1363+ "endpoints" : [ {
1364+ "host" : "api.example.com" ,
1365+ "port" : 443 ,
1366+ "protocol" : "rest" ,
1367+ "deny_rules" : [ { "method" : "POST" , "path" : "/admin" } ]
1368+ } ] ,
1369+ "binaries" : [ ]
1370+ }
1371+ }
1372+ } ) ;
1373+ let ( errors, _) = validate_l7_policies ( & data) ;
1374+ assert ! (
1375+ errors
1376+ . iter( )
1377+ . any( |e| e. contains( "deny_rules require rules or access" ) ) ,
1378+ "should require rules or access for deny_rules: {errors:?}"
1379+ ) ;
1380+ }
1381+
1382+ #[ test]
1383+ fn validate_deny_rules_empty_list_rejected ( ) {
1384+ let data = serde_json:: json!( {
1385+ "network_policies" : {
1386+ "test" : {
1387+ "endpoints" : [ {
1388+ "host" : "api.example.com" ,
1389+ "port" : 443 ,
1390+ "protocol" : "rest" ,
1391+ "access" : "full" ,
1392+ "deny_rules" : [ ]
1393+ } ] ,
1394+ "binaries" : [ ]
1395+ }
1396+ }
1397+ } ) ;
1398+ let ( errors, _) = validate_l7_policies ( & data) ;
1399+ assert ! (
1400+ errors
1401+ . iter( )
1402+ . any( |e| e. contains( "deny_rules list cannot be empty" ) ) ,
1403+ "should reject empty deny_rules: {errors:?}"
1404+ ) ;
1405+ }
1406+
1407+ #[ test]
1408+ fn validate_deny_rules_valid_config_accepted ( ) {
1409+ let data = serde_json:: json!( {
1410+ "network_policies" : {
1411+ "test" : {
1412+ "endpoints" : [ {
1413+ "host" : "api.example.com" ,
1414+ "port" : 443 ,
1415+ "protocol" : "rest" ,
1416+ "access" : "read-write" ,
1417+ "deny_rules" : [
1418+ { "method" : "POST" , "path" : "/repos/*/pulls/*/reviews" } ,
1419+ { "method" : "PUT" , "path" : "/repos/*/branches/*/protection" }
1420+ ]
1421+ } ] ,
1422+ "binaries" : [ ]
1423+ }
1424+ }
1425+ } ) ;
1426+ let ( errors, _) = validate_l7_policies ( & data) ;
1427+ assert ! (
1428+ errors. is_empty( ) ,
1429+ "valid deny_rules should not error: {errors:?}"
1430+ ) ;
1431+ }
1432+
1433+ #[ test]
1434+ fn validate_deny_rules_query_empty_any_rejected ( ) {
1435+ let data = serde_json:: json!( {
1436+ "network_policies" : {
1437+ "test" : {
1438+ "endpoints" : [ {
1439+ "host" : "api.example.com" ,
1440+ "port" : 443 ,
1441+ "protocol" : "rest" ,
1442+ "access" : "full" ,
1443+ "deny_rules" : [ {
1444+ "method" : "POST" ,
1445+ "path" : "/admin" ,
1446+ "query" : { "type" : { "any" : [ ] } }
1447+ } ]
1448+ } ] ,
1449+ "binaries" : [ ]
1450+ }
1451+ }
1452+ } ) ;
1453+ let ( errors, _) = validate_l7_policies ( & data) ;
1454+ assert ! (
1455+ errors
1456+ . iter( )
1457+ . any( |e| e. contains( "any: list must not be empty" ) ) ,
1458+ "should reject empty any list in deny query: {errors:?}"
1459+ ) ;
1460+ }
1461+
1462+ #[ test]
1463+ fn validate_deny_rules_query_non_string_rejected ( ) {
1464+ let data = serde_json:: json!( {
1465+ "network_policies" : {
1466+ "test" : {
1467+ "endpoints" : [ {
1468+ "host" : "api.example.com" ,
1469+ "port" : 443 ,
1470+ "protocol" : "rest" ,
1471+ "access" : "full" ,
1472+ "deny_rules" : [ {
1473+ "method" : "POST" ,
1474+ "path" : "/admin" ,
1475+ "query" : { "force" : 123 }
1476+ } ]
1477+ } ] ,
1478+ "binaries" : [ ]
1479+ }
1480+ }
1481+ } ) ;
1482+ let ( errors, _) = validate_l7_policies ( & data) ;
1483+ assert ! (
1484+ errors
1485+ . iter( )
1486+ . any( |e| e. contains( "expected string glob or object" ) ) ,
1487+ "should reject non-string/non-object matcher in deny query: {errors:?}"
1488+ ) ;
1489+ }
1490+
1491+ #[ test]
1492+ fn validate_deny_rules_query_valid_matchers_accepted ( ) {
1493+ let data = serde_json:: json!( {
1494+ "network_policies" : {
1495+ "test" : {
1496+ "endpoints" : [ {
1497+ "host" : "api.example.com" ,
1498+ "port" : 443 ,
1499+ "protocol" : "rest" ,
1500+ "access" : "full" ,
1501+ "deny_rules" : [ {
1502+ "method" : "POST" ,
1503+ "path" : "/admin/**" ,
1504+ "query" : {
1505+ "force" : "true" ,
1506+ "type" : { "any" : [ "admin-*" , "root-*" ] } ,
1507+ "scope" : { "glob" : "org-*" }
1508+ }
1509+ } ]
1510+ } ] ,
1511+ "binaries" : [ ]
1512+ }
1513+ }
1514+ } ) ;
1515+ let ( errors, _) = validate_l7_policies ( & data) ;
1516+ assert ! (
1517+ errors. is_empty( ) ,
1518+ "valid deny query matchers should not error: {errors:?}"
1519+ ) ;
1520+ }
12721521}
0 commit comments