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