@@ -81,6 +81,7 @@ pub enum FilterType {
8181 Filter ,
8282 SQL ,
8383 Search ,
84+ Promql ,
8485}
8586
8687impl FilterType {
@@ -89,6 +90,7 @@ impl FilterType {
8990 FilterType :: Filter => "filter" ,
9091 FilterType :: SQL => "sql" ,
9192 FilterType :: Search => "search" ,
93+ FilterType :: Promql => "promql" ,
9294 }
9395 }
9496}
@@ -190,23 +192,26 @@ impl Filters {
190192 let query: & str = f. query . filter_query . as_deref ( ) . unwrap_or ( "" ) ;
191193 let filter_type = & f. query . filter_type ;
192194
193- // if filter type is SQL, check if the user has access to the dataset based on the query string
194- // if filter type is search or filter, check if the user has access to the dataset based on the dataset name
195- if * filter_type == FilterType :: SQL {
196- if ( user_auth_for_query ( key, query) . await ) . is_ok ( ) {
197- _filters. push ( f. clone ( ) )
195+ match filter_type {
196+ // SQL queries can reference multiple datasets, so authorize the query itself.
197+ FilterType :: SQL => {
198+ if ( user_auth_for_query ( key, query) . await ) . is_ok ( ) {
199+ _filters. push ( f. clone ( ) )
200+ }
198201 }
199- } else if * filter_type == FilterType :: Search || * filter_type == FilterType :: Filter {
200- let dataset_name = & f. stream_name ;
201- if user_auth_for_datasets (
202- & permissions,
203- std:: slice:: from_ref ( dataset_name) ,
204- & tenant_id,
205- )
206- . await
207- . is_ok ( )
208- {
209- _filters. push ( f. clone ( ) )
202+ // Other filter types are scoped by the saved filter's dataset.
203+ FilterType :: Search | FilterType :: Filter | FilterType :: Promql => {
204+ let dataset_name = & f. stream_name ;
205+ if user_auth_for_datasets (
206+ & permissions,
207+ std:: slice:: from_ref ( dataset_name) ,
208+ & tenant_id,
209+ )
210+ . await
211+ . is_ok ( )
212+ {
213+ _filters. push ( f. clone ( ) )
214+ }
210215 }
211216 }
212217 }
@@ -247,6 +252,63 @@ mod tests {
247252 } ) ) . unwrap ( )
248253 }
249254
255+ fn promql_filter ( stream_name : & str ) -> Filter {
256+ serde_json:: from_value ( serde_json:: json!( {
257+ "version" : "v2" , "user_id" : "owner" , "filter_id" : format!( "promql-{stream_name}" ) ,
258+ "stream_name" : stream_name, "filter_name" : "Request rate" , "tenant_id" : null,
259+ "query" : { "filter_type" : "promql" , "filter_query" : "sum(rate(http_requests_total[5m]))" , "filter_builder" : null} ,
260+ "time_filter" : null
261+ } ) ) . unwrap ( )
262+ }
263+
264+ // `rbac::map::init` needs `PARSEABLE`, so tests create the maps directly.
265+ // A user without groups never reads `ROLES`, which other tests seed.
266+ fn session_with ( permissions : Vec < crate :: rbac:: role:: Permission > ) -> SessionKey {
267+ use crate :: rbac:: map:: { SESSIONS , Sessions , USER_GROUPS , USERS , UserGroups } ;
268+ USERS . get_or_init ( || parking_lot:: RwLock :: new ( crate :: rbac:: map:: Users :: default ( ) ) ) ;
269+ SESSIONS . get_or_init ( || parking_lot:: RwLock :: new ( Sessions :: default ( ) ) ) ;
270+ USER_GROUPS . get_or_init ( || parking_lot:: RwLock :: new ( UserGroups :: default ( ) ) ) ;
271+ let key = SessionKey :: SessionId ( ulid:: Ulid :: new ( ) ) ;
272+ crate :: rbac:: map:: mut_sessions ( ) . track_new (
273+ format ! ( "user-{key:?}" ) ,
274+ key. clone ( ) ,
275+ chrono:: DateTime :: < chrono:: Utc > :: MAX_UTC ,
276+ permissions,
277+ & None ,
278+ ) ;
279+ key
280+ }
281+
282+ #[ test]
283+ fn promql_filter_type_round_trips_as_lowercase ( ) {
284+ let filter = promql_filter ( "otel_metrics" ) ;
285+ assert_eq ! ( filter. query. filter_type, FilterType :: Promql ) ;
286+ assert_eq ! ( filter. query. filter_type. to_str( ) , "promql" ) ;
287+ let value = serde_json:: to_value ( & filter) . unwrap ( ) ;
288+ assert_eq ! ( value[ "query" ] [ "filter_type" ] , "promql" ) ;
289+ assert_eq ! ( serde_json:: from_value:: <Filter >( value) . unwrap( ) , filter) ;
290+ }
291+
292+ #[ tokio:: test]
293+ async fn promql_filters_are_authorized_by_dataset_not_parsed_as_sql ( ) {
294+ use crate :: rbac:: role:: { Action , Permission } ;
295+ let cache = Filters :: default ( ) ;
296+ let filter = promql_filter ( "otel_metrics" ) ;
297+ cache. update ( & filter, & None ) . await ;
298+
299+ // PromQL is not SQL: routing it through SQL table extraction would
300+ // fail and silently hide the filter from authorized users.
301+ let reader = session_with ( vec ! [ Permission :: Resource ( Action :: Query , None ) ] ) ;
302+ assert_eq ! ( cache. list_filters( & reader) . await , vec![ filter] ) ;
303+
304+ let unauthorized = session_with ( vec ! [ ] ) ;
305+ assert ! ( cache. list_filters( & unauthorized) . await . is_empty( ) ) ;
306+
307+ for key in [ reader, unauthorized] {
308+ crate :: rbac:: map:: mut_sessions ( ) . remove_session ( & key) ;
309+ }
310+ }
311+
250312 #[ tokio:: test]
251313 async fn saved_filter_cache_prefers_administrators_own_copy_of_shared_id ( ) {
252314 let cache = Filters :: default ( ) ;
0 commit comments