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            set_options: _,
1141            reset_options: _,
1142        }) => {
1143            let items = iter::once(sink.from).map(|gid| catalog.resolve_item_id(&gid));
1144            let mut privileges = generate_read_privileges(catalog, items, role_id);
1145            privileges.push((
1146                SystemObjectId::Object(in_cluster.into()),
1147                AclMode::CREATE,
1148                role_id,
1149            ));
1150            RbacRequirements {
1151                ownership: vec![ObjectId::Item(*item_id)],
1152                privileges,
1153                item_usage: &CREATE_ITEM_USAGE,
1154                ..Default::default()
1155            }
1156        }
1157        Plan::AlterClusterRename(plan::AlterClusterRenamePlan {
1158            id,
1159            name: _,
1160            to_name: _,
1161        }) => RbacRequirements {
1162            ownership: vec![ObjectId::Cluster(*id)],
1163            ..Default::default()
1164        },
1165        Plan::AlterClusterSwap(plan::AlterClusterSwapPlan {
1166            id_a,
1167            id_b,
1168            name_a: _,
1169            name_b: _,
1170            name_temp: _,
1171        }) => RbacRequirements {
1172            ownership: vec![ObjectId::Cluster(*id_a), ObjectId::Cluster(*id_b)],
1173            ..Default::default()
1174        },
1175        Plan::AlterClusterReplicaRename(plan::AlterClusterReplicaRenamePlan {
1176            cluster_id,
1177            replica_id,
1178            name: _,
1179            to_name: _,
1180        }) => RbacRequirements {
1181            ownership: vec![ObjectId::ClusterReplica((*cluster_id, *replica_id))],
1182            ..Default::default()
1183        },
1184        Plan::AlterItemRename(plan::AlterItemRenamePlan {
1185            id,
1186            current_full_name: _,
1187            to_name: _,
1188            object_type: _,
1189        }) => RbacRequirements {
1190            ownership: vec![ObjectId::Item(*id)],
1191            ..Default::default()
1192        },
1193        Plan::AlterSchemaRename(plan::AlterSchemaRenamePlan {
1194            cur_schema_spec,
1195            new_schema_name: _,
1196        }) => {
1197            let privileges = match cur_schema_spec.0 {
1198                ResolvedDatabaseSpecifier::Id(db_id) => vec![(
1199                    SystemObjectId::Object(ObjectId::Database(db_id)),
1200                    AclMode::CREATE,
1201                    role_id,
1202                )],
1203                ResolvedDatabaseSpecifier::Ambient => vec![],
1204            };
1205
1206            RbacRequirements {
1207                ownership: vec![ObjectId::Schema(*cur_schema_spec)],
1208                privileges,
1209                ..Default::default()
1210            }
1211        }
1212        Plan::AlterSchemaSwap(plan::AlterSchemaSwapPlan {
1213            schema_a_spec,
1214            schema_a_name: _,
1215            schema_b_spec,
1216            schema_b_name: _,
1217            name_temp: _,
1218        }) => {
1219            let mut privileges = vec![];
1220            if let ResolvedDatabaseSpecifier::Id(id_a) = schema_a_spec.0 {
1221                privileges.push((
1222                    SystemObjectId::Object(ObjectId::Database(id_a)),
1223                    AclMode::CREATE,
1224                    role_id,
1225                ));
1226            }
1227            if let ResolvedDatabaseSpecifier::Id(id_b) = schema_b_spec.0 {
1228                privileges.push((
1229                    SystemObjectId::Object(ObjectId::Database(id_b)),
1230                    AclMode::CREATE,
1231                    role_id,
1232                ));
1233            }
1234
1235            RbacRequirements {
1236                ownership: vec![
1237                    ObjectId::Schema(*schema_a_spec),
1238                    ObjectId::Schema(*schema_b_spec),
1239                ],
1240                privileges,
1241                ..Default::default()
1242            }
1243        }
1244        Plan::AlterSecret(plan::AlterSecretPlan { id, secret_as: _ }) => RbacRequirements {
1245            ownership: vec![ObjectId::Item(*id)],
1246            item_usage: &CREATE_ITEM_USAGE,
1247            ..Default::default()
1248        },
1249        Plan::AlterRole(plan::AlterRolePlan {
1250            id,
1251            name: _,
1252            option,
1253        }) => match option {
1254            // Only superusers can alter the superuserness of a role.
1255            plan::PlannedAlterRoleOption::Attributes(attributes)
1256                if attributes.superuser.is_some() =>
1257            {
1258                RbacRequirements {
1259                    superuser_action: Some("alter superuser role".to_string()),
1260                    ..Default::default()
1261                }
1262            }
1263            // Roles are allowed to change their own password, but only if
1264            // password is the sole attribute being changed.
1265            plan::PlannedAlterRoleOption::Attributes(plan::PlannedRoleAttributes {
1266                password,
1267                // scram_iterations and nopassword are password-related, so
1268                // they're fine to change alongside the password.
1269                scram_iterations: _,
1270                nopassword: _,
1271                // superuser is already handled by the match arm above, so it
1272                // will always be None here.
1273                superuser: None,
1274                inherit: None,
1275                login: None,
1276            }) if password.is_some() && role_id == *id => RbacRequirements::default(),
1277            // But no one elses...
1278            plan::PlannedAlterRoleOption::Attributes(attributes)
1279                if attributes.password.is_some() && role_id != *id =>
1280            {
1281                RbacRequirements {
1282                    superuser_action: Some("alter password of role".to_string()),
1283                    ..Default::default()
1284                }
1285            }
1286            // restrict_to_user_objects can only be set by superuser.
1287            // SECURITY: This must use case-insensitive comparison because
1288            // var.name() comes from Ident::to_string() which preserves the
1289            // original casing for quoted identifiers.
1290            plan::PlannedAlterRoleOption::Variable(var)
1291                if var.name().eq_ignore_ascii_case("restrict_to_user_objects") =>
1292            {
1293                RbacRequirements {
1294                    superuser_action: Some("set restrict_to_user_objects".to_string()),
1295                    ..Default::default()
1296                }
1297            }
1298            // Roles are allowed to change their own other variables.
1299            plan::PlannedAlterRoleOption::Variable(_) if role_id == *id => {
1300                RbacRequirements::default()
1301            }
1302            // Otherwise to ALTER a role, you need to have the CREATE_ROLE privilege.
1303            _ => RbacRequirements {
1304                privileges: vec![(SystemObjectId::System, AclMode::CREATE_ROLE, role_id)],
1305                item_usage: &CREATE_ITEM_USAGE,
1306                ..Default::default()
1307            },
1308        },
1309        Plan::AlterOwner(plan::AlterOwnerPlan {
1310            id,
1311            object_type: _,
1312            new_owner,
1313        }) => {
1314            let privileges = match id {
1315                ObjectId::ClusterReplica((cluster_id, _)) => {
1316                    vec![(
1317                        SystemObjectId::Object(cluster_id.into()),
1318                        AclMode::CREATE,
1319                        role_id,
1320                    )]
1321                }
1322                ObjectId::Schema((database_spec, _)) => match database_spec {
1323                    ResolvedDatabaseSpecifier::Ambient => Vec::new(),
1324                    ResolvedDatabaseSpecifier::Id(database_id) => {
1325                        vec![(
1326                            SystemObjectId::Object(database_id.into()),
1327                            AclMode::CREATE,
1328                            role_id,
1329                        )]
1330                    }
1331                },
1332                ObjectId::Item(item_id) => {
1333                    let item = catalog.get_item(item_id);
1334                    vec![(
1335                        SystemObjectId::Object(item.name().qualifiers.clone().into()),
1336                        AclMode::CREATE,
1337                        role_id,
1338                    )]
1339                }
1340                ObjectId::Cluster(_)
1341                | ObjectId::Database(_)
1342                | ObjectId::Role(_)
1343                | ObjectId::NetworkPolicy(_) => Vec::new(),
1344            };
1345            RbacRequirements {
1346                role_membership: BTreeSet::from([*new_owner]),
1347                ownership: vec![id.clone()],
1348                privileges,
1349                ..Default::default()
1350            }
1351        }
1352        Plan::AlterTableAddColumn(plan::AlterTablePlan { relation_id, .. }) => RbacRequirements {
1353            ownership: vec![ObjectId::Item(*relation_id)],
1354            item_usage: &CREATE_ITEM_USAGE,
1355            ..Default::default()
1356        },
1357        Plan::AlterMaterializedViewApplyReplacement(
1358            plan::AlterMaterializedViewApplyReplacementPlan { id, replacement_id },
1359        ) => RbacRequirements {
1360            ownership: vec![ObjectId::Item(*id), ObjectId::Item(*replacement_id)],
1361            item_usage: &CREATE_ITEM_USAGE,
1362            ..Default::default()
1363        },
1364        Plan::AlterNetworkPolicy(plan::AlterNetworkPolicyPlan { id, .. }) => RbacRequirements {
1365            ownership: vec![ObjectId::NetworkPolicy(*id)],
1366            item_usage: &CREATE_ITEM_USAGE,
1367            ..Default::default()
1368        },
1369        Plan::ReadThenWrite(plan::ReadThenWritePlan {
1370            id,
1371            selection,
1372            finishing: _,
1373            assignments,
1374            kind,
1375            returning,
1376        }) => {
1377            let acl_mode = match kind {
1378                MutationKind::Insert => AclMode::INSERT,
1379                MutationKind::Update => AclMode::UPDATE,
1380                MutationKind::Delete => AclMode::DELETE,
1381            };
1382            let schema_id: ObjectId = catalog.get_item(id).name().qualifiers.clone().into();
1383            let mut privileges = vec![
1384                (
1385                    SystemObjectId::Object(schema_id.clone()),
1386                    AclMode::USAGE,
1387                    role_id,
1388                ),
1389                (SystemObjectId::Object(id.into()), acl_mode, role_id),
1390            ];
1391            let mut seen = BTreeSet::from([(schema_id, role_id)]);
1392
1393            // We don't allow arbitrary sub-queries in `assignments` or `returning`. So either they
1394            // contains a column reference to the outer table or it's constant.
1395            if assignments
1396                .values()
1397                .chain(returning.iter())
1398                .any(|assignment| assignment.contains_column())
1399            {
1400                privileges.push((SystemObjectId::Object(id.into()), AclMode::SELECT, role_id));
1401                seen.insert((id.into(), role_id));
1402            }
1403
1404            // TODO(jkosh44) It's fairly difficult to determine what part of `selection` is from a
1405            //  user specified read and what part is from the implementation of the read then write.
1406            //  instead we are overly protective and always require SELECT privileges even though
1407            //  PostgreSQL doesn't always do this.
1408            //  As a concrete example, we require SELECT and UPDATE privileges to execute
1409            //  `UPDATE t SET a = 42;`, while PostgreSQL only requires UPDATE privileges.
1410            let items = selection
1411                .depends_on()
1412                .into_iter()
1413                .map(|gid| catalog.resolve_item_id(&gid));
1414            privileges.extend_from_slice(&generate_read_privileges_inner(
1415                catalog, items, role_id, &mut seen,
1416            ));
1417
1418            if let Some(privilege) = generate_cluster_usage_privileges(
1419                selection.as_const().is_some(),
1420                target_cluster_id,
1421                role_id,
1422            ) {
1423                privileges.push(privilege);
1424            }
1425            RbacRequirements {
1426                privileges,
1427                ..Default::default()
1428            }
1429        }
1430        Plan::GrantRole(plan::GrantRolePlan {
1431            role_ids: _,
1432            member_ids: _,
1433            grantor_id: _,
1434        })
1435        | Plan::RevokeRole(plan::RevokeRolePlan {
1436            role_ids: _,
1437            member_ids: _,
1438            grantor_id: _,
1439        }) => RbacRequirements {
1440            privileges: vec![(SystemObjectId::System, AclMode::CREATE_ROLE, role_id)],
1441            ..Default::default()
1442        },
1443        Plan::GrantPrivileges(plan::GrantPrivilegesPlan {
1444            update_privileges,
1445            grantees: _,
1446        })
1447        | Plan::RevokePrivileges(plan::RevokePrivilegesPlan {
1448            update_privileges,
1449            revokees: _,
1450        }) => {
1451            let mut privileges = Vec::with_capacity(update_privileges.len());
1452            for UpdatePrivilege { target_id, .. } in update_privileges {
1453                match target_id {
1454                    SystemObjectId::Object(object_id) => match object_id {
1455                        ObjectId::ClusterReplica((cluster_id, _)) => {
1456                            privileges.push((
1457                                SystemObjectId::Object(cluster_id.into()),
1458                                AclMode::USAGE,
1459                                role_id,
1460                            ));
1461                        }
1462                        ObjectId::Schema((database_spec, _)) => match database_spec {
1463                            ResolvedDatabaseSpecifier::Ambient => {}
1464                            ResolvedDatabaseSpecifier::Id(database_id) => {
1465                                privileges.push((
1466                                    SystemObjectId::Object(database_id.into()),
1467                                    AclMode::USAGE,
1468                                    role_id,
1469                                ));
1470                            }
1471                        },
1472                        ObjectId::Item(item_id) => {
1473                            let item = catalog.get_item(item_id);
1474                            privileges.push((
1475                                SystemObjectId::Object(item.name().qualifiers.clone().into()),
1476                                AclMode::USAGE,
1477                                role_id,
1478                            ))
1479                        }
1480                        ObjectId::Cluster(_)
1481                        | ObjectId::Database(_)
1482                        | ObjectId::Role(_)
1483                        | ObjectId::NetworkPolicy(_) => {}
1484                    },
1485                    SystemObjectId::System => {}
1486                }
1487            }
1488            RbacRequirements {
1489                ownership: update_privileges
1490                    .iter()
1491                    .filter_map(|update_privilege| update_privilege.target_id.object_id())
1492                    .cloned()
1493                    .collect(),
1494                privileges,
1495                // To grant/revoke a privilege on some object, generally the grantor/revoker must be the
1496                // owner of that object (or have a grant option on that object which isn't implemented in
1497                // Materialize yet). There is no owner of the entire system, so it's only reasonable to
1498                // restrict granting/revoking system privileges to superusers.
1499                superuser_action: if update_privileges
1500                    .iter()
1501                    .any(|update_privilege| update_privilege.target_id.is_system())
1502                {
1503                    Some("GRANT/REVOKE SYSTEM PRIVILEGES".to_string())
1504                } else {
1505                    None
1506                },
1507                ..Default::default()
1508            }
1509        }
1510        Plan::AlterDefaultPrivileges(plan::AlterDefaultPrivilegesPlan {
1511            privilege_objects,
1512            privilege_acl_items: _,
1513            is_grant: _,
1514        }) => RbacRequirements {
1515            role_membership: privilege_objects
1516                .iter()
1517                .map(|privilege_object| privilege_object.role_id)
1518                .collect(),
1519            privileges: privilege_objects
1520                .into_iter()
1521                .filter_map(|privilege_object| {
1522                    if let (Some(database_id), Some(_)) =
1523                        (privilege_object.database_id, privilege_object.schema_id)
1524                    {
1525                        Some((
1526                            SystemObjectId::Object(database_id.into()),
1527                            AclMode::USAGE,
1528                            role_id,
1529                        ))
1530                    } else {
1531                        None
1532                    }
1533                })
1534                .collect(),
1535            // Altering the default privileges for the PUBLIC role (aka ALL ROLES) will affect all roles
1536            // that currently exist and roles that will exist in the future. It's impossible for an exising
1537            // role to be a member of a role that doesn't exist yet, so no current role could possibly have
1538            // the privileges required to alter default privileges for the PUBLIC role. Therefore we
1539            // only superusers can alter default privileges for the PUBLIC role.
1540            superuser_action: if privilege_objects
1541                .iter()
1542                .any(|privilege_object| privilege_object.role_id.is_public())
1543            {
1544                Some("ALTER DEFAULT PRIVILEGES FOR ALL ROLES".to_string())
1545            } else {
1546                None
1547            },
1548            ..Default::default()
1549        },
1550        Plan::ReassignOwned(plan::ReassignOwnedPlan {
1551            old_roles,
1552            new_role,
1553            reassign_ids: _,
1554        }) => RbacRequirements {
1555            role_membership: old_roles
1556                .into_iter()
1557                .cloned()
1558                .chain(iter::once(*new_role))
1559                .collect(),
1560            ..Default::default()
1561        },
1562        Plan::SideEffectingFunc(func) => {
1563            let role_membership = match func {
1564                // A `NULL` argument cancels no connection (the function returns
1565                // `NULL`), so there is no role membership to require.
1566                SideEffectingFunc::PgCancelBackend {
1567                    connection_id: None,
1568                } => BTreeSet::new(),
1569                SideEffectingFunc::PgCancelBackend {
1570                    connection_id: Some(_),
1571                } => target_conn_role.map(|x| [x].into()).unwrap_or_default(),
1572            };
1573            RbacRequirements {
1574                role_membership,
1575                ..Default::default()
1576            }
1577        }
1578        Plan::ValidateConnection(plan::ValidateConnectionPlan { id, connection: _ }) => {
1579            let schema_id: ObjectId = catalog.get_item(id).name().qualifiers.clone().into();
1580            RbacRequirements {
1581                privileges: vec![
1582                    (SystemObjectId::Object(schema_id), AclMode::USAGE, role_id),
1583                    (SystemObjectId::Object(id.into()), AclMode::USAGE, role_id),
1584                ],
1585                ..Default::default()
1586            }
1587        }
1588        Plan::DiscardTemp
1589        | Plan::DiscardAll
1590        | Plan::EmptyQuery
1591        | Plan::ShowAllVariables
1592        | Plan::ShowVariable(plan::ShowVariablePlan { name: _ })
1593        | Plan::InspectShard(plan::InspectShardPlan { id: _ })
1594        | Plan::SetVariable(plan::SetVariablePlan {
1595            name: _,
1596            value: _,
1597            local: _,
1598        })
1599        | Plan::ResetVariable(plan::ResetVariablePlan { name: _ })
1600        | Plan::SetTransaction(plan::SetTransactionPlan { local: _, modes: _ })
1601        | Plan::StartTransaction(plan::StartTransactionPlan {
1602            access: _,
1603            isolation_level: _,
1604        })
1605        | Plan::CommitTransaction(plan::CommitTransactionPlan {
1606            transaction_type: _,
1607        })
1608        | Plan::AbortTransaction(plan::AbortTransactionPlan {
1609            transaction_type: _,
1610        })
1611        | Plan::AlterNoop(plan::AlterNoopPlan { object_type: _ })
1612        | Plan::AlterSystemSet(plan::AlterSystemSetPlan { name: _, value: _ })
1613        | Plan::AlterSystemReset(plan::AlterSystemResetPlan { name: _ })
1614        | Plan::AlterSystemResetAll(plan::AlterSystemResetAllPlan {})
1615        | Plan::Declare(plan::DeclarePlan {
1616            name: _,
1617            stmt: _,
1618            sql: _,
1619            params: _,
1620        })
1621        | Plan::Fetch(plan::FetchPlan {
1622            name: _,
1623            count: _,
1624            timeout: _,
1625        })
1626        | Plan::Close(plan::ClosePlan { name: _ })
1627        | Plan::Prepare(plan::PreparePlan {
1628            name: _,
1629            stmt: _,
1630            desc: _,
1631            sql: _,
1632        })
1633        | Plan::Execute(plan::ExecutePlan { name: _, params: _ })
1634        | Plan::Deallocate(plan::DeallocatePlan { name: _ })
1635        | Plan::Raise(plan::RaisePlan { severity: _ }) => Default::default(),
1636    }
1637}
1638
1639/// Reports whether any role has ownership over an object.
1640fn check_owner_roles(
1641    object_id: &ObjectId,
1642    role_ids: &BTreeSet<RoleId>,
1643    catalog: &impl SessionCatalog,
1644) -> bool {
1645    if let Some(owner_id) = catalog.get_owner_id(object_id) {
1646        role_ids.contains(&owner_id)
1647    } else {
1648        true
1649    }
1650}
1651
1652fn ownership_err(
1653    unheld_ownership: Vec<ObjectId>,
1654    catalog: &impl SessionCatalog,
1655) -> Result<(), UnauthorizedError> {
1656    if !unheld_ownership.is_empty() {
1657        let objects = unheld_ownership
1658            .into_iter()
1659            .map(|ownership| match ownership {
1660                ObjectId::Cluster(id) => (
1661                    ObjectType::Cluster,
1662                    catalog.get_cluster(id).name().to_string(),
1663                ),
1664                ObjectId::ClusterReplica((cluster_id, replica_id)) => {
1665                    let cluster = catalog.get_cluster(cluster_id);
1666                    let replica = catalog.get_cluster_replica(cluster_id, replica_id);
1667                    // Note: using unchecked here is okay because the values are coming from an
1668                    // already existing name.
1669                    let name = QualifiedReplica {
1670                        cluster: Ident::new_unchecked(cluster.name()),
1671                        replica: Ident::new_unchecked(replica.name()),
1672                    };
1673                    (ObjectType::ClusterReplica, name.to_string())
1674                }
1675                ObjectId::Database(id) => (
1676                    ObjectType::Database,
1677                    catalog.get_database(&id).name().to_string(),
1678                ),
1679                ObjectId::Schema((database_spec, schema_spec)) => {
1680                    let schema = catalog.get_schema(&database_spec, &schema_spec);
1681                    let name = catalog.resolve_full_schema_name(schema.name());
1682                    (ObjectType::Schema, name.to_string())
1683                }
1684                ObjectId::Item(id) => {
1685                    let item = catalog.get_item(&id);
1686                    let name = catalog.resolve_full_name(item.name());
1687                    (item.item_type().into(), name.to_string())
1688                }
1689                ObjectId::NetworkPolicy(id) => (
1690                    ObjectType::NetworkPolicy,
1691                    catalog.get_network_policy(&id).name().to_string(),
1692                ),
1693                ObjectId::Role(_) => unreachable!("roles have no owner"),
1694            })
1695            .collect();
1696        Err(UnauthorizedError::Ownership { objects })
1697    } else {
1698        Ok(())
1699    }
1700}
1701
1702fn generate_required_source_privileges(
1703    name: &QualifiedItemName,
1704    data_source: &DataSourceDesc,
1705    in_cluster: Option<ClusterId>,
1706    role_id: RoleId,
1707) -> Vec<(SystemObjectId, AclMode, RoleId)> {
1708    let mut privileges = vec![(
1709        SystemObjectId::Object(name.qualifiers.clone().into()),
1710        AclMode::CREATE,
1711        role_id,
1712    )];
1713    match (data_source, in_cluster) {
1714        (_, Some(id)) => {
1715            privileges.push((SystemObjectId::Object(id.into()), AclMode::CREATE, role_id))
1716        }
1717        (DataSourceDesc::Ingestion(_), None) => {
1718            privileges.push((SystemObjectId::System, AclMode::CREATE_CLUSTER, role_id))
1719        }
1720        // Non-ingestion data-sources have meaningless cluster config's (for now...) and they need
1721        // to be ignored.
1722        // This feels very brittle, but there's not much we can do until the UNDEFINED cluster
1723        // config is removed.
1724        (_, None) => {}
1725    }
1726    privileges
1727}
1728
1729/// Generates all the privileges required to execute a read that includes the objects in `ids`.
1730///
1731/// Not only do we need to validate that `role_id` has read privileges on all relations in `ids`,
1732/// but if any object is a view or materialized view then we need to validate that the owner of
1733/// that view has all of the privileges required to execute the query within the view.
1734///
1735/// For more details see: <https://www.postgresql.org/docs/15/rules-privileges.html>
1736fn generate_read_privileges(
1737    catalog: &impl SessionCatalog,
1738    ids: impl Iterator<Item = CatalogItemId>,
1739    role_id: RoleId,
1740) -> Vec<(SystemObjectId, AclMode, RoleId)> {
1741    generate_read_privileges_inner(catalog, ids, role_id, &mut BTreeSet::new())
1742}
1743
1744fn generate_read_privileges_inner(
1745    catalog: &impl SessionCatalog,
1746    ids: impl Iterator<Item = CatalogItemId>,
1747    role_id: RoleId,
1748    seen: &mut BTreeSet<(ObjectId, RoleId)>,
1749) -> Vec<(SystemObjectId, AclMode, RoleId)> {
1750    let mut privileges = Vec::new();
1751
1752    // Iterative worklist traversal rather than recursion. View dependency
1753    // chains are user controlled and can be arbitrarily deep.
1754    let mut queue: VecDeque<(CatalogItemId, RoleId)> = ids.map(|id| (id, role_id)).collect();
1755    while let Some((id, role_id)) = queue.pop_front() {
1756        if seen.insert((id.into(), role_id)) {
1757            let item = catalog.get_item(&id);
1758            let schema_id: ObjectId = item.name().qualifiers.clone().into();
1759            if seen.insert((schema_id.clone(), role_id)) {
1760                privileges.push((SystemObjectId::Object(schema_id), AclMode::USAGE, role_id))
1761            }
1762            match item.item_type() {
1763                CatalogItemType::View | CatalogItemType::MaterializedView => {
1764                    privileges.push((SystemObjectId::Object(id.into()), AclMode::SELECT, role_id));
1765                    let view_owner = item.owner_id();
1766                    queue.extend(item.references().items().map(|id| (*id, view_owner)));
1767                }
1768                CatalogItemType::Table | CatalogItemType::Source => {
1769                    privileges.push((SystemObjectId::Object(id.into()), AclMode::SELECT, role_id));
1770                }
1771                CatalogItemType::Type | CatalogItemType::Secret | CatalogItemType::Connection => {
1772                    privileges.push((SystemObjectId::Object(id.into()), AclMode::USAGE, role_id));
1773                }
1774                CatalogItemType::Sink | CatalogItemType::Index | CatalogItemType::Func => {}
1775            }
1776        }
1777    }
1778
1779    privileges
1780}
1781
1782fn generate_usage_privileges(
1783    catalog: &impl SessionCatalog,
1784    ids: &ResolvedIds,
1785    role_id: RoleId,
1786    item_types: &BTreeSet<CatalogItemType>,
1787) -> BTreeSet<(SystemObjectId, AclMode, RoleId)> {
1788    // Use a `BTreeSet` to remove duplicate privileges.
1789    ids.items()
1790        .filter_map(move |id| {
1791            let item = catalog.get_item(id);
1792            if item_types.contains(&item.item_type()) {
1793                let schema_id = item.name().qualifiers.clone().into();
1794                Some([
1795                    (SystemObjectId::Object(schema_id), AclMode::USAGE, role_id),
1796                    (SystemObjectId::Object(id.into()), AclMode::USAGE, role_id),
1797                ])
1798            } else {
1799                None
1800            }
1801        })
1802        .flatten()
1803        .collect()
1804}
1805
1806fn generate_cluster_usage_privileges(
1807    expr_is_const: bool,
1808    target_cluster_id: Option<ClusterId>,
1809    role_id: RoleId,
1810) -> Option<(SystemObjectId, AclMode, RoleId)> {
1811    // TODO(jkosh44) expr hasn't been fully optimized yet, so it might actually be a constant,
1812    //  but we mistakenly think that it's not. For now it's ok to be overly protective.
1813    if !expr_is_const {
1814        if let Some(cluster_id) = target_cluster_id {
1815            return Some((
1816                SystemObjectId::Object(cluster_id.into()),
1817                AclMode::USAGE,
1818                role_id,
1819            ));
1820        }
1821    }
1822
1823    None
1824}
1825
1826fn check_object_privileges(
1827    catalog: &impl SessionCatalog,
1828    privileges: Vec<(SystemObjectId, AclMode, RoleId)>,
1829    role_membership: BTreeSet<RoleId>,
1830    current_role_id: RoleId,
1831) -> Result<(), UnauthorizedError> {
1832    let mut role_memberships: BTreeMap<RoleId, BTreeSet<RoleId>> = BTreeMap::new();
1833    role_memberships.insert(current_role_id, role_membership);
1834    for (object_id, acl_mode, role_id) in privileges {
1835        // Temporary schemas are owned by the connection that created them,
1836        // so users implicitly have all privileges on their own temp schema.
1837        // The schema may not exist yet (lazy creation), so we skip the check.
1838        if matches!(
1839            &object_id,
1840            SystemObjectId::Object(ObjectId::Schema((_, SchemaSpecifier::Temporary)))
1841        ) {
1842            continue;
1843        }
1844
1845        let role_membership = role_memberships
1846            .entry(role_id)
1847            .or_insert_with_key(|role_id| catalog.collect_role_membership(role_id));
1848        let object_privileges = catalog
1849            .get_privileges(&object_id)
1850            .expect("only object types with privileges will generate required privileges");
1851        let role_privileges = role_membership
1852            .iter()
1853            .flat_map(|role_id| object_privileges.get_acl_items_for_grantee(role_id))
1854            .map(|mz_acl_item| mz_acl_item.acl_mode)
1855            .fold(AclMode::empty(), |accum, acl_mode| accum.union(acl_mode));
1856        if !role_privileges.contains(acl_mode) {
1857            let role_name = catalog.get_role(&role_id).name().to_string();
1858            let privileges = acl_mode.to_error_string();
1859            return Err(UnauthorizedError::Privilege {
1860                object_description: ErrorMessageObjectDescription::from_sys_id(&object_id, catalog),
1861                role_name,
1862                privileges,
1863            });
1864        }
1865    }
1866
1867    Ok(())
1868}
1869
1870pub const fn all_object_privileges(object_type: SystemObjectType) -> AclMode {
1871    const TABLE_ACL_MODE: AclMode = AclMode::INSERT
1872        .union(AclMode::SELECT)
1873        .union(AclMode::UPDATE)
1874        .union(AclMode::DELETE);
1875    const USAGE_CREATE_ACL_MODE: AclMode = AclMode::USAGE.union(AclMode::CREATE);
1876    const ALL_SYSTEM_PRIVILEGES: AclMode = AclMode::CREATE_ROLE
1877        .union(AclMode::CREATE_DB)
1878        .union(AclMode::CREATE_CLUSTER)
1879        .union(AclMode::CREATE_NETWORK_POLICY);
1880
1881    const EMPTY_ACL_MODE: AclMode = AclMode::empty();
1882    match object_type {
1883        SystemObjectType::Object(ObjectType::Table) => TABLE_ACL_MODE,
1884        SystemObjectType::Object(ObjectType::View) => AclMode::SELECT,
1885        SystemObjectType::Object(ObjectType::MaterializedView) => AclMode::SELECT,
1886        SystemObjectType::Object(ObjectType::Source) => AclMode::SELECT,
1887        SystemObjectType::Object(ObjectType::Sink) => EMPTY_ACL_MODE,
1888        SystemObjectType::Object(ObjectType::Index) => EMPTY_ACL_MODE,
1889        SystemObjectType::Object(ObjectType::Type) => AclMode::USAGE,
1890        SystemObjectType::Object(ObjectType::Role) => EMPTY_ACL_MODE,
1891        SystemObjectType::Object(ObjectType::Cluster) => USAGE_CREATE_ACL_MODE,
1892        SystemObjectType::Object(ObjectType::ClusterReplica) => EMPTY_ACL_MODE,
1893        SystemObjectType::Object(ObjectType::Secret) => AclMode::USAGE,
1894        SystemObjectType::Object(ObjectType::NetworkPolicy) => AclMode::USAGE,
1895        SystemObjectType::Object(ObjectType::Connection) => AclMode::USAGE,
1896        SystemObjectType::Object(ObjectType::Database) => USAGE_CREATE_ACL_MODE,
1897        SystemObjectType::Object(ObjectType::Schema) => USAGE_CREATE_ACL_MODE,
1898        SystemObjectType::Object(ObjectType::Func) => EMPTY_ACL_MODE,
1899        SystemObjectType::System => ALL_SYSTEM_PRIVILEGES,
1900    }
1901}
1902
1903pub const fn owner_privilege(object_type: ObjectType, owner_id: RoleId) -> MzAclItem {
1904    MzAclItem {
1905        grantee: owner_id,
1906        grantor: owner_id,
1907        acl_mode: all_object_privileges(SystemObjectType::Object(object_type)),
1908    }
1909}
1910
1911const fn default_builtin_object_acl_mode(object_type: ObjectType) -> AclMode {
1912    match object_type {
1913        ObjectType::Table
1914        | ObjectType::View
1915        | ObjectType::MaterializedView
1916        | ObjectType::Source => AclMode::SELECT,
1917        ObjectType::Type | ObjectType::Schema => AclMode::USAGE,
1918        ObjectType::Sink
1919        | ObjectType::Index
1920        | ObjectType::Role
1921        | ObjectType::Cluster
1922        | ObjectType::ClusterReplica
1923        | ObjectType::Secret
1924        | ObjectType::Connection
1925        | ObjectType::Database
1926        | ObjectType::Func
1927        | ObjectType::NetworkPolicy => AclMode::empty(),
1928    }
1929}
1930
1931pub const fn support_builtin_object_privilege(object_type: ObjectType) -> MzAclItem {
1932    let acl_mode = default_builtin_object_acl_mode(object_type);
1933    MzAclItem {
1934        grantee: MZ_SUPPORT_ROLE_ID,
1935        grantor: MZ_SYSTEM_ROLE_ID,
1936        acl_mode,
1937    }
1938}
1939
1940pub const fn default_builtin_object_privilege(object_type: ObjectType) -> MzAclItem {
1941    let acl_mode = default_builtin_object_acl_mode(object_type);
1942    MzAclItem {
1943        grantee: RoleId::Public,
1944        grantor: MZ_SYSTEM_ROLE_ID,
1945        acl_mode,
1946    }
1947}