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