Skip to main content

auth/
permission.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::fmt::Debug;
16use std::sync::Arc;
17
18use api::v1::RowInsertRequests;
19use api::v1::greptime_request::Request;
20use api::v1::query_request::Query;
21use common_telemetry::debug;
22use sql::statements::statement::Statement;
23
24use crate::error::{PermissionDeniedSnafu, Result};
25use crate::user_info::DefaultUserInfo;
26use crate::{PermissionCheckerRef, UserInfo, UserInfoRef};
27
28/// A user-visible table target for permission checks.
29///
30/// Use [`PermissionTableTargets::resolved`] to validate that all components are
31/// non-empty before passing targets to a permission checker.
32#[derive(Debug, Clone, PartialEq, Eq, Hash)]
33pub struct PermissionTableTarget {
34    pub catalog: String,
35    pub schema: String,
36    pub table: String,
37    /// Set from catalog metadata, never from a client-supplied table name.
38    /// Physical metric tables contain data belonging to multiple logical tables.
39    pub is_physical_metric_table: bool,
40}
41
42impl PermissionTableTarget {
43    /// Creates a table target candidate.
44    pub fn new(
45        catalog: impl Into<String>,
46        schema: impl Into<String>,
47        table: impl Into<String>,
48    ) -> Self {
49        Self {
50            catalog: catalog.into(),
51            schema: schema.into(),
52            table: table.into(),
53            is_physical_metric_table: false,
54        }
55    }
56
57    /// Sets whether catalog metadata identifies this target as a physical metric table.
58    pub fn with_physical_metric_table(mut self, is_physical_metric_table: bool) -> Self {
59        self.is_physical_metric_table = is_physical_metric_table;
60        self
61    }
62}
63
64/// The result of resolving every table target in a permission request.
65///
66/// `Resolved(vec![])` means the request contains no table targets. The operation
67/// privilege must still be checked, but there are no table ACLs to evaluate.
68/// [`PermissionTableTargets::Unresolved`] means the targets could not be
69/// determined safely.
70#[derive(Debug, Clone, PartialEq, Eq)]
71pub enum PermissionTableTargets {
72    /// Every target was resolved, including the valid empty-target case.
73    Resolved(Vec<PermissionTableTarget>),
74    /// One or more targets could not be determined safely.
75    Unresolved,
76}
77
78impl PermissionTableTargets {
79    /// Marks targets as resolved only when every component is non-empty.
80    pub fn resolved(targets: Vec<PermissionTableTarget>) -> Self {
81        if targets.iter().any(|target| {
82            target.catalog.is_empty() || target.schema.is_empty() || target.table.is_empty()
83        }) {
84            Self::Unresolved
85        } else {
86            Self::Resolved(targets)
87        }
88    }
89
90    /// Resolves logical table targets from normalized row insert requests.
91    pub fn from_row_insert_requests(
92        catalog: &str,
93        schema: &str,
94        requests: &RowInsertRequests,
95    ) -> Self {
96        Self::resolved(
97            requests
98                .inserts
99                .iter()
100                .map(|request| PermissionTableTarget::new(catalog, schema, &request.table_name))
101                .collect(),
102        )
103    }
104}
105
106/// The access mode of a named permission action.
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub enum AccessMode {
109    Read,
110    Write,
111}
112
113/// A named permission action.
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub struct PermissionAction {
116    name: &'static str,
117    access_mode: AccessMode,
118}
119
120impl PermissionAction {
121    /// Creates a read action with a stable name.
122    pub const fn read(name: &'static str) -> Self {
123        Self {
124            name,
125            access_mode: AccessMode::Read,
126        }
127    }
128
129    /// Creates a write action with a stable name.
130    pub const fn write(name: &'static str) -> Self {
131        Self {
132            name,
133            access_mode: AccessMode::Write,
134        }
135    }
136
137    /// Returns the stable action name.
138    pub const fn name(self) -> &'static str {
139        self.name
140    }
141
142    /// Returns the action's access mode.
143    pub const fn access_mode(self) -> AccessMode {
144        self.access_mode
145    }
146}
147
148pub const PROMQL_QUERY: PermissionAction = PermissionAction::read("promql.query");
149pub const LOG_QUERY: PermissionAction = PermissionAction::read("log.query");
150pub const OPENTSDB_WRITE: PermissionAction = PermissionAction::write("opentsdb.write");
151pub const INFLUXDB_WRITE: PermissionAction = PermissionAction::write("influxdb.write");
152pub const PROM_STORE_WRITE: PermissionAction = PermissionAction::write("prom_store.write");
153pub const PROM_STORE_READ: PermissionAction = PermissionAction::read("prom_store.read");
154pub const OTLP_WRITE: PermissionAction = PermissionAction::write("otlp.write");
155pub const LOG_WRITE: PermissionAction = PermissionAction::write("log.write");
156pub const JAEGER_QUERY: PermissionAction = PermissionAction::read("jaeger.query");
157pub const PIPELINE_QUERY: PermissionAction = PermissionAction::read("pipeline.query");
158pub const PIPELINE_INSERT: PermissionAction = PermissionAction::write("pipeline.insert");
159pub const PIPELINE_DELETE: PermissionAction = PermissionAction::write("pipeline.delete");
160pub const DASHBOARD_QUERY: PermissionAction = PermissionAction::read("dashboard.query");
161pub const DASHBOARD_SAVE: PermissionAction = PermissionAction::write("dashboard.save");
162pub const DASHBOARD_DELETE: PermissionAction = PermissionAction::write("dashboard.delete");
163/// Reading a telemetry table through the entity-graph derivation
164/// (`greptime_private.semantic_entities` / `semantic_relationships`); checked
165/// once per candidate source table with that table as the target.
166pub const SEMANTIC_GRAPH_QUERY: PermissionAction = PermissionAction::read("semantic_graph.query");
167
168/// All permission actions built into GreptimeDB.
169///
170/// Downstream crates may define additional actions with [`PermissionAction::read`] and
171/// [`PermissionAction::write`].
172pub const ALL_ACTIONS: &[PermissionAction] = &[
173    PROMQL_QUERY,
174    LOG_QUERY,
175    OPENTSDB_WRITE,
176    INFLUXDB_WRITE,
177    PROM_STORE_WRITE,
178    PROM_STORE_READ,
179    OTLP_WRITE,
180    LOG_WRITE,
181    JAEGER_QUERY,
182    PIPELINE_QUERY,
183    PIPELINE_INSERT,
184    PIPELINE_DELETE,
185    DASHBOARD_QUERY,
186    DASHBOARD_SAVE,
187    DASHBOARD_DELETE,
188    SEMANTIC_GRAPH_QUERY,
189];
190
191#[derive(Debug, Clone)]
192pub enum PermissionReq<'a> {
193    GrpcRequest(&'a Request),
194    SqlStatement(&'a Statement),
195    Action(PermissionAction),
196    /// Enumerates a schema's metadata without accessing any table data.
197    SchemaRead {
198        catalog: &'a str,
199        schema: &'a str,
200    },
201    BulkInsert {
202        catalog: &'a str,
203        schema: &'a str,
204        table: &'a str,
205    },
206}
207
208impl<'a> PermissionReq<'a> {
209    /// Returns true if the permission request is for read operations.
210    pub fn is_readonly(&self) -> bool {
211        match self {
212            PermissionReq::GrpcRequest(Request::Query(query_request)) => {
213                !matches!(query_request.query, Some(Query::InsertIntoPlan(_)))
214            }
215            PermissionReq::SqlStatement(stmt) => stmt.is_readonly(),
216            PermissionReq::Action(action) => action.access_mode() == AccessMode::Read,
217            PermissionReq::SchemaRead { .. } => true,
218
219            PermissionReq::GrpcRequest(_) | PermissionReq::BulkInsert { .. } => false,
220        }
221    }
222
223    /// Returns true if the permission request is for write operations.
224    pub fn is_write(&self) -> bool {
225        !self.is_readonly()
226    }
227}
228
229#[derive(Debug)]
230pub enum PermissionResp {
231    Allow,
232    Reject,
233}
234
235pub trait PermissionChecker: Send + Sync {
236    fn check_permission(
237        &self,
238        user_info: UserInfoRef,
239        req: PermissionReq,
240    ) -> Result<PermissionResp>;
241
242    fn check_permission_with_context(
243        &self,
244        user_info: UserInfoRef,
245        req: PermissionReq,
246        _current_schema: Option<&str>,
247    ) -> Result<PermissionResp> {
248        self.check_permission(user_info, req)
249    }
250
251    /// Returns whether authorization depends on resolved table targets.
252    ///
253    /// The conservative default keeps target resolution enabled. Implementations
254    /// may return `false` only when their authorization result is independent of
255    /// the `targets` passed to [`Self::check_permission_with_table_targets`].
256    fn uses_table_targets(&self) -> bool {
257        true
258    }
259
260    /// Extracts user-visible gRPC targets before the frontend classifies them
261    /// using catalog metadata. Unrecognized requests must fail closed.
262    fn grpc_table_targets(
263        &self,
264        _request: &Request,
265        _catalog: &str,
266        _schema: &str,
267    ) -> PermissionTableTargets {
268        PermissionTableTargets::Unresolved
269    }
270
271    /// Checks the operation privilege and its resolved user-visible table targets.
272    ///
273    /// [`PermissionTableTargets::Resolved`] with an empty vector still requires
274    /// checking the operation privilege in `req`, but has no table ACLs to check.
275    ///
276    /// ACL-aware implementations must apply any admin bypass first, then reject
277    /// [`PermissionTableTargets::Unresolved`] for non-admin users.
278    fn check_permission_with_table_targets(
279        &self,
280        user_info: UserInfoRef,
281        req: PermissionReq,
282        targets: PermissionTableTargets,
283    ) -> Result<PermissionResp>;
284}
285
286fn check_permission_result(result: Result<PermissionResp>) -> Result<PermissionResp> {
287    match result {
288        Ok(PermissionResp::Reject) => PermissionDeniedSnafu.fail(),
289        Ok(PermissionResp::Allow) => Ok(PermissionResp::Allow),
290        Err(e) => Err(e),
291    }
292}
293
294impl PermissionChecker for Option<&PermissionCheckerRef> {
295    fn check_permission(
296        &self,
297        user_info: UserInfoRef,
298        req: PermissionReq,
299    ) -> Result<PermissionResp> {
300        self.check_permission_with_context(user_info, req, None)
301    }
302
303    fn check_permission_with_context(
304        &self,
305        user_info: UserInfoRef,
306        req: PermissionReq,
307        current_schema: Option<&str>,
308    ) -> Result<PermissionResp> {
309        match self {
310            Some(checker) => check_permission_result(checker.check_permission_with_context(
311                user_info,
312                req,
313                current_schema,
314            )),
315            None => Ok(PermissionResp::Allow),
316        }
317    }
318
319    fn uses_table_targets(&self) -> bool {
320        match self {
321            Some(checker) => checker.uses_table_targets(),
322            None => false,
323        }
324    }
325
326    fn grpc_table_targets(
327        &self,
328        request: &Request,
329        catalog: &str,
330        schema: &str,
331    ) -> PermissionTableTargets {
332        match self {
333            Some(checker) => checker.grpc_table_targets(request, catalog, schema),
334            None => PermissionTableTargets::Unresolved,
335        }
336    }
337
338    fn check_permission_with_table_targets(
339        &self,
340        user_info: UserInfoRef,
341        req: PermissionReq,
342        targets: PermissionTableTargets,
343    ) -> Result<PermissionResp> {
344        match self {
345            Some(checker) => check_permission_result(
346                checker.check_permission_with_table_targets(user_info, req, targets),
347            ),
348            None => Ok(PermissionResp::Allow),
349        }
350    }
351}
352
353/// The default permission checker implementation.
354/// It checks the permission mode of [DefaultUserInfo].
355pub struct DefaultPermissionChecker;
356
357impl DefaultPermissionChecker {
358    /// Returns a new [PermissionCheckerRef] instance.
359    pub fn arc() -> PermissionCheckerRef {
360        Arc::new(DefaultPermissionChecker)
361    }
362}
363
364impl PermissionChecker for DefaultPermissionChecker {
365    fn check_permission(
366        &self,
367        user_info: UserInfoRef,
368        req: PermissionReq,
369    ) -> Result<PermissionResp> {
370        if let Some(default_user) = user_info.as_any().downcast_ref::<DefaultUserInfo>() {
371            let permission_mode = default_user.permission_mode();
372
373            if req.is_readonly() && !permission_mode.can_read() {
374                debug!(
375                    "Permission denied: read operation not allowed, user = {}, permission = {}",
376                    default_user.username(),
377                    permission_mode.as_str()
378                );
379                return Ok(PermissionResp::Reject);
380            }
381
382            if req.is_write() && !permission_mode.can_write() {
383                debug!(
384                    "Permission denied: write operation not allowed, user = {}, permission = {}",
385                    default_user.username(),
386                    permission_mode.as_str()
387                );
388                return Ok(PermissionResp::Reject);
389            }
390        }
391
392        // default allow all
393        Ok(PermissionResp::Allow)
394    }
395
396    fn uses_table_targets(&self) -> bool {
397        false
398    }
399
400    fn check_permission_with_table_targets(
401        &self,
402        user_info: UserInfoRef,
403        req: PermissionReq,
404        _targets: PermissionTableTargets,
405    ) -> Result<PermissionResp> {
406        self.check_permission(user_info, req)
407    }
408}
409#[cfg(test)]
410mod tests {
411    use std::collections::HashSet;
412
413    use super::*;
414    use crate::error::{Error, InternalStateSnafu};
415    use crate::user_info::PermissionMode;
416
417    struct TargetAwarePermissionChecker;
418
419    impl PermissionChecker for TargetAwarePermissionChecker {
420        fn check_permission(
421            &self,
422            _user_info: UserInfoRef,
423            _req: PermissionReq,
424        ) -> Result<PermissionResp> {
425            Ok(PermissionResp::Reject)
426        }
427
428        fn grpc_table_targets(
429            &self,
430            request: &Request,
431            catalog: &str,
432            schema: &str,
433        ) -> PermissionTableTargets {
434            match request {
435                Request::RowInserts(requests) => {
436                    PermissionTableTargets::from_row_insert_requests(catalog, schema, requests)
437                }
438                _ => PermissionTableTargets::Unresolved,
439            }
440        }
441
442        fn check_permission_with_table_targets(
443            &self,
444            _user_info: UserInfoRef,
445            req: PermissionReq,
446            targets: PermissionTableTargets,
447        ) -> Result<PermissionResp> {
448            if !matches!(req, PermissionReq::Action(PROM_STORE_READ)) {
449                return Ok(PermissionResp::Reject);
450            }
451            let PermissionTableTargets::Resolved(targets) = targets else {
452                return Ok(PermissionResp::Reject);
453            };
454            if targets.iter().any(|target| target.table == "error") {
455                return InternalStateSnafu {
456                    msg: "testing".to_string(),
457                }
458                .fail();
459            }
460            Ok(if targets.iter().all(|target| target.table == "allowed") {
461                PermissionResp::Allow
462            } else {
463                PermissionResp::Reject
464            })
465        }
466    }
467
468    fn resolved_targets(table: &str) -> PermissionTableTargets {
469        PermissionTableTargets::resolved(vec![PermissionTableTarget::new(
470            "greptime", "public", table,
471        )])
472    }
473
474    #[test]
475    fn test_resolve_permission_table_targets() {
476        assert_eq!(
477            PermissionTableTargets::Resolved(Vec::new()),
478            PermissionTableTargets::resolved(Vec::new())
479        );
480        assert_eq!(
481            resolved_targets("metrics"),
482            PermissionTableTargets::Resolved(vec![PermissionTableTarget::new(
483                "greptime", "public", "metrics"
484            )])
485        );
486
487        for target in [
488            PermissionTableTarget::new("", "public", "metrics"),
489            PermissionTableTarget::new("greptime", "", "metrics"),
490            PermissionTableTarget::new("greptime", "public", ""),
491        ] {
492            assert_eq!(
493                PermissionTableTargets::Unresolved,
494                PermissionTableTargets::resolved(vec![target])
495            );
496        }
497    }
498
499    #[test]
500    fn test_resolve_row_insert_request_targets() {
501        let requests = RowInsertRequests {
502            inserts: ["cpu", "mem"]
503                .into_iter()
504                .map(|table_name| api::v1::RowInsertRequest {
505                    table_name: table_name.to_string(),
506                    ..Default::default()
507                })
508                .collect(),
509        };
510
511        assert_eq!(
512            PermissionTableTargets::Resolved(vec![
513                PermissionTableTarget::new("greptime", "public", "cpu"),
514                PermissionTableTarget::new("greptime", "public", "mem"),
515            ]),
516            PermissionTableTargets::from_row_insert_requests("greptime", "public", &requests)
517        );
518        assert_eq!(
519            PermissionTableTargets::Unresolved,
520            PermissionTableTargets::from_row_insert_requests("", "public", &requests)
521        );
522        assert_eq!(
523            PermissionTableTargets::Unresolved,
524            PermissionTableTargets::from_row_insert_requests("greptime", "", &requests)
525        );
526
527        let unresolved = RowInsertRequests {
528            inserts: vec![api::v1::RowInsertRequest::default()],
529        };
530        assert_eq!(
531            PermissionTableTargets::Unresolved,
532            PermissionTableTargets::from_row_insert_requests("greptime", "public", &unresolved)
533        );
534
535        let empty = RowInsertRequests::default();
536        assert_eq!(
537            PermissionTableTargets::Resolved(Vec::new()),
538            PermissionTableTargets::from_row_insert_requests("greptime", "public", &empty)
539        );
540    }
541
542    #[test]
543    fn test_default_permission_checker_allow_all_operations() {
544        let checker = DefaultPermissionChecker;
545        let user_info =
546            DefaultUserInfo::with_name_and_permission("test_user", PermissionMode::ReadWrite);
547
548        let read_req = PermissionReq::Action(PROMQL_QUERY);
549        let write_req = PermissionReq::Action(PROM_STORE_WRITE);
550
551        let read_result = checker
552            .check_permission(user_info.clone(), read_req)
553            .unwrap();
554        let write_result = checker.check_permission(user_info, write_req).unwrap();
555
556        assert!(matches!(read_result, PermissionResp::Allow));
557        assert!(matches!(write_result, PermissionResp::Allow));
558    }
559
560    #[test]
561    fn test_default_permission_checker_readonly_user() {
562        let checker = DefaultPermissionChecker;
563        let user_info =
564            DefaultUserInfo::with_name_and_permission("readonly_user", PermissionMode::ReadOnly);
565
566        let read_req = PermissionReq::Action(PROMQL_QUERY);
567        let write_req = PermissionReq::Action(PROM_STORE_WRITE);
568
569        let read_result = checker
570            .check_permission(user_info.clone(), read_req)
571            .unwrap();
572        let write_result = checker.check_permission(user_info, write_req).unwrap();
573
574        assert!(matches!(read_result, PermissionResp::Allow));
575        assert!(matches!(write_result, PermissionResp::Reject));
576    }
577
578    #[test]
579    fn test_default_permission_checker_writeonly_user() {
580        let checker = DefaultPermissionChecker;
581        let user_info =
582            DefaultUserInfo::with_name_and_permission("writeonly_user", PermissionMode::WriteOnly);
583
584        let read_req = PermissionReq::Action(LOG_QUERY);
585        let write_req = PermissionReq::Action(LOG_WRITE);
586
587        let read_result = checker
588            .check_permission(user_info.clone(), read_req)
589            .unwrap();
590        let write_result = checker.check_permission(user_info, write_req).unwrap();
591
592        assert!(matches!(read_result, PermissionResp::Reject));
593        assert!(matches!(write_result, PermissionResp::Allow));
594    }
595
596    #[test]
597    fn test_grpc_insert_into_plan_is_write_request() {
598        let request = Request::Query(api::v1::QueryRequest {
599            query: Some(Query::InsertIntoPlan(api::v1::InsertIntoPlan::default())),
600        });
601        let req = PermissionReq::GrpcRequest(&request);
602
603        assert!(req.is_write());
604    }
605
606    #[test]
607    fn test_bulk_insert_is_write_request() {
608        let req = PermissionReq::BulkInsert {
609            catalog: "greptime",
610            schema: "public",
611            table: "metrics",
612        };
613
614        assert!(req.is_write());
615    }
616
617    #[test]
618    fn test_action_access_modes() {
619        let checker = DefaultPermissionChecker;
620        let mut names = HashSet::new();
621
622        for &action in ALL_ACTIONS {
623            assert!(
624                names.insert(action.name()),
625                "duplicate permission action: {}",
626                action.name()
627            );
628            let access_mode = action.access_mode();
629            let req = PermissionReq::Action(action);
630            assert_eq!(
631                access_mode == AccessMode::Read,
632                req.is_readonly(),
633                "{}",
634                action.name()
635            );
636
637            for (permission_mode, allowed) in [
638                (PermissionMode::ReadOnly, access_mode == AccessMode::Read),
639                (PermissionMode::WriteOnly, access_mode == AccessMode::Write),
640                (PermissionMode::ReadWrite, true),
641            ] {
642                let user = DefaultUserInfo::with_name_and_permission("test_user", permission_mode);
643                let result = checker.check_permission(user, req.clone()).unwrap();
644                assert_eq!(
645                    allowed,
646                    matches!(result, PermissionResp::Allow),
647                    "{permission_mode:?}: {}",
648                    action.name()
649                );
650            }
651        }
652    }
653
654    #[test]
655    fn test_table_target_permission_forwarding() {
656        let inner_checker: PermissionCheckerRef = Arc::new(TargetAwarePermissionChecker);
657        let checker = Some(&inner_checker);
658        assert!(checker.uses_table_targets());
659
660        let allowed = checker
661            .check_permission_with_table_targets(
662                crate::userinfo_by_name(None),
663                PermissionReq::Action(PROM_STORE_READ),
664                resolved_targets("allowed"),
665            )
666            .unwrap();
667        assert!(matches!(allowed, PermissionResp::Allow));
668
669        let empty = checker
670            .check_permission_with_table_targets(
671                crate::userinfo_by_name(None),
672                PermissionReq::Action(PROM_STORE_READ),
673                PermissionTableTargets::resolved(Vec::new()),
674            )
675            .unwrap();
676        assert!(matches!(empty, PermissionResp::Allow));
677
678        let rejected_operation = checker.check_permission_with_table_targets(
679            crate::userinfo_by_name(None),
680            PermissionReq::Action(PROM_STORE_WRITE),
681            PermissionTableTargets::resolved(Vec::new()),
682        );
683        assert!(matches!(
684            rejected_operation,
685            Err(Error::PermissionDenied { .. })
686        ));
687
688        let rejected = checker.check_permission_with_table_targets(
689            crate::userinfo_by_name(None),
690            PermissionReq::Action(PROM_STORE_READ),
691            resolved_targets("denied"),
692        );
693        assert!(matches!(rejected, Err(Error::PermissionDenied { .. })));
694
695        let mixed = checker.check_permission_with_table_targets(
696            crate::userinfo_by_name(None),
697            PermissionReq::Action(PROM_STORE_READ),
698            PermissionTableTargets::resolved(vec![
699                PermissionTableTarget::new("greptime", "public", "allowed"),
700                PermissionTableTarget::new("greptime", "public", "denied"),
701            ]),
702        );
703        assert!(matches!(mixed, Err(Error::PermissionDenied { .. })));
704
705        let error = checker.check_permission_with_table_targets(
706            crate::userinfo_by_name(None),
707            PermissionReq::Action(PROM_STORE_READ),
708            resolved_targets("error"),
709        );
710        assert!(matches!(error, Err(Error::InternalState { msg }) if msg == "testing"));
711
712        let no_checker: Option<&PermissionCheckerRef> = None;
713        assert!(!no_checker.uses_table_targets());
714        for request in [
715            Request::RowInserts(RowInsertRequests {
716                inserts: vec![api::v1::RowInsertRequest {
717                    table_name: "allowed".to_string(),
718                    ..Default::default()
719                }],
720            }),
721            Request::RowInserts(RowInsertRequests::default()),
722            Request::Query(api::v1::QueryRequest::default()),
723        ] {
724            assert_eq!(
725                inner_checker.grpc_table_targets(&request, "catalog", "schema"),
726                checker.grpc_table_targets(&request, "catalog", "schema"),
727            );
728            assert_eq!(
729                PermissionTableTargets::Unresolved,
730                no_checker.grpc_table_targets(&request, "catalog", "schema"),
731            );
732        }
733        let allowed = no_checker
734            .check_permission_with_table_targets(
735                crate::userinfo_by_name(None),
736                PermissionReq::Action(PROM_STORE_READ),
737                PermissionTableTargets::Unresolved,
738            )
739            .unwrap();
740        assert!(matches!(allowed, PermissionResp::Allow));
741    }
742
743    #[test]
744    fn test_default_permission_checker_table_target_parity() {
745        let checker = DefaultPermissionChecker;
746        assert!(!checker.uses_table_targets());
747
748        for (permission, req) in [
749            (
750                PermissionMode::ReadOnly,
751                PermissionReq::Action(PROMQL_QUERY),
752            ),
753            (
754                PermissionMode::ReadOnly,
755                PermissionReq::Action(PROM_STORE_WRITE),
756            ),
757            (
758                PermissionMode::WriteOnly,
759                PermissionReq::Action(PROMQL_QUERY),
760            ),
761            (
762                PermissionMode::WriteOnly,
763                PermissionReq::Action(PROM_STORE_WRITE),
764            ),
765        ] {
766            let user = DefaultUserInfo::with_name_and_permission("test_user", permission);
767            let direct = checker.check_permission(user.clone(), req.clone()).unwrap();
768            let targeted = checker
769                .check_permission_with_table_targets(user, req, resolved_targets("metrics"))
770                .unwrap();
771
772            assert_eq!(
773                matches!(direct, PermissionResp::Allow),
774                matches!(targeted, PermissionResp::Allow)
775            );
776        }
777    }
778}