33
44//! Exact finite abstraction of decoded REST query parameters for exact / `*`
55//! matchers. Each key has one Boolean per literal mentioned by either policy,
6- //! plus one shared class for all other values. Multiple classes may be present:
6+ //! plus separate wildcard-matching and nonmatching classes for other values.
7+ //! The runtime's `glob.match(pattern, [], value)` uses `.` as a delimiter,
8+ //! so `*` does not match dotted values. Multiple classes may be present:
79//! runtime allow rules require ALL repeated values to match, while deny rules
810//! require ANY value to match. Multiplicity and order do not affect either rule.
911//! All-false represents an absent key. The REST parser never produces a present
@@ -45,8 +47,13 @@ pub(super) struct SymbolicQuery(BTreeMap<String, QueryKey>);
4547
4648struct QueryKey {
4749 literals : BTreeMap < String , Bool > ,
48- other : Bool ,
49- other_value : String ,
50+ // Index 0 matches `*`; index 1 does not.
51+ other : [ Bool ; 2 ] ,
52+ other_values : [ String ; 2 ] ,
53+ }
54+
55+ fn wildcard_matches ( value : & str ) -> bool {
56+ !value. contains ( '.' )
5057}
5158
5259pub ( super ) fn supported ( rules : & QueryRules ) -> bool {
@@ -63,7 +70,9 @@ pub(super) fn supported(rules: &QueryRules) -> bool {
6370pub ( super ) fn contains ( boundary : & QueryRules , candidate : & QueryRules ) -> bool {
6471 boundary. iter ( ) . all ( |( key, required) | {
6572 candidate. get ( key) . is_some_and ( |proposed| {
66- required == proposed || matches ! ( required, QueryMatcher :: Glob ( value) if value == "*" )
73+ required == proposed
74+ || ( matches ! ( required, QueryMatcher :: Glob ( value) if value == "*" )
75+ && matches ! ( proposed, QueryMatcher :: Glob ( value) if wildcard_matches( value) ) )
6776 } )
6877 } )
6978}
@@ -110,9 +119,11 @@ impl SymbolicQuery {
110119 . into_iter ( )
111120 . enumerate ( )
112121 . map ( |( key_index, ( key, values) ) | {
113- let mut other_value = String :: new ( ) ;
114- while values. contains ( & other_value) {
115- other_value. push ( 'a' ) ;
122+ let mut other_values = [ String :: new ( ) , "." . to_owned ( ) ] ;
123+ for value in & mut other_values {
124+ while values. contains ( value) {
125+ value. push ( 'a' ) ;
126+ }
116127 }
117128 (
118129 key,
@@ -129,8 +140,10 @@ impl SymbolicQuery {
129140 )
130141 } )
131142 . collect ( ) ,
132- other : Bool :: new_const ( format ! ( "{name}_query_{key_index}_other" ) ) ,
133- other_value,
143+ other : std:: array:: from_fn ( |class| {
144+ Bool :: new_const ( format ! ( "{name}_query_{key_index}_other_{class}" ) )
145+ } ) ,
146+ other_values,
134147 } ,
135148 )
136149 } )
@@ -146,11 +159,12 @@ impl SymbolicQuery {
146159 let mut query = Self :: new ( "concrete" , boundary, candidate) ;
147160 for ( key, classes) in & mut query. 0 {
148161 let present = values. get ( key) . map_or ( & [ ] [ ..] , Vec :: as_slice) ;
149- classes. other = Bool :: from_bool (
150- present
151- . iter ( )
152- . any ( |value| !classes. literals . contains_key ( value) ) ,
153- ) ;
162+ classes. other = std:: array:: from_fn ( |class| {
163+ Bool :: from_bool ( present. iter ( ) . any ( |value| {
164+ !classes. literals . contains_key ( value)
165+ && usize:: from ( !wildcard_matches ( value) ) == class
166+ } ) )
167+ } ) ;
154168 for ( value, flag) in & mut classes. literals {
155169 * flag = Bool :: from_bool ( present. contains ( value) ) ;
156170 }
@@ -168,19 +182,35 @@ impl SymbolicQuery {
168182 unreachable ! ( "query matchers must be validated before modeling" )
169183 } ;
170184 if value == "*" {
171- bool_or (
185+ let matching = bool_or (
172186 classes
173187 . literals
174- . values ( )
175- . cloned ( )
176- . chain ( [ classes. other . clone ( ) ] ) ,
177- )
188+ . iter ( )
189+ . filter ( |( literal, _) | wildcard_matches ( literal) )
190+ . map ( |( _, flag) | flag. clone ( ) )
191+ . chain ( [ classes. other [ 0 ] . clone ( ) ] ) ,
192+ ) ;
193+ if deny {
194+ matching
195+ } else {
196+ Bool :: and ( & [
197+ matching,
198+ !bool_or (
199+ classes
200+ . literals
201+ . iter ( )
202+ . filter ( |( literal, _) | !wildcard_matches ( literal) )
203+ . map ( |( _, flag) | flag. clone ( ) )
204+ . chain ( [ classes. other [ 1 ] . clone ( ) ] ) ,
205+ ) ,
206+ ] )
207+ }
178208 } else if deny {
179209 classes. literals [ value] . clone ( )
180210 } else {
181211 Bool :: and ( & [
182212 classes. literals [ value] . clone ( ) ,
183- !classes. other . clone ( ) ,
213+ !bool_or ( classes. other . iter ( ) . cloned ( ) ) ,
184214 !bool_or (
185215 classes
186216 . literals
@@ -204,8 +234,10 @@ impl SymbolicQuery {
204234 values. push ( literal. clone ( ) ) ;
205235 }
206236 }
207- if model. eval ( & classes. other , true ) ?. as_bool ( ) ? {
208- values. push ( classes. other_value . clone ( ) ) ;
237+ for ( flag, value) in classes. other . iter ( ) . zip ( & classes. other_values ) {
238+ if model. eval ( flag, true ) ?. as_bool ( ) ? {
239+ values. push ( value. clone ( ) ) ;
240+ }
209241 }
210242 if !values. is_empty ( ) {
211243 query. insert ( key. clone ( ) , values) ;
@@ -222,6 +254,36 @@ mod tests {
222254 use serde_json:: json;
223255 use z3:: ast:: Ast ;
224256
257+ #[ test]
258+ fn symbolic_wildcard_preserves_mixed_other_values_in_witness ( ) {
259+ let policy = super :: super :: parse_policy_str (
260+ r#"{"version":1,"network_policies":{"n":{"endpoints":[{
261+ "host":"example.com","port":443,"protocol":"rest","enforcement":"enforce",
262+ "rules":[{"allow":{"method":"GET","path":"/**","query":{"q":"*"}}}]
263+ }]}}}"# ,
264+ )
265+ . unwrap ( ) ;
266+ let query = SymbolicQuery :: new ( "mixed" , & policy, & policy) ;
267+ let rules = BTreeMap :: from ( [ ( "q" . to_owned ( ) , QueryMatcher :: Glob ( "*" . to_owned ( ) ) ) ] ) ;
268+ let solver = z3:: Solver :: new ( ) ;
269+ solver. assert ( & query. 0 [ "q" ] . other [ 0 ] ) ;
270+ solver. assert ( & query. 0 [ "q" ] . other [ 1 ] ) ;
271+ solver. assert ( !query. matches ( & rules, false ) ) ;
272+ solver. assert ( query. matches ( & rules, true ) ) ;
273+ assert_eq ! ( solver. check( ) , z3:: SatResult :: Sat ) ;
274+ let decoded = query. decode ( & solver. get_model ( ) . unwrap ( ) ) . unwrap ( ) ;
275+ assert_eq ! ( decoded[ "q" ] , [ "" , "." ] ) ;
276+ let concrete = SymbolicQuery :: concrete ( & policy, & policy, & decoded) ;
277+ assert_eq ! (
278+ concrete. matches( & rules, false ) . simplify( ) . as_bool( ) ,
279+ Some ( false )
280+ ) ;
281+ assert_eq ! (
282+ concrete. matches( & rules, true ) . simplify( ) . as_bool( ) ,
283+ Some ( true )
284+ ) ;
285+ }
286+
225287 #[ test]
226288 fn decoded_query_model_matches_runtime_for_missing_and_repeated_values ( ) {
227289 let mut engine = Engine :: new ( ) ;
@@ -248,6 +310,7 @@ deny := data.openshell.sandbox.deny_query_params_match(input.request, input.rule
248310 json ! ( { } ) ,
249311 json ! ( { "service" : "*" } ) ,
250312 json ! ( { "service" : "a" } ) ,
313+ json ! ( { "service" : "a.b" } ) ,
251314 json ! ( { "service" : "" } ) ,
252315 json ! ( { "service" : "a" , "v" : "2" } ) ,
253316 json ! ( { "" : "*" } ) ,
@@ -265,6 +328,12 @@ deny := data.openshell.sandbox.deny_query_params_match(input.request, input.rule
265328 json ! ( { } ) ,
266329 json ! ( { "service" : [ "a" ] } ) ,
267330 json ! ( { "service" : [ "b" ] } ) ,
331+ json ! ( { "service" : [ "a.b" ] } ) ,
332+ json ! ( { "service" : [ "a" , "a.b" ] } ) ,
333+ json ! ( { "service" : [ "a.b" , "a" ] } ) ,
334+ json ! ( { "service" : [ "a.b" , "a.b" ] } ) ,
335+ json ! ( { "service" : [ "." ] } ) ,
336+ json ! ( { "service" : [ "/" , "a/b" , "é" , "\n " ] } ) ,
268337 json ! ( { "service" : [ "a" , "a" ] } ) ,
269338 json ! ( { "service" : [ "a" , "b" ] } ) ,
270339 json ! ( { "service" : [ "" ] } ) ,
0 commit comments