Skip to main content

mz_sql/
rbac.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10use std::collections::{BTreeMap, BTreeSet, VecDeque};
11use std::iter;
12use std::sync::LazyLock;
13
14use itertools::Itertools;
15use maplit::btreeset;
16use mz_controller_types::ClusterId;
17use mz_expr::CollectionPlan;
18use mz_ore::str::StrExt;
19use mz_repr::CatalogItemId;
20use mz_repr::adt::mz_acl_item::{AclMode, MzAclItem};
21use mz_repr::role_id::RoleId;
22use mz_sql_parser::ast::{Ident, QualifiedReplica};
23use tracing::debug;
24
25use crate::catalog::{
26    CatalogItemType, ErrorMessageObjectDescription, ObjectType, SessionCatalog, SystemObjectType,
27};
28use crate::names::{
29    CommentObjectId, ObjectId, QualifiedItemName, ResolvedDatabaseSpecifier, ResolvedIds,
30    SchemaSpecifier, SystemObjectId,
31};
32use crate::plan::{self, PlanKind};
33use crate::plan::{
34    DataSourceDesc, Explainee, MutationKind, Plan, SideEffectingFunc, UpdatePrivilege,
35};
36use crate::session::metadata::SessionMetadata;
37use crate::session::user::{MZ_SUPPORT_ROLE_ID, MZ_SYSTEM_ROLE_ID, SUPPORT_USER, SYSTEM_USER};
38use crate::session::vars::SystemVars;
39
40/// Common checks that need to be performed before we can start checking a role's privileges.
41fn rbac_check_preamble(
42    catalog: &impl SessionCatalog,
43    session_meta: &dyn SessionMetadata,
44) -> Result<(), UnauthorizedError> {
45    // PostgreSQL allows users that have their role dropped to perform some actions,
46    // such as `SET ROLE` and certain `SELECT` queries. We haven't implemented
47    // `SET ROLE` and feel it's safer to force to user to re-authenticate if their
48    // role is dropped.
49    if catalog
50        .try_get_role(&session_meta.role_metadata().current_role)
51        .is_none()
52    {
53        return Err(UnauthorizedError::ConcurrentRoleDrop(
54            session_meta.role_metadata().current_role.clone(),
55        ));
56    };
57    if catalog
58        .try_get_role(&session_meta.role_metadata().session_role)
59        .is_none()
60    {
61        return Err(UnauthorizedError::ConcurrentRoleDrop(
62            session_meta.role_metadata().session_role.clone(),
63        ));
64    };
65    if catalog
66        .try_get_role(&session_meta.role_metadata().authenticated_role)
67        .is_none()
68    {
69        return Err(UnauthorizedError::ConcurrentRoleDrop(
70            session_meta.role_metadata().authenticated_role.clone(),
71        ));
72    };
73
74    Ok(())
75}
76
77/// Filters `RbacRequirements` based on the session role metadata and RBAC related feature flags.
78fn filter_requirements(
79    catalog: &impl SessionCatalog,
80    session_meta: &dyn SessionMetadata,
81    rbac_requirements: RbacRequirements,
82) -> RbacRequirements {
83    // Skip RBAC non-mandatory checks if RBAC is disabled. However, we never skip RBAC checks for
84    // system roles. This allows us to limit access of system users even when RBAC is off.
85    let is_rbac_disabled = !is_rbac_enabled_for_session(catalog.system_vars(), session_meta)
86        && !session_meta.role_metadata().current_role.is_system()
87        && !session_meta.role_metadata().session_role.is_system();
88    // Skip RBAC checks on user items if the session is a superuser.
89    let is_superuser = session_meta.is_superuser();
90    if is_rbac_disabled || is_superuser {
91        return rbac_requirements.filter_to_mandatory_requirements();
92    }
93
94    rbac_requirements
95}
96
97// The default item types that most statements require USAGE privileges for.
98static DEFAULT_ITEM_USAGE: LazyLock<BTreeSet<CatalogItemType>> = LazyLock::new(|| {
99    btreeset! {CatalogItemType::Secret, CatalogItemType::Connection}
100});
101// CREATE statements require USAGE privileges on the default item types and USAGE privileges on
102// Types.
103pub static CREATE_ITEM_USAGE: LazyLock<BTreeSet<CatalogItemType>> = LazyLock::new(|| {
104    let mut items = DEFAULT_ITEM_USAGE.clone();
105    items.insert(CatalogItemType::Type);
106    items
107});
108pub static EMPTY_ITEM_USAGE: LazyLock<BTreeSet<CatalogItemType>> = LazyLock::new(BTreeSet::new);
109
110/// System catalog objects exempted from `check_restrict_to_user_objects`.
111///
112/// The `mz_mcp_data_product*` views are how the MCP agent endpoint
113/// discovers data products; blocking them defeats the isolation model.
114/// `mz_show_my_cluster_privileges` is referenced by those views to null the
115/// advertised cluster unless the role has USAGE on it (it uses
116/// `mz_session_role_memberships()` rather than a `has_cluster_privilege`
117/// body that referenced `mz_roles`), and is itself useful for a restricted
118/// session to inspect its own privileges.
119static RESTRICT_TO_USER_OBJECTS_ALLOWED_OIDS: LazyLock<BTreeSet<u32>> = LazyLock::new(|| {
120    use mz_pgrepr::oid;
121    btreeset! {
122        oid::VIEW_MZ_MCP_DATA_PRODUCTS_OID,
123        oid::VIEW_MZ_MCP_DATA_PRODUCT_DETAILS_OID,
124        oid::VIEW_MZ_SHOW_MY_CLUSTER_PRIVILEGES_OID,
125    }
126});
127
128/// Errors that can occur due to an unauthorized action.
129#[derive(Debug, thiserror::Error)]
130pub enum UnauthorizedError {
131    /// The action can only be performed by a superuser.
132    #[error("permission denied to {action}")]
133    Superuser { action: String },
134    /// The action requires ownership of an object.
135    #[error("must be owner of {}", objects.iter().map(|(object_type, object_name)| format!("{object_type} {object_name}")).join(", "))]
136    Ownership { objects: Vec<(ObjectType, String)> },
137    /// Altering an owner requires membership of the new owner role.
138    #[error("must be a member of {}", role_names.iter().map(|role| role.quoted()).join(", "))]
139    RoleMembership { role_names: Vec<String> },
140    /// The action requires one or more privileges.
141    #[error("permission denied for {object_description}")]
142    Privilege {
143        object_description: ErrorMessageObjectDescription,
144        role_name: String,
145        privileges: String,
146    },
147    // TODO(jkosh44) When we implement parameter privileges, this can be replaced with a regular
148    //  privilege error.
149    /// The action can only be performed by the mz_system role.
150    #[error("permission denied to {action}")]
151    MzSystem { action: String },
152    /// The action cannot be performed by the mz_support role.
153    #[error("permission denied to {action}")]
154    MzSupport { action: String },
155    /// The active role was dropped while a user was logged in.
156    #[error("role {0} was concurrently dropped")]
157    ConcurrentRoleDrop(RoleId),
158    /// Access to system objects is restricted by the restrict_to_user_objects session variable.
159    #[error("access to system object {object_name} is restricted")]
160    RestrictedSystemObject { object_name: String },
161}
162
163impl UnauthorizedError {
164    pub fn detail(&self) -> Option<String> {
165        match &self {
166            UnauthorizedError::Superuser { action } => {
167                Some(format!("You must be a superuser to {}", action))
168            }
169            UnauthorizedError::Privilege {
170                object_description,
171                role_name,
172                privileges,
173            } => Some(format!(
174                "The '{role_name}' role needs {privileges} privileges on {object_description}"
175            )),
176            UnauthorizedError::MzSystem { .. } => {
177                Some(format!("You must be the '{}' role", SYSTEM_USER.name))
178            }
179            UnauthorizedError::MzSupport { .. } => Some(format!(
180                "The '{}' role has very limited privileges",
181                SUPPORT_USER.name
182            )),
183            UnauthorizedError::ConcurrentRoleDrop(_) => {
184                Some("Please disconnect and re-connect with a valid role.".into())
185            }
186            UnauthorizedError::RestrictedSystemObject { .. } => Some(
187                "Access to system catalog objects is restricted for this role. \
188                Contact your administrator if you need access."
189                    .into(),
190            ),
191            UnauthorizedError::Ownership { .. } | UnauthorizedError::RoleMembership { .. } => None,
192        }
193    }
194}
195
196/// RBAC requirements for executing a given plan.
197#[derive(Debug)]
198struct RbacRequirements {
199    /// The role memberships required.
200    role_membership: BTreeSet<RoleId>,
201    /// The object ownerships required.
202    ownership: Vec<ObjectId>,
203    /// The privileges required. The tuples are of the form:
204    /// (What object the privilege is on, What privilege is required, Who must possess the privilege).
205    privileges: Vec<(SystemObjectId, AclMode, RoleId)>,
206    /// The types of catalog items that this plan requires USAGE privileges on.
207    ///
208    /// Most plans will require USAGE on secrets and connections but some plans, like SHOW CREATE,
209    /// can reference an item without requiring any privileges on that item.
210    item_usage: &'static BTreeSet<CatalogItemType>,
211    /// Some action if superuser is required to perform that action, None otherwise.
212    superuser_action: Option<String>,
213}
214
215impl RbacRequirements {
216    fn empty() -> RbacRequirements {
217        RbacRequirements {
218            role_membership: BTreeSet::new(),
219            ownership: Vec::new(),
220            privileges: Vec::new(),
221            item_usage: &EMPTY_ITEM_USAGE,
222            superuser_action: None,
223        }
224    }
225
226    fn validate(
227        self,
228        catalog: &impl SessionCatalog,
229        session: &dyn SessionMetadata,
230        resolved_ids: &ResolvedIds,
231    ) -> Result<(), UnauthorizedError> {
232        // Obtain all roles that the current session is a member of.
233        let role_membership =
234            catalog.collect_role_membership(&session.role_metadata().current_role);
235
236        check_usage(catalog, session, resolved_ids, self.item_usage)?;
237
238        // Validate that the current session has the required role membership to execute the provided
239        // plan.
240        let unheld_membership: Vec<_> = self.role_membership.difference(&role_membership).collect();
241        if !unheld_membership.is_empty() {
242            let role_names = unheld_membership
243                .into_iter()
244                .map(|role_id| {
245                    // Some role references may no longer exist due to concurrent drops.
246                    catalog
247                        .try_get_role(role_id)
248                        .map(|role| role.name().to_string())
249                        .unwrap_or_else(|| role_id.to_string())
250                })
251                .collect();
252            return Err(UnauthorizedError::RoleMembership { role_names });
253        }
254
255        // Validate that the current session has the required object ownership to execute the provided
256        // plan.
257        let unheld_ownership = self
258            .ownership
259            .into_iter()
260            .filter(|ownership| !check_owner_roles(ownership, &role_membership, catalog))
261            .collect();
262        ownership_err(unheld_ownership, catalog)?;
263
264        check_object_privileges(
265            catalog,
266            self.privileges,
267            role_membership,
268            session.role_metadata().current_role,
269        )?;
270
271        if let Some(action) = self.superuser_action {
272            return Err(UnauthorizedError::Superuser { action });
273        }
274
275        Ok(())
276    }
277
278    fn filter_to_mandatory_requirements(self) -> RbacRequirements {
279        let RbacRequirements {
280            role_membership,
281            ownership,
282            privileges,
283            item_usage,
284            superuser_action: _,
285        } = self;
286        let role_membership = role_membership
287            .into_iter()
288            .filter(|id| id.is_system())
289            .collect();
290        let ownership = ownership.into_iter().filter(|id| id.is_system()).collect();
291        let privileges = privileges
292            .into_iter()
293            .filter(|(id, _, _)| matches!(id, SystemObjectId::Object(oid) if oid.is_system()))
294            // We allow reading objects for superusers and when RBAC is off.
295            .update(|(_, acl_mode, _)| acl_mode.remove(AclMode::SELECT))
296            .filter(|(_, acl_mode, _)| !acl_mode.is_empty())
297            .collect();
298        let superuser_action = None;
299        RbacRequirements {
300            role_membership,
301            ownership,
302            privileges,
303            item_usage,
304            superuser_action,
305        }
306    }
307}
308
309impl Default for RbacRequirements {
310    fn default() -> Self {
311        RbacRequirements {
312            role_membership: BTreeSet::new(),
313            ownership: Vec::new(),
314            privileges: Vec::new(),
315            item_usage: &DEFAULT_ITEM_USAGE,
316            superuser_action: None,
317        }
318    }
319}
320
321/// When `restrict_to_user_objects` is active, rejects access to system catalog objects.
322///
323/// Functions and types are allowed through because they are needed for query execution.
324/// All other system items (tables, views, sources, sinks, etc.) are blocked. This is an
325/// allow-list — new catalog item types are blocked by default.
326///
327/// See: doc/developer/design/20260508_restrict_to_user_objects.md
328fn check_restrict_to_user_objects(
329    catalog: &impl SessionCatalog,
330    session: &dyn SessionMetadata,
331    resolved_ids: &ResolvedIds,
332) -> Result<(), UnauthorizedError> {
333    if !session.restrict_to_user_objects() {
334        return Ok(());
335    }
336    for item_id in resolved_ids.items() {
337        if item_id.is_system() {
338            if let Some(item) = catalog.try_get_item(item_id) {
339                match item.item_type() {
340                    CatalogItemType::Func | CatalogItemType::Type => {}
341                    _ => {
342                        if RESTRICT_TO_USER_OBJECTS_ALLOWED_OIDS.contains(&item.oid()) {
343                            continue;
344                        }
345                        return Err(UnauthorizedError::RestrictedSystemObject {
346                            object_name: item.name().item.clone(),
347                        });
348                    }
349                }
350            }
351        }
352    }
353    Ok(())
354}
355
356/// Checks if a `session` is authorized to use `resolved_ids`. If not, an error is returned.
357pub fn check_usage(
358    catalog: &impl SessionCatalog,
359    session: &dyn SessionMetadata,
360    resolved_ids: &ResolvedIds,
361    item_types: &BTreeSet<CatalogItemType>,
362) -> Result<(), UnauthorizedError> {
363    rbac_check_preamble(catalog, session)?;
364
365    // See: doc/developer/design/20260508_restrict_to_user_objects.md
366    check_restrict_to_user_objects(catalog, session, resolved_ids)?;
367
368    // Obtain all roles that the current session is a member of.
369    let role_membership = catalog.collect_role_membership(&session.role_metadata().current_role);
370
371    // Certain statements depend on objects that haven't been created yet, like sub-sources, so we
372    // need to filter those out.
373    let existing_resolved_ids =
374        resolved_ids.retain_items(|item_id| catalog.try_get_item(item_id).is_some());
375
376    let required_privileges = generate_usage_privileges(
377        catalog,
378        &existing_resolved_ids,
379        session.role_metadata().current_role,
380        item_types,
381    )
382    .into_iter()
383    .collect();
384
385    let mut rbac_requirements = RbacRequirements::empty();
386    rbac_requirements.privileges = required_privileges;
387    let rbac_requirements = filter_requirements(catalog, session, rbac_requirements);
388    let required_privileges = rbac_requirements.privileges;
389
390    check_object_privileges(
391        catalog,
392        required_privileges,
393        role_membership,
394        session.role_metadata().current_role,
395    )?;
396
397    Ok(())
398}
399
400/// Checks if a session is authorized to execute a plan. If not, an error is returned.
401///
402/// `sql_impl_resolved_ids` contains resolved IDs discovered inside SQL-implemented function
403/// bodies during planning. These are kept separate from `resolved_ids` because they are
404/// implementation details of the functions, not dependencies of the statement. They are
405/// only checked by the `restrict_to_user_objects` restriction.
406pub fn check_plan(
407    catalog: &impl SessionCatalog,
408    // The authenticated role of the connection targeted by the plan, if the plan is a
409    // Plan::SideEffectingFunc that targets an existing connection. The role may have been
410    // dropped concurrently. Ignored for other plan types.
411    target_conn_role: Option<RoleId>,
412    session: &dyn SessionMetadata,
413    plan: &Plan,
414    target_cluster_id: Option<ClusterId>,
415    resolved_ids: &ResolvedIds,
416    sql_impl_resolved_ids: &ResolvedIds,
417) -> Result<(), UnauthorizedError> {
418    rbac_check_preamble(catalog, session)?;
419
420    // Check sql_impl function body dependencies against restrict_to_user_objects.
421    // These are checked separately from the main resolved_ids because they are
422    // implementation details that should not affect dependency tracking.
423    check_restrict_to_user_objects(catalog, session, sql_impl_resolved_ids)?;
424
425    let rbac_requirements = generate_rbac_requirements(
426        catalog,
427        plan,
428        target_conn_role,
429        target_cluster_id,
430        session.role_metadata().current_role,
431    );
432    let rbac_requirements = filter_requirements(catalog, session, rbac_requirements);
433    debug!(
434        "rbac requirements {rbac_requirements:?} for plan {:?}",
435        PlanKind::from(plan)
436    );
437    rbac_requirements.validate(catalog, session, resolved_ids)
438}
439
440/// Returns true if RBAC is turned on for a session, false otherwise.
441pub fn is_rbac_enabled_for_session(
442    system_vars: &SystemVars,
443    session: &dyn SessionMetadata,
444) -> bool {
445    let server_enabled = system_vars.enable_rbac_checks();
446    let session_enabled = session.enable_session_rbac_checks();
447
448    // The session flag allows users to turn RBAC on for just their session while the server flag
449    // allows users to turn RBAC on for everyone.
450    server_enabled || session_enabled
451}
452
453/// Generates all requirements needed to execute a given plan.
454fn generate_rbac_requirements(
455    catalog: &impl SessionCatalog,
456    plan: &Plan,
457    target_conn_role: Option<RoleId>,
458    target_cluster_id: Option<ClusterId>,
459    role_id: RoleId,
460) -> RbacRequirements {
461    match plan {
462        Plan::CreateConnection(plan::CreateConnectionPlan {
463            name,
464            if_not_exists: _,
465            connection: _,
466            validate: _,
467        }) => RbacRequirements {
468            privileges: vec![(
469                SystemObjectId::Object(name.qualifiers.clone().into()),
470                AclMode::CREATE,
471                role_id,
472            )],
473            item_usage: &CREATE_ITEM_USAGE,
474            ..Default::default()
475        },
476        Plan::CreateDatabase(plan::CreateDatabasePlan {
477            name: _,
478            if_not_exists: _,
479        }) => RbacRequirements {
480            privileges: vec![(SystemObjectId::System, AclMode::CREATE_DB, role_id)],
481            item_usage: &CREATE_ITEM_USAGE,
482            ..Default::default()
483        },
484        Plan::CreateSchema(plan::CreateSchemaPlan {
485            database_spec,
486            schema_name: _,
487            if_not_exists: _,
488        }) => {
489            let privileges = match database_spec {
490                ResolvedDatabaseSpecifier::Ambient => Vec::new(),
491                ResolvedDatabaseSpecifier::Id(database_id) => {
492                    vec![(
493                        SystemObjectId::Object(database_id.into()),
494                        AclMode::CREATE,
495                        role_id,
496                    )]
497                }
498            };
499            RbacRequirements {
500                privileges,
501                item_usage: &CREATE_ITEM_USAGE,
502                ..Default::default()
503            }
504        }
505        Plan::CreateRole(plan::CreateRolePlan {
506            name: _,
507            attributes,
508        }) => {
509            if attributes.superuser.unwrap_or(false) {
510                RbacRequirements {
511                    superuser_action: Some("create superuser role".to_string()),
512                    ..Default::default()
513                }
514            } else {
515                RbacRequirements {
516                    privileges: vec![(SystemObjectId::System, AclMode::CREATE_ROLE, role_id)],
517                    item_usage: &CREATE_ITEM_USAGE,
518                    ..Default::default()
519                }
520            }
521        }
522        Plan::CreateNetworkPolicy(plan::CreateNetworkPolicyPlan { .. }) => RbacRequirements {
523            privileges: vec![(
524                SystemObjectId::System,
525                AclMode::CREATE_NETWORK_POLICY,
526                role_id,
527            )],
528            item_usage: &CREATE_ITEM_USAGE,
529            ..Default::default()
530        },
531        Plan::CreateCluster(plan::CreateClusterPlan {
532            name: _,
533            variant: _,
534            workload_class: _,
535            if_not_exists: _,
536        }) => RbacRequirements {
537            privileges: vec![(SystemObjectId::System, AclMode::CREATE_CLUSTER, role_id)],
538            item_usage: &CREATE_ITEM_USAGE,
539            ..Default::default()
540        },
541        Plan::CreateClusterReplica(plan::CreateClusterReplicaPlan {
542            cluster_id,
543            name: _,
544            config: _,
545            if_not_exists: _,
546        }) => RbacRequirements {
547            ownership: vec![ObjectId::Cluster(*cluster_id)],
548            item_usage: &CREATE_ITEM_USAGE,
549            ..Default::default()
550        },
551        Plan::CreateSource(plan::CreateSourcePlan {
552            name,
553            source,
554            if_not_exists: _,
555            timeline: _,
556            in_cluster,
557        }) => RbacRequirements {
558            privileges: generate_required_source_privileges(
559                name,
560                &source.data_source,
561                *in_cluster,
562                role_id,
563            ),
564            item_usage: &CREATE_ITEM_USAGE,
565            ..Default::default()
566        },
567        Plan::CreateSources(plans) => RbacRequirements {
568            privileges: plans
569                .iter()
570                .flat_map(
571                    |plan::CreateSourcePlanBundle {
572                         item_id: _,
573                         global_id: _,
574                         plan:
575                             plan::CreateSourcePlan {
576                                 name,
577                                 source,
578                                 if_not_exists: _,
579                                 timeline: _,
580                                 in_cluster,
581                             },
582                         resolved_ids: _,
583                         available_source_references: _,
584                     }| {
585                        generate_required_source_privileges(
586                            name,
587                            &source.data_source,
588                            *in_cluster,
589                            role_id,
590                        )
591                        .into_iter()
592                    },
593                )
594                .collect(),
595            item_usage: &CREATE_ITEM_USAGE,
596            ..Default::default()
597        },
598        Plan::CreateSecret(plan::CreateSecretPlan {
599            name,
600            secret: _,
601            if_not_exists: _,
602        }) => RbacRequirements {
603            privileges: vec![(
604                SystemObjectId::Object(name.qualifiers.clone().into()),
605                AclMode::CREATE,
606                role_id,
607            )],
608            item_usage: &CREATE_ITEM_USAGE,
609            ..Default::default()
610        },
611        Plan::CreateSink(plan::CreateSinkPlan {
612            name,
613            sink,
614            with_snapshot: _,
615            if_not_exists: _,
616            in_cluster,
617        }) => {
618            let mut privileges = vec![(
619                SystemObjectId::Object(name.qualifiers.clone().into()),
620                AclMode::CREATE,
621                role_id,
622            )];
623            let items = iter::once(sink.from).map(|gid| catalog.resolve_item_id(&gid));
624            privileges.extend_from_slice(&generate_read_privileges(catalog, items, role_id));
625            privileges.push((
626                SystemObjectId::Object(in_cluster.into()),
627                AclMode::CREATE,
628                role_id,
629            ));
630            RbacRequirements {
631                privileges,
632                item_usage: &CREATE_ITEM_USAGE,
633                ..Default::default()
634            }
635        }
636        Plan::CreateTable(plan::CreateTablePlan {
637            name,
638            table: _,
639            if_not_exists: _,
640        }) => RbacRequirements {
641            privileges: vec![(
642                SystemObjectId::Object(name.qualifiers.clone().into()),
643                AclMode::CREATE,
644                role_id,
645            )],
646            item_usage: &CREATE_ITEM_USAGE,
647            ..Default::default()
648        },
649        Plan::CreateView(plan::CreateViewPlan {
650            name,
651            view: _,
652            replace,
653            drop_ids: _,
654            if_not_exists: _,
655            ambiguous_columns: _,
656        }) => RbacRequirements {
657            ownership: replace
658                .map(|id| vec![ObjectId::Item(id)])
659                .unwrap_or_default(),
660            privileges: vec![(
661                SystemObjectId::Object(name.qualifiers.clone().into()),
662                AclMode::CREATE,
663                role_id,
664            )],
665            item_usage: &CREATE_ITEM_USAGE,
666            ..Default::default()
667        },
668        Plan::CreateMaterializedView(plan::CreateMaterializedViewPlan {
669            name,
670            materialized_view,
671            replace,
672            drop_ids: _,
673            if_not_exists: _,
674            ambiguous_columns: _,
675        }) => RbacRequirements {
676            // `CREATE REPLACEMENT MATERIALIZED VIEW ... FOR <target>` requires ownership of the
677            // target, mirroring `ALTER ... APPLY REPLACEMENT` and `CREATE INDEX`, which require
678            // ownership of the item they act on. `replace` is the separate `CREATE OR REPLACE`
679            // target. Both are optional and independent, so require ownership of whichever are set.
680            ownership: replace
681                .iter()
682                .chain(materialized_view.replacement_target.iter())
683                .map(|id| ObjectId::Item(*id))
684                .collect(),
685            privileges: vec![
686                (
687                    SystemObjectId::Object(name.qualifiers.clone().into()),
688                    AclMode::CREATE,
689                    role_id,
690                ),
691                (
692                    SystemObjectId::Object(materialized_view.cluster_id.into()),
693                    AclMode::CREATE,
694                    role_id,
695                ),
696            ],
697            item_usage: &CREATE_ITEM_USAGE,
698            ..Default::default()
699        },
700        Plan::CreateIndex(plan::CreateIndexPlan {
701            name,
702            index,
703            if_not_exists: _,
704        }) => {
705            let index_on_item = catalog.resolve_item_id(&index.on);
706            RbacRequirements {
707                ownership: vec![ObjectId::Item(index_on_item)],
708                privileges: vec![
709                    (
710                        SystemObjectId::Object(name.qualifiers.clone().into()),
711                        AclMode::CREATE,
712                        role_id,
713                    ),
714                    (
715                        SystemObjectId::Object(index.cluster_id.into()),
716                        AclMode::CREATE,
717                        role_id,
718                    ),
719                ],
720                item_usage: &CREATE_ITEM_USAGE,
721                ..Default::default()
722            }
723        }
724        Plan::CreateMetricSink(plan::CreateMetricSinkPlan {
725            name,
726            metric_sink,
727            if_not_exists: _,
728        }) => {
729            // A metric sink republishes the FROM relation's rows on the replica's scrape
730            // endpoint, so it is an egress path for that relation's contents, exactly like
731            // CREATE SINK. The guard is therefore read privileges on the source, not
732            // ownership of it.
733            let mut privileges = vec![(
734                SystemObjectId::Object(name.qualifiers.clone().into()),
735                AclMode::CREATE,
736                role_id,
737            )];
738            let items = iter::once(metric_sink.from).map(|gid| catalog.resolve_item_id(&gid));
739            privileges.extend_from_slice(&generate_read_privileges(catalog, items, role_id));
740            privileges.push((
741                SystemObjectId::Object(metric_sink.cluster_id.into()),
742                AclMode::CREATE,
743                role_id,
744            ));
745            RbacRequirements {
746                privileges,
747                item_usage: &CREATE_ITEM_USAGE,
748                ..Default::default()
749            }
750        }
751        Plan::CreateType(plan::CreateTypePlan { name, typ: _ }) => RbacRequirements {
752            privileges: vec![(
753                SystemObjectId::Object(name.qualifiers.clone().into()),
754                AclMode::CREATE,
755                role_id,
756            )],
757            item_usage: &CREATE_ITEM_USAGE,
758            ..Default::default()
759        },
760        Plan::Comment(plan::CommentPlan {
761            object_id,
762            sub_component: _,
763            comment: _,
764        }) => {
765            let (ownership, privileges) = match object_id {
766                // Roles don't have owners, instead we require the current session to have the
767                // `CREATEROLE` privilege.
768                CommentObjectId::Role(_) => (
769                    Vec::new(),
770                    vec![(SystemObjectId::System, AclMode::CREATE_ROLE, role_id)],
771                ),
772                _ => (vec![ObjectId::from(*object_id)], Vec::new()),
773            };
774            RbacRequirements {
775                ownership,
776                privileges,
777                ..Default::default()
778            }
779        }
780        Plan::DropObjects(plan::DropObjectsPlan {
781            referenced_ids,
782            drop_ids: _,
783            object_type,
784        }) => {
785            let privileges = if object_type == &ObjectType::Role {
786                vec![(SystemObjectId::System, AclMode::CREATE_ROLE, role_id)]
787            } else {
788                referenced_ids
789                    .iter()
790                    .filter_map(|id| match id {
791                        ObjectId::ClusterReplica((cluster_id, _)) => Some((
792                            SystemObjectId::Object(cluster_id.into()),
793                            AclMode::USAGE,
794                            role_id,
795                        )),
796                        ObjectId::Schema((database_spec, _)) => match database_spec {
797                            ResolvedDatabaseSpecifier::Ambient => None,
798                            ResolvedDatabaseSpecifier::Id(database_id) => Some((
799                                SystemObjectId::Object(database_id.into()),
800                                AclMode::USAGE,
801                                role_id,
802                            )),
803                        },
804                        ObjectId::Item(item_id) => {
805                            let item = catalog.get_item(item_id);
806                            Some((
807                                SystemObjectId::Object(item.name().qualifiers.clone().into()),
808                                AclMode::USAGE,
809                                role_id,
810                            ))
811                        }
812                        ObjectId::Cluster(_)
813                        | ObjectId::Database(_)
814                        | ObjectId::Role(_)
815                        | ObjectId::NetworkPolicy(_) => None,
816                    })
817                    .collect()
818            };
819            RbacRequirements {
820                // Do not need ownership of descendant objects.
821                ownership: referenced_ids.clone(),
822                privileges,
823                ..Default::default()
824            }
825        }
826        Plan::DropOwned(plan::DropOwnedPlan {
827            role_ids,
828            drop_ids: _,
829            privilege_revokes: _,
830            default_privilege_revokes: _,
831        }) => RbacRequirements {
832            role_membership: role_ids.into_iter().cloned().collect(),
833            ..Default::default()
834        },
835        Plan::ShowCreate(plan::ShowCreatePlan { id, row: _ }) => {
836            let container_id = match id {
837                ObjectId::Item(id) => Some(SystemObjectId::Object(
838                    catalog.get_item(id).name().qualifiers.clone().into(),
839                )),
840                ObjectId::Schema((database_id, _schema_id)) => match database_id {
841                    ResolvedDatabaseSpecifier::Ambient => None,
842                    ResolvedDatabaseSpecifier::Id(id) => Some(SystemObjectId::Object(id.into())),
843                },
844                ObjectId::Cluster(_)
845                | ObjectId::ClusterReplica(_)
846                | ObjectId::Database(_)
847                | ObjectId::Role(_)
848                | ObjectId::NetworkPolicy(_) => None,
849            };
850            let privileges = match container_id {
851                Some(id) => vec![(id, AclMode::USAGE, role_id)],
852                None => Vec::new(),
853            };
854            RbacRequirements {
855                privileges,
856                item_usage: &EMPTY_ITEM_USAGE,
857                ..Default::default()
858            }
859        }
860        Plan::ShowColumns(plan::ShowColumnsPlan {
861            id,
862            select_plan,
863            new_resolved_ids: _,
864        }) => {
865            let mut privileges = vec![(
866                SystemObjectId::Object(catalog.get_item(id).name().qualifiers.clone().into()),
867                AclMode::USAGE,
868                role_id,
869            )];
870
871            for privilege in generate_rbac_requirements(
872                catalog,
873                &Plan::Select(select_plan.clone()),
874                target_conn_role,
875                target_cluster_id,
876                role_id,
877            )
878            .privileges
879            {
880                privileges.push(privilege);
881            }
882            RbacRequirements {
883                privileges,
884                ..Default::default()
885            }
886        }
887        Plan::Select(plan::SelectPlan {
888            source,
889            select: _,
890            when: _,
891            finishing: _,
892            copy_to: _,
893        }) => {
894            let items = source
895                .depends_on()
896                .into_iter()
897                .map(|gid| catalog.resolve_item_id(&gid));
898            let mut privileges = generate_read_privileges(catalog, items, role_id);
899            if let Some(privilege) = generate_cluster_usage_privileges(
900                source.as_const().is_some(),
901                target_cluster_id,
902                role_id,
903            ) {
904                privileges.push(privilege);
905            }
906            RbacRequirements {
907                privileges,
908                ..Default::default()
909            }
910        }
911        Plan::Subscribe(plan::SubscribePlan {
912            from,
913            with_snapshot: _,
914            when: _,
915            up_to: _,
916            copy_to: _,
917            emit_progress: _,
918            output: _,
919        }) => {
920            let items = from
921                .depends_on()
922                .into_iter()
923                .map(|gid| catalog.resolve_item_id(&gid));
924            let mut privileges = generate_read_privileges(catalog, items, role_id);
925            if let Some(cluster_id) = target_cluster_id {
926                privileges.push((
927                    SystemObjectId::Object(cluster_id.into()),
928                    AclMode::USAGE,
929                    role_id,
930                ));
931            }
932            RbacRequirements {
933                privileges,
934                ..Default::default()
935            }
936        }
937        Plan::CopyFrom(plan::CopyFromPlan {
938            target_name: _,
939            target_id,
940            source: _,
941            columns: _,
942            source_desc: _,
943            mfp: _,
944            params: _,
945            filter: _,
946        }) => RbacRequirements {
947            privileges: vec![
948                (
949                    SystemObjectId::Object(
950                        catalog.get_item(target_id).name().qualifiers.clone().into(),
951                    ),
952                    AclMode::USAGE,
953                    role_id,
954                ),
955                (
956                    SystemObjectId::Object(target_id.into()),
957                    AclMode::INSERT,
958                    role_id,
959                ),
960            ],
961            ..Default::default()
962        },
963        Plan::CopyTo(plan::CopyToPlan {
964            select_plan,
965            desc: _,
966            to: _,
967            connection: _,
968            connection_id: _,
969            format: _,
970            max_file_size: _,
971        }) => {
972            let items = select_plan
973                .source
974                .depends_on()
975                .into_iter()
976                .map(|gid| catalog.resolve_item_id(&gid));
977            let mut privileges = generate_read_privileges(catalog, items, role_id);
978            if let Some(cluster_id) = target_cluster_id {
979                privileges.push((
980                    SystemObjectId::Object(cluster_id.into()),
981                    AclMode::USAGE,
982                    role_id,
983                ));
984            }
985            RbacRequirements {
986                privileges,
987                ..Default::default()
988            }
989        }
990        Plan::ExplainPlan(plan::ExplainPlanPlan {
991            stage: _,
992            format: _,
993            config: _,
994            explainee,
995        })
996        | Plan::ExplainPushdown(plan::ExplainPushdownPlan { explainee }) => RbacRequirements {
997            privileges: match explainee {
998                Explainee::View(id)
999                | Explainee::MaterializedView(id)
1000                | Explainee::Index(id)
1001                | Explainee::ReplanView(id)
1002                | Explainee::ReplanMaterializedView(id)
1003                | Explainee::ReplanIndex(id) => {
1004                    let item = catalog.get_item(id);
1005                    let schema_id: ObjectId = item.name().qualifiers.clone().into();
1006                    vec![(SystemObjectId::Object(schema_id), AclMode::USAGE, role_id)]
1007                }
1008                Explainee::Statement(stmt) => stmt
1009                    .depends_on()
1010                    .into_iter()
1011                    .map(|id| {
1012                        let item = catalog.get_item_by_global_id(&id);
1013                        let schema_id: ObjectId = item.name().qualifiers.clone().into();
1014                        (SystemObjectId::Object(schema_id), AclMode::USAGE, role_id)
1015                    })
1016                    .collect(),
1017            },
1018            item_usage: match explainee {
1019                Explainee::View(..)
1020                | Explainee::MaterializedView(..)
1021                | Explainee::Index(..)
1022                | Explainee::ReplanView(..)
1023                | Explainee::ReplanMaterializedView(..)
1024                | Explainee::ReplanIndex(..) => &EMPTY_ITEM_USAGE,
1025                Explainee::Statement(_) => &DEFAULT_ITEM_USAGE,
1026            },
1027            ..Default::default()
1028        },
1029        Plan::ExplainSinkSchema(plan::ExplainSinkSchemaPlan { sink_from, .. }) => {
1030            RbacRequirements {
1031                privileges: {
1032                    let item = catalog.get_item_by_global_id(sink_from);
1033                    let schema_id: ObjectId = item.name().qualifiers.clone().into();
1034                    vec![(SystemObjectId::Object(schema_id), AclMode::USAGE, role_id)]
1035                },
1036                item_usage: &EMPTY_ITEM_USAGE,
1037                ..Default::default()
1038            }
1039        }
1040        Plan::ExplainTimestamp(plan::ExplainTimestampPlan {
1041            format: _,
1042            raw_plan,
1043            when: _,
1044        }) => RbacRequirements {
1045            privileges: raw_plan
1046                .depends_on()
1047                .into_iter()
1048                .map(|id| {
1049                    let item = catalog.get_item_by_global_id(&id);
1050                    let schema_id: ObjectId = item.name().qualifiers.clone().into();
1051                    (SystemObjectId::Object(schema_id), AclMode::USAGE, role_id)
1052                })
1053                .collect(),
1054            ..Default::default()
1055        },
1056        Plan::Insert(plan::InsertPlan {
1057            id,
1058            values,
1059            returning,
1060        }) => {
1061            let schema_id: ObjectId = catalog.get_item(id).name().qualifiers.clone().into();
1062            let mut privileges = vec![
1063                (
1064                    SystemObjectId::Object(schema_id.clone()),
1065                    AclMode::USAGE,
1066                    role_id,
1067                ),
1068                (SystemObjectId::Object(id.into()), AclMode::INSERT, role_id),
1069            ];
1070            let mut seen = BTreeSet::from([(schema_id, role_id)]);
1071
1072            // We don't allow arbitrary sub-queries in `returning`. So either it
1073            // contains a column reference to the outer table or it's constant.
1074            if returning
1075                .iter()
1076                .any(|assignment| assignment.contains_column())
1077            {
1078                privileges.push((SystemObjectId::Object(id.into()), AclMode::SELECT, role_id));
1079                seen.insert((id.into(), role_id));
1080            }
1081
1082            let items = values
1083                .depends_on()
1084                .into_iter()
1085                .map(|gid| catalog.resolve_item_id(&gid));
1086            privileges.extend_from_slice(&generate_read_privileges_inner(
1087                catalog, items, role_id, &mut seen,
1088            ));
1089
1090            if let Some(privilege) = generate_cluster_usage_privileges(
1091                values.as_const().is_some(),
1092                target_cluster_id,
1093                role_id,
1094            ) {
1095                privileges.push(privilege);
1096            } else if !returning.is_empty() {
1097                // TODO(jkosh44) returning may be a constant, but for now we are overly protective
1098                //  and require cluster privileges for all returning.
1099                if let Some(cluster_id) = target_cluster_id {
1100                    privileges.push((
1101                        SystemObjectId::Object(cluster_id.into()),
1102                        AclMode::USAGE,
1103                        role_id,
1104                    ));
1105                }
1106            }
1107            RbacRequirements {
1108                privileges,
1109                ..Default::default()
1110            }
1111        }
1112        Plan::AlterCluster(plan::AlterClusterPlan {
1113            id,
1114            name: _,
1115            options: _,
1116            strategy: _,
1117        }) => RbacRequirements {
1118            ownership: vec![ObjectId::Cluster(*id)],
1119            item_usage: &CREATE_ITEM_USAGE,
1120            ..Default::default()
1121        },
1122        Plan::AlterSetCluster(plan::AlterSetClusterPlan { id, set_cluster }) => RbacRequirements {
1123            ownership: vec![ObjectId::Item(*id)],
1124            privileges: vec![(
1125                SystemObjectId::Object(set_cluster.into()),
1126                AclMode::CREATE,
1127                role_id,
1128            )],
1129            item_usage: &CREATE_ITEM_USAGE,
1130            ..Default::default()
1131        },
1132        Plan::AlterRetainHistory(plan::AlterRetainHistoryPlan {
1133            id,
1134            window: _,
1135            value: _,
1136            object_type: _,
1137        }) => RbacRequirements {
1138            ownership: vec![ObjectId::Item(*id)],
1139            item_usage: &CREATE_ITEM_USAGE,
1140            ..Default::default()
1141        },
1142        Plan::AlterSourceTimestampInterval(plan::AlterSourceTimestampIntervalPlan {
1143            id,
1144            value: _,
1145            interval: _,
1146        }) => RbacRequirements {
1147            ownership: vec![ObjectId::Item(*id)],
1148            item_usage: &CREATE_ITEM_USAGE,
1149            ..Default::default()
1150        },
1151        Plan::AlterConnection(plan::AlterConnectionPlan { id, action: _ }) => RbacRequirements {
1152            ownership: vec![ObjectId::Item(*id)],
1153            ..Default::default()
1154        },
1155        Plan::AlterSource(plan::AlterSourcePlan {
1156            item_id,
1157            ingestion_id: _,
1158            action: _,
1159        }) => RbacRequirements {
1160            ownership: vec![ObjectId::Item(*item_id)],
1161            item_usage: &CREATE_ITEM_USAGE,
1162            ..Default::default()
1163        },
1164        Plan::AlterSink(plan::AlterSinkPlan {
1165            item_id,
1166            global_id: _,
1167            sink,
1168            with_snapshot: _,
1169            in_cluster,
1170            set_options: _,
1171            reset_options: _,
1172        }) => {
1173            let items = iter::once(sink.from).map(|gid| catalog.resolve_item_id(&gid));
1174            let mut privileges = generate_read_privileges(catalog, items, role_id);
1175            privileges.push((
1176                SystemObjectId::Object(in_cluster.into()),
1177                AclMode::CREATE,
1178                role_id,
1179            ));
1180            RbacRequirements {
1181                ownership: vec![ObjectId::Item(*item_id)],
1182                privileges,
1183                item_usage: &CREATE_ITEM_USAGE,
1184                ..Default::default()
1185            }
1186        }
1187        Plan::AlterClusterRename(plan::AlterClusterRenamePlan {
1188            id,
1189            name: _,
1190            to_name: _,
1191        }) => RbacRequirements {
1192            ownership: vec![ObjectId::Cluster(*id)],
1193            ..Default::default()
1194        },
1195        Plan::AlterClusterSwap(plan::AlterClusterSwapPlan {
1196            id_a,
1197            id_b,
1198            name_a: _,
1199            name_b: _,
1200            name_temp: _,
1201        }) => RbacRequirements {
1202            ownership: vec![ObjectId::Cluster(*id_a), ObjectId::Cluster(*id_b)],
1203            ..Default::default()
1204        },
1205        Plan::AlterClusterReplicaRename(plan::AlterClusterReplicaRenamePlan {
1206            cluster_id,
1207            replica_id,
1208            name: _,
1209            to_name: _,
1210        }) => RbacRequirements {
1211            ownership: vec![ObjectId::ClusterReplica((*cluster_id, *replica_id))],
1212            ..Default::default()
1213        },
1214        Plan::AlterItemRename(plan::AlterItemRenamePlan {
1215            id,
1216            current_full_name: _,
1217            to_name: _,
1218            object_type: _,
1219        }) => RbacRequirements {
1220            ownership: vec![ObjectId::Item(*id)],
1221            ..Default::default()
1222        },
1223        Plan::AlterSchemaRename(plan::AlterSchemaRenamePlan {
1224            cur_schema_spec,
1225            new_schema_name: _,
1226        }) => {
1227            let privileges = match cur_schema_spec.0 {
1228                ResolvedDatabaseSpecifier::Id(db_id) => vec![(
1229                    SystemObjectId::Object(ObjectId::Database(db_id)),
1230                    AclMode::CREATE,
1231                    role_id,
1232                )],
1233                ResolvedDatabaseSpecifier::Ambient => vec![],
1234            };
1235
1236            RbacRequirements {
1237                ownership: vec![ObjectId::Schema(*cur_schema_spec)],
1238                privileges,
1239                ..Default::default()
1240            }
1241        }
1242        Plan::AlterSchemaSwap(plan::AlterSchemaSwapPlan {
1243            schema_a_spec,
1244            schema_a_name: _,
1245            schema_b_spec,
1246            schema_b_name: _,
1247            name_temp: _,
1248        }) => {
1249            let mut privileges = vec![];
1250            if let ResolvedDatabaseSpecifier::Id(id_a) = schema_a_spec.0 {
1251                privileges.push((
1252                    SystemObjectId::Object(ObjectId::Database(id_a)),
1253                    AclMode::CREATE,
1254                    role_id,
1255                ));
1256            }
1257            if let ResolvedDatabaseSpecifier::Id(id_b) = schema_b_spec.0 {
1258                privileges.push((
1259                    SystemObjectId::Object(ObjectId::Database(id_b)),
1260                    AclMode::CREATE,
1261                    role_id,
1262                ));
1263            }
1264
1265            RbacRequirements {
1266                ownership: vec![
1267                    ObjectId::Schema(*schema_a_spec),
1268                    ObjectId::Schema(*schema_b_spec),
1269                ],
1270                privileges,
1271                ..Default::default()
1272            }
1273        }
1274        Plan::AlterSecret(plan::AlterSecretPlan { id, secret_as: _ }) => RbacRequirements {
1275            ownership: vec![ObjectId::Item(*id)],
1276            item_usage: &CREATE_ITEM_USAGE,
1277            ..Default::default()
1278        },
1279        Plan::AlterRole(plan::AlterRolePlan {
1280            id,
1281            name: _,
1282            option,
1283        }) => match option {
1284            // Only superusers can alter the superuserness of a role.
1285            plan::PlannedAlterRoleOption::Attributes(attributes)
1286                if attributes.superuser.is_some() =>
1287            {
1288                RbacRequirements {
1289                    superuser_action: Some("alter superuser role".to_string()),
1290                    ..Default::default()
1291                }
1292            }
1293            // Roles are allowed to change their own password, but only if
1294            // password is the sole attribute being changed.
1295            plan::PlannedAlterRoleOption::Attributes(plan::PlannedRoleAttributes {
1296                password,
1297                // scram_iterations and nopassword are password-related, so
1298                // they're fine to change alongside the password.
1299                scram_iterations: _,
1300                nopassword: _,
1301                // superuser is already handled by the match arm above, so it
1302                // will always be None here.
1303                superuser: None,
1304                inherit: None,
1305                login: None,
1306            }) if password.is_some() && role_id == *id => RbacRequirements::default(),
1307            // But no one elses...
1308            plan::PlannedAlterRoleOption::Attributes(attributes)
1309                if attributes.password.is_some() && role_id != *id =>
1310            {
1311                RbacRequirements {
1312                    superuser_action: Some("alter password of role".to_string()),
1313                    ..Default::default()
1314                }
1315            }
1316            // restrict_to_user_objects can only be set by superuser.
1317            // SECURITY: This must use case-insensitive comparison because
1318            // var.name() comes from Ident::to_string() which preserves the
1319            // original casing for quoted identifiers.
1320            plan::PlannedAlterRoleOption::Variable(var)
1321                if var.name().eq_ignore_ascii_case("restrict_to_user_objects") =>
1322            {
1323                RbacRequirements {
1324                    superuser_action: Some("set restrict_to_user_objects".to_string()),
1325                    ..Default::default()
1326                }
1327            }
1328            // Roles are allowed to change their own other variables.
1329            plan::PlannedAlterRoleOption::Variable(_) if role_id == *id => {
1330                RbacRequirements::default()
1331            }
1332            // Otherwise to ALTER a role, you need to have the CREATE_ROLE privilege.
1333            _ => RbacRequirements {
1334                privileges: vec![(SystemObjectId::System, AclMode::CREATE_ROLE, role_id)],
1335                item_usage: &CREATE_ITEM_USAGE,
1336                ..Default::default()
1337            },
1338        },
1339        Plan::AlterOwner(plan::AlterOwnerPlan {
1340            id,
1341            object_type: _,
1342            new_owner,
1343        }) => {
1344            let privileges = match id {
1345                ObjectId::ClusterReplica((cluster_id, _)) => {
1346                    vec![(
1347                        SystemObjectId::Object(cluster_id.into()),
1348                        AclMode::CREATE,
1349                        role_id,
1350                    )]
1351                }
1352                ObjectId::Schema((database_spec, _)) => match database_spec {
1353                    ResolvedDatabaseSpecifier::Ambient => Vec::new(),
1354                    ResolvedDatabaseSpecifier::Id(database_id) => {
1355                        vec![(
1356                            SystemObjectId::Object(database_id.into()),
1357                            AclMode::CREATE,
1358                            role_id,
1359                        )]
1360                    }
1361                },
1362                ObjectId::Item(item_id) => {
1363                    let item = catalog.get_item(item_id);
1364                    vec![(
1365                        SystemObjectId::Object(item.name().qualifiers.clone().into()),
1366                        AclMode::CREATE,
1367                        role_id,
1368                    )]
1369                }
1370                ObjectId::Cluster(_)
1371                | ObjectId::Database(_)
1372                | ObjectId::Role(_)
1373                | ObjectId::NetworkPolicy(_) => Vec::new(),
1374            };
1375            RbacRequirements {
1376                role_membership: BTreeSet::from([*new_owner]),
1377                ownership: vec![id.clone()],
1378                privileges,
1379                ..Default::default()
1380            }
1381        }
1382        Plan::AlterTableAddColumn(plan::AlterTablePlan { relation_id, .. }) => RbacRequirements {
1383            ownership: vec![ObjectId::Item(*relation_id)],
1384            item_usage: &CREATE_ITEM_USAGE,
1385            ..Default::default()
1386        },
1387        Plan::AlterMaterializedViewApplyReplacement(
1388            plan::AlterMaterializedViewApplyReplacementPlan { id, replacement_id },
1389        ) => RbacRequirements {
1390            ownership: vec![ObjectId::Item(*id), ObjectId::Item(*replacement_id)],
1391            item_usage: &CREATE_ITEM_USAGE,
1392            ..Default::default()
1393        },
1394        Plan::AlterNetworkPolicy(plan::AlterNetworkPolicyPlan { id, .. }) => RbacRequirements {
1395            ownership: vec![ObjectId::NetworkPolicy(*id)],
1396            item_usage: &CREATE_ITEM_USAGE,
1397            ..Default::default()
1398        },
1399        Plan::ReadThenWrite(plan::ReadThenWritePlan {
1400            id,
1401            selection,
1402            finishing: _,
1403            assignments,
1404            kind,
1405            returning,
1406        }) => {
1407            let acl_mode = match kind {
1408                MutationKind::Insert => AclMode::INSERT,
1409                MutationKind::Update => AclMode::UPDATE,
1410                MutationKind::Delete => AclMode::DELETE,
1411            };
1412            let schema_id: ObjectId = catalog.get_item(id).name().qualifiers.clone().into();
1413            let mut privileges = vec![
1414                (
1415                    SystemObjectId::Object(schema_id.clone()),
1416                    AclMode::USAGE,
1417                    role_id,
1418                ),
1419                (SystemObjectId::Object(id.into()), acl_mode, role_id),
1420            ];
1421            let mut seen = BTreeSet::from([(schema_id, role_id)]);
1422
1423            // We don't allow arbitrary sub-queries in `assignments` or `returning`. So either they
1424            // contains a column reference to the outer table or it's constant.
1425            if assignments
1426                .values()
1427                .chain(returning.iter())
1428                .any(|assignment| assignment.contains_column())
1429            {
1430                privileges.push((SystemObjectId::Object(id.into()), AclMode::SELECT, role_id));
1431                seen.insert((id.into(), role_id));
1432            }
1433
1434            // TODO(jkosh44) It's fairly difficult to determine what part of `selection` is from a
1435            //  user specified read and what part is from the implementation of the read then write.
1436            //  instead we are overly protective and always require SELECT privileges even though
1437            //  PostgreSQL doesn't always do this.
1438            //  As a concrete example, we require SELECT and UPDATE privileges to execute
1439            //  `UPDATE t SET a = 42;`, while PostgreSQL only requires UPDATE privileges.
1440            let items = selection
1441                .depends_on()
1442                .into_iter()
1443                .map(|gid| catalog.resolve_item_id(&gid));
1444            privileges.extend_from_slice(&generate_read_privileges_inner(
1445                catalog, items, role_id, &mut seen,
1446            ));
1447
1448            if let Some(privilege) = generate_cluster_usage_privileges(
1449                selection.as_const().is_some(),
1450                target_cluster_id,
1451                role_id,
1452            ) {
1453                privileges.push(privilege);
1454            }
1455            RbacRequirements {
1456                privileges,
1457                ..Default::default()
1458            }
1459        }
1460        Plan::GrantRole(plan::GrantRolePlan {
1461            role_ids: _,
1462            member_ids: _,
1463            grantor_id: _,
1464        })
1465        | Plan::RevokeRole(plan::RevokeRolePlan {
1466            role_ids: _,
1467            member_ids: _,
1468            grantor_id: _,
1469        }) => RbacRequirements {
1470            privileges: vec![(SystemObjectId::System, AclMode::CREATE_ROLE, role_id)],
1471            ..Default::default()
1472        },
1473        Plan::GrantPrivileges(plan::GrantPrivilegesPlan {
1474            update_privileges,
1475            grantees: _,
1476        })
1477        | Plan::RevokePrivileges(plan::RevokePrivilegesPlan {
1478            update_privileges,
1479            revokees: _,
1480        }) => {
1481            let mut privileges = Vec::with_capacity(update_privileges.len());
1482            for UpdatePrivilege { target_id, .. } in update_privileges {
1483                match target_id {
1484                    SystemObjectId::Object(object_id) => match object_id {
1485                        ObjectId::ClusterReplica((cluster_id, _)) => {
1486                            privileges.push((
1487                                SystemObjectId::Object(cluster_id.into()),
1488                                AclMode::USAGE,
1489                                role_id,
1490                            ));
1491                        }
1492                        ObjectId::Schema((database_spec, _)) => match database_spec {
1493                            ResolvedDatabaseSpecifier::Ambient => {}
1494                            ResolvedDatabaseSpecifier::Id(database_id) => {
1495                                privileges.push((
1496                                    SystemObjectId::Object(database_id.into()),
1497                                    AclMode::USAGE,
1498                                    role_id,
1499                                ));
1500                            }
1501                        },
1502                        ObjectId::Item(item_id) => {
1503                            let item = catalog.get_item(item_id);
1504                            privileges.push((
1505                                SystemObjectId::Object(item.name().qualifiers.clone().into()),
1506                                AclMode::USAGE,
1507                                role_id,
1508                            ))
1509                        }
1510                        ObjectId::Cluster(_)
1511                        | ObjectId::Database(_)
1512                        | ObjectId::Role(_)
1513                        | ObjectId::NetworkPolicy(_) => {}
1514                    },
1515                    SystemObjectId::System => {}
1516                }
1517            }
1518            RbacRequirements {
1519                ownership: update_privileges
1520                    .iter()
1521                    .filter_map(|update_privilege| update_privilege.target_id.object_id())
1522                    .cloned()
1523                    .collect(),
1524                privileges,
1525                // To grant/revoke a privilege on some object, generally the grantor/revoker must be the
1526                // owner of that object (or have a grant option on that object which isn't implemented in
1527                // Materialize yet). There is no owner of the entire system, so it's only reasonable to
1528                // restrict granting/revoking system privileges to superusers.
1529                superuser_action: if update_privileges
1530                    .iter()
1531                    .any(|update_privilege| update_privilege.target_id.is_system())
1532                {
1533                    Some("GRANT/REVOKE SYSTEM PRIVILEGES".to_string())
1534                } else {
1535                    None
1536                },
1537                ..Default::default()
1538            }
1539        }
1540        Plan::AlterDefaultPrivileges(plan::AlterDefaultPrivilegesPlan {
1541            privilege_objects,
1542            privilege_acl_items: _,
1543            is_grant: _,
1544        }) => RbacRequirements {
1545            role_membership: privilege_objects
1546                .iter()
1547                .map(|privilege_object| privilege_object.role_id)
1548                .collect(),
1549            privileges: privilege_objects
1550                .into_iter()
1551                .filter_map(|privilege_object| {
1552                    if let (Some(database_id), Some(_)) =
1553                        (privilege_object.database_id, privilege_object.schema_id)
1554                    {
1555                        Some((
1556                            SystemObjectId::Object(database_id.into()),
1557                            AclMode::USAGE,
1558                            role_id,
1559                        ))
1560                    } else {
1561                        None
1562                    }
1563                })
1564                .collect(),
1565            // Altering the default privileges for the PUBLIC role (aka ALL ROLES) will affect all roles
1566            // that currently exist and roles that will exist in the future. It's impossible for an exising
1567            // role to be a member of a role that doesn't exist yet, so no current role could possibly have
1568            // the privileges required to alter default privileges for the PUBLIC role. Therefore we
1569            // only superusers can alter default privileges for the PUBLIC role.
1570            superuser_action: if privilege_objects
1571                .iter()
1572                .any(|privilege_object| privilege_object.role_id.is_public())
1573            {
1574                Some("ALTER DEFAULT PRIVILEGES FOR ALL ROLES".to_string())
1575            } else {
1576                None
1577            },
1578            ..Default::default()
1579        },
1580        Plan::ReassignOwned(plan::ReassignOwnedPlan {
1581            old_roles,
1582            new_role,
1583            reassign_ids: _,
1584        }) => RbacRequirements {
1585            role_membership: old_roles
1586                .into_iter()
1587                .cloned()
1588                .chain(iter::once(*new_role))
1589                .collect(),
1590            ..Default::default()
1591        },
1592        Plan::SideEffectingFunc(func) => {
1593            let role_membership = match func {
1594                // A `NULL` argument cancels no connection (the function returns
1595                // `NULL`), so there is no role membership to require.
1596                SideEffectingFunc::PgCancelBackend {
1597                    connection_id: None,
1598                } => BTreeSet::new(),
1599                SideEffectingFunc::PgCancelBackend {
1600                    connection_id: Some(_),
1601                } => target_conn_role.map(|x| [x].into()).unwrap_or_default(),
1602            };
1603            RbacRequirements {
1604                role_membership,
1605                ..Default::default()
1606            }
1607        }
1608        Plan::ValidateConnection(plan::ValidateConnectionPlan { id, connection: _ }) => {
1609            let schema_id: ObjectId = catalog.get_item(id).name().qualifiers.clone().into();
1610            RbacRequirements {
1611                privileges: vec![
1612                    (SystemObjectId::Object(schema_id), AclMode::USAGE, role_id),
1613                    (SystemObjectId::Object(id.into()), AclMode::USAGE, role_id),
1614                ],
1615                ..Default::default()
1616            }
1617        }
1618        Plan::DiscardTemp
1619        | Plan::DiscardAll
1620        | Plan::EmptyQuery
1621        | Plan::ShowAllVariables
1622        | Plan::ShowVariable(plan::ShowVariablePlan { name: _ })
1623        | Plan::InspectShard(plan::InspectShardPlan { id: _ })
1624        | Plan::SetVariable(plan::SetVariablePlan {
1625            name: _,
1626            value: _,
1627            local: _,
1628        })
1629        | Plan::ResetVariable(plan::ResetVariablePlan { name: _ })
1630        | Plan::SetTransaction(plan::SetTransactionPlan { local: _, modes: _ })
1631        | Plan::StartTransaction(plan::StartTransactionPlan {
1632            access: _,
1633            isolation_level: _,
1634        })
1635        | Plan::CommitTransaction(plan::CommitTransactionPlan {
1636            transaction_type: _,
1637        })
1638        | Plan::AbortTransaction(plan::AbortTransactionPlan {
1639            transaction_type: _,
1640        })
1641        | Plan::AlterNoop(plan::AlterNoopPlan { object_type: _ })
1642        | Plan::AlterSystemSet(plan::AlterSystemSetPlan { name: _, value: _ })
1643        | Plan::AlterSystemReset(plan::AlterSystemResetPlan { name: _ })
1644        | Plan::AlterSystemResetAll(plan::AlterSystemResetAllPlan {})
1645        | Plan::Declare(plan::DeclarePlan {
1646            name: _,
1647            stmt: _,
1648            sql: _,
1649            params: _,
1650        })
1651        | Plan::Fetch(plan::FetchPlan {
1652            name: _,
1653            count: _,
1654            timeout: _,
1655        })
1656        | Plan::Close(plan::ClosePlan { name: _ })
1657        | Plan::Prepare(plan::PreparePlan {
1658            name: _,
1659            stmt: _,
1660            desc: _,
1661            sql: _,
1662        })
1663        | Plan::Execute(plan::ExecutePlan { name: _, params: _ })
1664        | Plan::Deallocate(plan::DeallocatePlan { name: _ })
1665        | Plan::Raise(plan::RaisePlan { severity: _ }) => Default::default(),
1666    }
1667}
1668
1669/// Reports whether any role has ownership over an object.
1670fn check_owner_roles(
1671    object_id: &ObjectId,
1672    role_ids: &BTreeSet<RoleId>,
1673    catalog: &impl SessionCatalog,
1674) -> bool {
1675    if let Some(owner_id) = catalog.get_owner_id(object_id) {
1676        role_ids.contains(&owner_id)
1677    } else {
1678        true
1679    }
1680}
1681
1682fn ownership_err(
1683    unheld_ownership: Vec<ObjectId>,
1684    catalog: &impl SessionCatalog,
1685) -> Result<(), UnauthorizedError> {
1686    if !unheld_ownership.is_empty() {
1687        let objects = unheld_ownership
1688            .into_iter()
1689            .map(|ownership| match ownership {
1690                ObjectId::Cluster(id) => (
1691                    ObjectType::Cluster,
1692                    catalog.get_cluster(id).name().to_string(),
1693                ),
1694                ObjectId::ClusterReplica((cluster_id, replica_id)) => {
1695                    let cluster = catalog.get_cluster(cluster_id);
1696                    let replica = catalog.get_cluster_replica(cluster_id, replica_id);
1697                    // Note: using unchecked here is okay because the values are coming from an
1698                    // already existing name.
1699                    let name = QualifiedReplica {
1700                        cluster: Ident::new_unchecked(cluster.name()),
1701                        replica: Ident::new_unchecked(replica.name()),
1702                    };
1703                    (ObjectType::ClusterReplica, name.to_string())
1704                }
1705                ObjectId::Database(id) => (
1706                    ObjectType::Database,
1707                    catalog.get_database(&id).name().to_string(),
1708                ),
1709                ObjectId::Schema((database_spec, schema_spec)) => {
1710                    let schema = catalog.get_schema(&database_spec, &schema_spec);
1711                    let name = catalog.resolve_full_schema_name(schema.name());
1712                    (ObjectType::Schema, name.to_string())
1713                }
1714                ObjectId::Item(id) => {
1715                    let item = catalog.get_item(&id);
1716                    let name = catalog.resolve_full_name(item.name());
1717                    (item.item_type().into(), name.to_string())
1718                }
1719                ObjectId::NetworkPolicy(id) => (
1720                    ObjectType::NetworkPolicy,
1721                    catalog.get_network_policy(&id).name().to_string(),
1722                ),
1723                ObjectId::Role(_) => unreachable!("roles have no owner"),
1724            })
1725            .collect();
1726        Err(UnauthorizedError::Ownership { objects })
1727    } else {
1728        Ok(())
1729    }
1730}
1731
1732fn generate_required_source_privileges(
1733    name: &QualifiedItemName,
1734    data_source: &DataSourceDesc,
1735    in_cluster: Option<ClusterId>,
1736    role_id: RoleId,
1737) -> Vec<(SystemObjectId, AclMode, RoleId)> {
1738    let mut privileges = vec![(
1739        SystemObjectId::Object(name.qualifiers.clone().into()),
1740        AclMode::CREATE,
1741        role_id,
1742    )];
1743    match (data_source, in_cluster) {
1744        (_, Some(id)) => {
1745            privileges.push((SystemObjectId::Object(id.into()), AclMode::CREATE, role_id))
1746        }
1747        (DataSourceDesc::Ingestion(_), None) => {
1748            privileges.push((SystemObjectId::System, AclMode::CREATE_CLUSTER, role_id))
1749        }
1750        // Non-ingestion data-sources have meaningless cluster config's (for now...) and they need
1751        // to be ignored.
1752        // This feels very brittle, but there's not much we can do until the UNDEFINED cluster
1753        // config is removed.
1754        (_, None) => {}
1755    }
1756    privileges
1757}
1758
1759/// Generates all the privileges required to execute a read that includes the objects in `ids`.
1760///
1761/// Not only do we need to validate that `role_id` has read privileges on all relations in `ids`,
1762/// but if any object is a view or materialized view then we need to validate that the owner of
1763/// that view has all of the privileges required to execute the query within the view.
1764///
1765/// For more details see: <https://www.postgresql.org/docs/15/rules-privileges.html>
1766fn generate_read_privileges(
1767    catalog: &impl SessionCatalog,
1768    ids: impl Iterator<Item = CatalogItemId>,
1769    role_id: RoleId,
1770) -> Vec<(SystemObjectId, AclMode, RoleId)> {
1771    generate_read_privileges_inner(catalog, ids, role_id, &mut BTreeSet::new())
1772}
1773
1774fn generate_read_privileges_inner(
1775    catalog: &impl SessionCatalog,
1776    ids: impl Iterator<Item = CatalogItemId>,
1777    role_id: RoleId,
1778    seen: &mut BTreeSet<(ObjectId, RoleId)>,
1779) -> Vec<(SystemObjectId, AclMode, RoleId)> {
1780    let mut privileges = Vec::new();
1781
1782    // Iterative worklist traversal rather than recursion. View dependency
1783    // chains are user controlled and can be arbitrarily deep.
1784    let mut queue: VecDeque<(CatalogItemId, RoleId)> = ids.map(|id| (id, role_id)).collect();
1785    while let Some((id, role_id)) = queue.pop_front() {
1786        if seen.insert((id.into(), role_id)) {
1787            let item = catalog.get_item(&id);
1788            let schema_id: ObjectId = item.name().qualifiers.clone().into();
1789            if seen.insert((schema_id.clone(), role_id)) {
1790                privileges.push((SystemObjectId::Object(schema_id), AclMode::USAGE, role_id))
1791            }
1792            match item.item_type() {
1793                CatalogItemType::View | CatalogItemType::MaterializedView => {
1794                    privileges.push((SystemObjectId::Object(id.into()), AclMode::SELECT, role_id));
1795                    let view_owner = item.owner_id();
1796                    queue.extend(item.references().items().map(|id| (*id, view_owner)));
1797                }
1798                CatalogItemType::Table | CatalogItemType::Source => {
1799                    privileges.push((SystemObjectId::Object(id.into()), AclMode::SELECT, role_id));
1800                }
1801                CatalogItemType::Type | CatalogItemType::Secret | CatalogItemType::Connection => {
1802                    privileges.push((SystemObjectId::Object(id.into()), AclMode::USAGE, role_id));
1803                }
1804                CatalogItemType::Sink
1805                | CatalogItemType::MetricSink
1806                | CatalogItemType::Index
1807                | CatalogItemType::Func => {}
1808            }
1809        }
1810    }
1811
1812    privileges
1813}
1814
1815fn generate_usage_privileges(
1816    catalog: &impl SessionCatalog,
1817    ids: &ResolvedIds,
1818    role_id: RoleId,
1819    item_types: &BTreeSet<CatalogItemType>,
1820) -> BTreeSet<(SystemObjectId, AclMode, RoleId)> {
1821    // Use a `BTreeSet` to remove duplicate privileges.
1822    ids.items()
1823        .filter_map(move |id| {
1824            let item = catalog.get_item(id);
1825            if item_types.contains(&item.item_type()) {
1826                let schema_id = item.name().qualifiers.clone().into();
1827                Some([
1828                    (SystemObjectId::Object(schema_id), AclMode::USAGE, role_id),
1829                    (SystemObjectId::Object(id.into()), AclMode::USAGE, role_id),
1830                ])
1831            } else {
1832                None
1833            }
1834        })
1835        .flatten()
1836        .collect()
1837}
1838
1839fn generate_cluster_usage_privileges(
1840    expr_is_const: bool,
1841    target_cluster_id: Option<ClusterId>,
1842    role_id: RoleId,
1843) -> Option<(SystemObjectId, AclMode, RoleId)> {
1844    // TODO(jkosh44) expr hasn't been fully optimized yet, so it might actually be a constant,
1845    //  but we mistakenly think that it's not. For now it's ok to be overly protective.
1846    if !expr_is_const {
1847        if let Some(cluster_id) = target_cluster_id {
1848            return Some((
1849                SystemObjectId::Object(cluster_id.into()),
1850                AclMode::USAGE,
1851                role_id,
1852            ));
1853        }
1854    }
1855
1856    None
1857}
1858
1859fn check_object_privileges(
1860    catalog: &impl SessionCatalog,
1861    privileges: Vec<(SystemObjectId, AclMode, RoleId)>,
1862    role_membership: BTreeSet<RoleId>,
1863    current_role_id: RoleId,
1864) -> Result<(), UnauthorizedError> {
1865    let mut role_memberships: BTreeMap<RoleId, BTreeSet<RoleId>> = BTreeMap::new();
1866    role_memberships.insert(current_role_id, role_membership);
1867    for (object_id, acl_mode, role_id) in privileges {
1868        // Temporary schemas are owned by the connection that created them,
1869        // so users implicitly have all privileges on their own temp schema.
1870        // The schema may not exist yet (lazy creation), so we skip the check.
1871        if matches!(
1872            &object_id,
1873            SystemObjectId::Object(ObjectId::Schema((_, SchemaSpecifier::Temporary)))
1874        ) {
1875            continue;
1876        }
1877
1878        let role_membership = role_memberships
1879            .entry(role_id)
1880            .or_insert_with_key(|role_id| catalog.collect_role_membership(role_id));
1881        let object_privileges = catalog
1882            .get_privileges(&object_id)
1883            .expect("only object types with privileges will generate required privileges");
1884        let role_privileges = role_membership
1885            .iter()
1886            .flat_map(|role_id| object_privileges.get_acl_items_for_grantee(role_id))
1887            .map(|mz_acl_item| mz_acl_item.acl_mode)
1888            .fold(AclMode::empty(), |accum, acl_mode| accum.union(acl_mode));
1889        if !role_privileges.contains(acl_mode) {
1890            let role_name = catalog.get_role(&role_id).name().to_string();
1891            let privileges = acl_mode.to_error_string();
1892            return Err(UnauthorizedError::Privilege {
1893                object_description: ErrorMessageObjectDescription::from_sys_id(&object_id, catalog),
1894                role_name,
1895                privileges,
1896            });
1897        }
1898    }
1899
1900    Ok(())
1901}
1902
1903pub const fn all_object_privileges(object_type: SystemObjectType) -> AclMode {
1904    const TABLE_ACL_MODE: AclMode = AclMode::INSERT
1905        .union(AclMode::SELECT)
1906        .union(AclMode::UPDATE)
1907        .union(AclMode::DELETE);
1908    const USAGE_CREATE_ACL_MODE: AclMode = AclMode::USAGE.union(AclMode::CREATE);
1909    const ALL_SYSTEM_PRIVILEGES: AclMode = AclMode::CREATE_ROLE
1910        .union(AclMode::CREATE_DB)
1911        .union(AclMode::CREATE_CLUSTER)
1912        .union(AclMode::CREATE_NETWORK_POLICY);
1913
1914    const EMPTY_ACL_MODE: AclMode = AclMode::empty();
1915    match object_type {
1916        SystemObjectType::Object(ObjectType::Table) => TABLE_ACL_MODE,
1917        SystemObjectType::Object(ObjectType::View) => AclMode::SELECT,
1918        SystemObjectType::Object(ObjectType::MaterializedView) => AclMode::SELECT,
1919        SystemObjectType::Object(ObjectType::Source) => AclMode::SELECT,
1920        SystemObjectType::Object(ObjectType::Sink) => EMPTY_ACL_MODE,
1921        SystemObjectType::Object(ObjectType::MetricSink) => EMPTY_ACL_MODE,
1922        SystemObjectType::Object(ObjectType::Index) => EMPTY_ACL_MODE,
1923        SystemObjectType::Object(ObjectType::Type) => AclMode::USAGE,
1924        SystemObjectType::Object(ObjectType::Role) => EMPTY_ACL_MODE,
1925        SystemObjectType::Object(ObjectType::Cluster) => USAGE_CREATE_ACL_MODE,
1926        SystemObjectType::Object(ObjectType::ClusterReplica) => EMPTY_ACL_MODE,
1927        SystemObjectType::Object(ObjectType::Secret) => AclMode::USAGE,
1928        SystemObjectType::Object(ObjectType::NetworkPolicy) => AclMode::USAGE,
1929        SystemObjectType::Object(ObjectType::Connection) => AclMode::USAGE,
1930        SystemObjectType::Object(ObjectType::Database) => USAGE_CREATE_ACL_MODE,
1931        SystemObjectType::Object(ObjectType::Schema) => USAGE_CREATE_ACL_MODE,
1932        SystemObjectType::Object(ObjectType::Func) => EMPTY_ACL_MODE,
1933        SystemObjectType::System => ALL_SYSTEM_PRIVILEGES,
1934    }
1935}
1936
1937pub const fn owner_privilege(object_type: ObjectType, owner_id: RoleId) -> MzAclItem {
1938    MzAclItem {
1939        grantee: owner_id,
1940        grantor: owner_id,
1941        acl_mode: all_object_privileges(SystemObjectType::Object(object_type)),
1942    }
1943}
1944
1945const fn default_builtin_object_acl_mode(object_type: ObjectType) -> AclMode {
1946    match object_type {
1947        ObjectType::Table
1948        | ObjectType::View
1949        | ObjectType::MaterializedView
1950        | ObjectType::Source => AclMode::SELECT,
1951        ObjectType::Type | ObjectType::Schema => AclMode::USAGE,
1952        ObjectType::Sink
1953        | ObjectType::MetricSink
1954        | ObjectType::Index
1955        | ObjectType::Role
1956        | ObjectType::Cluster
1957        | ObjectType::ClusterReplica
1958        | ObjectType::Secret
1959        | ObjectType::Connection
1960        | ObjectType::Database
1961        | ObjectType::Func
1962        | ObjectType::NetworkPolicy => AclMode::empty(),
1963    }
1964}
1965
1966pub const fn support_builtin_object_privilege(object_type: ObjectType) -> MzAclItem {
1967    let acl_mode = default_builtin_object_acl_mode(object_type);
1968    MzAclItem {
1969        grantee: MZ_SUPPORT_ROLE_ID,
1970        grantor: MZ_SYSTEM_ROLE_ID,
1971        acl_mode,
1972    }
1973}
1974
1975pub const fn default_builtin_object_privilege(object_type: ObjectType) -> MzAclItem {
1976    let acl_mode = default_builtin_object_acl_mode(object_type);
1977    MzAclItem {
1978        grantee: RoleId::Public,
1979        grantor: MZ_SYSTEM_ROLE_ID,
1980        acl_mode,
1981    }
1982}