@@ -1454,23 +1454,76 @@ pub fn validate_l7_policies(data_json: &serde_json::Value) -> (Vec<String>, Vec<
14541454 ( errors, warnings)
14551455}
14561456
1457+ /// Map a supported L7 `access` preset to explicit rules for `protocol`.
1458+ ///
1459+ /// Returns `None` when `access` is not `read-only`, `read-write`, or `full`.
1460+ /// Graphql and websocket presets differ from REST; all other protocols use
1461+ /// REST presets. Requires prior `validate_l7_policies` so mcp/json-rpc `access`
1462+ /// is rejected before expansion runs.
1463+ fn access_preset_rules ( protocol : & str , access : & str ) -> Option < Vec < serde_json:: Value > > {
1464+ if protocol == "graphql" {
1465+ match access {
1466+ "read-only" => Some ( vec ! [ graphql_rule_json( "query" ) ] ) ,
1467+ "read-write" => Some ( vec ! [
1468+ graphql_rule_json( "query" ) ,
1469+ graphql_rule_json( "mutation" ) ,
1470+ ] ) ,
1471+ "full" => Some ( vec ! [ graphql_rule_json( "*" ) ] ) ,
1472+ _ => None ,
1473+ }
1474+ } else if protocol == "websocket" {
1475+ match access {
1476+ "read-only" => Some ( vec ! [ rule_json( "GET" , "**" ) ] ) ,
1477+ "read-write" => Some ( vec ! [
1478+ rule_json( "GET" , "**" ) ,
1479+ rule_json( "WEBSOCKET_TEXT" , "**" ) ,
1480+ ] ) ,
1481+ "full" => Some ( vec ! [ rule_json( "*" , "**" ) ] ) ,
1482+ _ => None ,
1483+ }
1484+ } else {
1485+ match access {
1486+ "read-only" => Some ( vec ! [
1487+ rule_json( "GET" , "**" ) ,
1488+ rule_json( "HEAD" , "**" ) ,
1489+ rule_json( "OPTIONS" , "**" ) ,
1490+ ] ) ,
1491+ "read-write" => Some ( vec ! [
1492+ rule_json( "GET" , "**" ) ,
1493+ rule_json( "HEAD" , "**" ) ,
1494+ rule_json( "OPTIONS" , "**" ) ,
1495+ rule_json( "POST" , "**" ) ,
1496+ rule_json( "PUT" , "**" ) ,
1497+ rule_json( "PATCH" , "**" ) ,
1498+ ] ) ,
1499+ "full" => Some ( vec ! [ rule_json( "*" , "**" ) ] ) ,
1500+ _ => None ,
1501+ }
1502+ }
1503+ }
1504+
14571505/// Expand `access` presets into explicit `rules` in the policy data.
14581506///
14591507/// This preprocesses the JSON data so Rego only needs to handle explicit rules.
1460- pub fn expand_access_presets ( data : & mut serde_json:: Value ) {
1508+ /// Returns warnings for unsupported `access` values that were ignored.
1509+ ///
1510+ /// Unsupported preset detection lives here rather than in `validate_l7_policies`
1511+ /// so supported presets stay defined only in `access_preset_rules`.
1512+ pub fn expand_access_presets ( data : & mut serde_json:: Value ) -> Vec < String > {
1513+ let mut warnings = Vec :: new ( ) ;
14611514 let Some ( policies) = data
14621515 . get_mut ( "network_policies" )
14631516 . and_then ( |v| v. as_object_mut ( ) )
14641517 else {
1465- return ;
1518+ return warnings ;
14661519 } ;
14671520
1468- for ( _name , policy) in policies. iter_mut ( ) {
1521+ for ( name , policy) in policies. iter_mut ( ) {
14691522 let Some ( endpoints) = policy. get_mut ( "endpoints" ) . and_then ( |v| v. as_array_mut ( ) ) else {
14701523 continue ;
14711524 } ;
14721525
1473- for ep in endpoints. iter_mut ( ) {
1526+ for ( i , ep ) in endpoints. iter_mut ( ) . enumerate ( ) {
14741527 let protocol = ep
14751528 . get ( "protocol" )
14761529 . and_then ( |v| v. as_str ( ) )
@@ -1511,45 +1564,22 @@ pub fn expand_access_presets(data: &mut serde_json::Value) {
15111564 continue ;
15121565 }
15131566
1514- let rules = if protocol == "graphql" {
1515- match access. as_str ( ) {
1516- "read-only" => vec ! [ graphql_rule_json( "query" ) ] ,
1517- "read-write" => vec ! [ graphql_rule_json( "query" ) , graphql_rule_json( "mutation" ) ] ,
1518- "full" => vec ! [ graphql_rule_json( "*" ) ] ,
1519- _ => continue ,
1520- }
1521- } else if protocol == "websocket" {
1522- match access. as_str ( ) {
1523- "read-only" => vec ! [ rule_json( "GET" , "**" ) ] ,
1524- "read-write" => vec ! [ rule_json( "GET" , "**" ) , rule_json( "WEBSOCKET_TEXT" , "**" ) ] ,
1525- "full" => vec ! [ rule_json( "*" , "**" ) ] ,
1526- _ => continue ,
1527- }
1528- } else {
1529- match access. as_str ( ) {
1530- "read-only" => vec ! [
1531- rule_json( "GET" , "**" ) ,
1532- rule_json( "HEAD" , "**" ) ,
1533- rule_json( "OPTIONS" , "**" ) ,
1534- ] ,
1535- "read-write" => vec ! [
1536- rule_json( "GET" , "**" ) ,
1537- rule_json( "HEAD" , "**" ) ,
1538- rule_json( "OPTIONS" , "**" ) ,
1539- rule_json( "POST" , "**" ) ,
1540- rule_json( "PUT" , "**" ) ,
1541- rule_json( "PATCH" , "**" ) ,
1542- ] ,
1543- "full" => vec ! [ rule_json( "*" , "**" ) ] ,
1544- _ => continue ,
1545- }
1567+ let Some ( rules) = access_preset_rules ( protocol, & access) else {
1568+ let loc = format ! ( "{name}.endpoints[{i}]" ) ;
1569+ let warning = format ! (
1570+ "{loc}: unsupported L7 access preset '{access}' ignored; expected one of: read-only, read-write, full"
1571+ ) ;
1572+ warnings. push ( warning) ;
1573+ continue ;
15461574 } ;
15471575
15481576 ep. as_object_mut ( )
15491577 . unwrap ( )
15501578 . insert ( "rules" . to_string ( ) , serde_json:: Value :: Array ( rules) ) ;
15511579 }
15521580 }
1581+
1582+ warnings
15531583}
15541584
15551585fn rule_json ( method : & str , path : & str ) -> serde_json:: Value {
@@ -1840,7 +1870,8 @@ mod tests {
18401870 }
18411871 } ) ;
18421872
1843- expand_access_presets ( & mut data) ;
1873+ let warnings = expand_access_presets ( & mut data) ;
1874+ assert ! ( warnings. is_empty( ) , "expected no warnings: {warnings:?}" ) ;
18441875 let rules = data[ "network_policies" ] [ "test" ] [ "endpoints" ] [ 0 ] [ "rules" ]
18451876 . as_array ( )
18461877 . unwrap ( ) ;
@@ -2700,7 +2731,8 @@ mod tests {
27002731 }
27012732 }
27022733 } ) ;
2703- expand_access_presets ( & mut data) ;
2734+ let warnings = expand_access_presets ( & mut data) ;
2735+ assert ! ( warnings. is_empty( ) , "expected no warnings: {warnings:?}" ) ;
27042736 let rules = data[ "network_policies" ] [ "test" ] [ "endpoints" ] [ 0 ] [ "rules" ]
27052737 . as_array ( )
27062738 . unwrap ( ) ;
@@ -2729,7 +2761,8 @@ mod tests {
27292761 }
27302762 }
27312763 } ) ;
2732- expand_access_presets ( & mut data) ;
2764+ let warnings = expand_access_presets ( & mut data) ;
2765+ assert ! ( warnings. is_empty( ) , "expected no warnings: {warnings:?}" ) ;
27332766 let rules = data[ "network_policies" ] [ "test" ] [ "endpoints" ] [ 0 ] [ "rules" ]
27342767 . as_array ( )
27352768 . unwrap ( ) ;
@@ -2738,6 +2771,36 @@ mod tests {
27382771 assert_eq ! ( rules[ 0 ] [ "allow" ] [ "path" ] . as_str( ) . unwrap( ) , "**" ) ;
27392772 }
27402773
2774+ #[ test]
2775+ fn expand_unsupported_access_preset_warns_and_leaves_rules_unexpanded ( ) {
2776+ let mut data = serde_json:: json!( {
2777+ "network_policies" : {
2778+ "allow-my-service" : {
2779+ "endpoints" : [ {
2780+ "host" : "my-service.example.com" ,
2781+ "port" : 80 ,
2782+ "protocol" : "rest" ,
2783+ "access" : "allow"
2784+ } ] ,
2785+ "binaries" : [ ]
2786+ }
2787+ }
2788+ } ) ;
2789+ let warnings = expand_access_presets ( & mut data) ;
2790+ assert ! (
2791+ data[ "network_policies" ] [ "allow-my-service" ] [ "endpoints" ] [ 0 ]
2792+ . get( "rules" )
2793+ . is_none( ) ,
2794+ "unsupported access preset must not expand rules"
2795+ ) ;
2796+ assert_eq ! (
2797+ warnings,
2798+ vec![
2799+ "allow-my-service.endpoints[0]: unsupported L7 access preset 'allow' ignored; expected one of: read-only, read-write, full" . to_string( )
2800+ ]
2801+ ) ;
2802+ }
2803+
27412804 #[ test]
27422805 fn expand_graphql_readonly_preset ( ) {
27432806 let mut data = serde_json:: json!( {
@@ -2753,7 +2816,8 @@ mod tests {
27532816 }
27542817 }
27552818 } ) ;
2756- expand_access_presets ( & mut data) ;
2819+ let warnings = expand_access_presets ( & mut data) ;
2820+ assert ! ( warnings. is_empty( ) , "expected no warnings: {warnings:?}" ) ;
27572821 let rules = data[ "network_policies" ] [ "test" ] [ "endpoints" ] [ 0 ] [ "rules" ]
27582822 . as_array ( )
27592823 . unwrap ( ) ;
@@ -2826,7 +2890,8 @@ mod tests {
28262890 }
28272891 }
28282892 } ) ;
2829- expand_access_presets ( & mut data) ;
2893+ let warnings = expand_access_presets ( & mut data) ;
2894+ assert ! ( warnings. is_empty( ) , "expected no warnings: {warnings:?}" ) ;
28302895 assert ! (
28312896 data[ "network_policies" ] [ "test" ] [ "endpoints" ] [ 0 ]
28322897 . get( "rules" )
0 commit comments