Skip to main content

mz_sql/plan/statement/
acl.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
10//! Access control list (ACL).
11//!
12//! This module houses the handlers for statements that modify privileges in the catalog, like
13//! `GRANT`, `REVOKE`, and `REASSIGN OWNED`.
14
15use std::collections::BTreeSet;
16
17use itertools::Itertools;
18use mz_sql_parser::ast::display::AstDisplay;
19
20use crate::ast::{Ident, UnresolvedDatabaseName};
21use crate::catalog::{
22    DefaultPrivilegeAclItem, DefaultPrivilegeObject, ErrorMessageObjectDescription, ObjectType,
23    SystemObjectType,
24};
25use crate::names::{
26    Aug, ObjectId, ResolvedDatabaseSpecifier, ResolvedRoleName, SchemaSpecifier, SystemObjectId,
27};
28use crate::plan::error::PlanError;
29use crate::plan::statement::ddl::{
30    resolve_cluster, resolve_database, resolve_item_or_type, resolve_network_policy, resolve_schema,
31};
32use crate::plan::statement::{StatementContext, StatementDesc};
33use crate::plan::{
34    AlterDefaultPrivilegesPlan, AlterNoopPlan, AlterOwnerPlan, GrantPrivilegesPlan, GrantRolePlan,
35    Plan, PlanNotice, ReassignOwnedPlan, RevokePrivilegesPlan, RevokeRolePlan, UpdatePrivilege,
36};
37use crate::session::user::SYSTEM_USER;
38use mz_ore::str::StrExt;
39use mz_repr::adt::mz_acl_item::AclMode;
40use mz_repr::role_id::RoleId;
41use mz_sql_parser::ast::{
42    AbbreviatedGrantOrRevokeStatement, AlterDefaultPrivilegesStatement, AlterOwnerStatement,
43    GrantPrivilegesStatement, GrantRoleStatement, GrantTargetAllSpecification,
44    GrantTargetSpecification, GrantTargetSpecificationInner, Privilege, PrivilegeSpecification,
45    ReassignOwnedStatement, RevokePrivilegesStatement, RevokeRoleStatement,
46    TargetRoleSpecification, UnresolvedItemName, UnresolvedObjectName, UnresolvedSchemaName,
47};
48
49pub fn describe_alter_owner(
50    _: &StatementContext,
51    _: AlterOwnerStatement<Aug>,
52) -> Result<StatementDesc, PlanError> {
53    Ok(StatementDesc::new(None))
54}
55
56pub fn plan_alter_owner(
57    scx: &StatementContext,
58    AlterOwnerStatement {
59        object_type,
60        if_exists,
61        name,
62        new_owner,
63    }: AlterOwnerStatement<Aug>,
64) -> Result<Plan, PlanError> {
65    let object_type = object_type.into();
66    match (object_type, name) {
67        (ObjectType::Cluster, UnresolvedObjectName::Cluster(name)) => {
68            plan_alter_cluster_owner(scx, if_exists, name, new_owner.id)
69        }
70        (ObjectType::ClusterReplica, UnresolvedObjectName::ClusterReplica(_)) => {
71            bail_never_supported!("altering the owner of a cluster replica");
72        }
73        (ObjectType::Database, UnresolvedObjectName::Database(name)) => {
74            plan_alter_database_owner(scx, if_exists, name, new_owner.id)
75        }
76        (ObjectType::Schema, UnresolvedObjectName::Schema(name)) => {
77            plan_alter_schema_owner(scx, if_exists, name, new_owner.id)
78        }
79        (ObjectType::NetworkPolicy, UnresolvedObjectName::NetworkPolicy(name)) => {
80            plan_alter_network_policy_owner(scx, if_exists, name, new_owner.id)
81        }
82        // The parser should have rejected this.
83        (ObjectType::Role, UnresolvedObjectName::Role(_)) => {
84            bail_internal!("cannot ALTER OWNER of a role")
85        }
86        (
87            object_type @ ObjectType::Cluster
88            | object_type @ ObjectType::ClusterReplica
89            | object_type @ ObjectType::Database
90            | object_type @ ObjectType::Schema
91            | object_type @ ObjectType::Role,
92            name,
93        )
94        | (
95            object_type,
96            name @ UnresolvedObjectName::Cluster(_)
97            | name @ UnresolvedObjectName::ClusterReplica(_)
98            | name @ UnresolvedObjectName::Database(_)
99            | name @ UnresolvedObjectName::Schema(_)
100            | name @ UnresolvedObjectName::NetworkPolicy(_)
101            | name @ UnresolvedObjectName::Role(_),
102        ) => {
103            // The parser should not have produced this combination.
104            bail_internal!("invalid object type '{object_type}' for ALTER OWNER with name {name}")
105        }
106        (object_type, UnresolvedObjectName::Item(name)) => {
107            plan_alter_item_owner(scx, object_type, if_exists, name, new_owner.id)
108        }
109    }
110}
111
112fn plan_alter_cluster_owner(
113    scx: &StatementContext,
114    if_exists: bool,
115    name: Ident,
116    new_owner: RoleId,
117) -> Result<Plan, PlanError> {
118    match resolve_cluster(scx, &name, if_exists)? {
119        Some(cluster) => Ok(Plan::AlterOwner(AlterOwnerPlan {
120            id: ObjectId::Cluster(cluster.id()),
121            object_type: ObjectType::Cluster,
122            new_owner,
123        })),
124        None => {
125            scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
126                name: name.to_ast_string_simple(),
127                object_type: ObjectType::Cluster,
128            });
129            Ok(Plan::AlterNoop(AlterNoopPlan {
130                object_type: ObjectType::Cluster,
131            }))
132        }
133    }
134}
135
136fn plan_alter_database_owner(
137    scx: &StatementContext,
138    if_exists: bool,
139    name: UnresolvedDatabaseName,
140    new_owner: RoleId,
141) -> Result<Plan, PlanError> {
142    match resolve_database(scx, &name, if_exists)? {
143        Some(database) => Ok(Plan::AlterOwner(AlterOwnerPlan {
144            id: ObjectId::Database(database.id()),
145            object_type: ObjectType::Database,
146            new_owner,
147        })),
148        None => {
149            scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
150                name: name.to_ast_string_simple(),
151                object_type: ObjectType::Database,
152            });
153
154            Ok(Plan::AlterNoop(AlterNoopPlan {
155                object_type: ObjectType::Database,
156            }))
157        }
158    }
159}
160
161fn plan_alter_schema_owner(
162    scx: &StatementContext,
163    if_exists: bool,
164    name: UnresolvedSchemaName,
165    new_owner: RoleId,
166) -> Result<Plan, PlanError> {
167    // Special case for mz_temp: with lazy temporary schema creation, the temp
168    // schema may not exist yet, but we still need to return the correct error.
169    // Check the schema name directly against MZ_TEMP_SCHEMA.
170    let normalized = crate::normalize::unresolved_schema_name(name.clone())?;
171    if normalized.database.is_none() && normalized.schema == mz_repr::namespaces::MZ_TEMP_SCHEMA {
172        sql_bail!("cannot alter schema {name} because it is a temporary schema",)
173    }
174
175    match resolve_schema(scx, name.clone(), if_exists)? {
176        Some((database_spec, schema_spec)) => {
177            if let ResolvedDatabaseSpecifier::Ambient = database_spec {
178                sql_bail!(
179                    "cannot alter schema {name} because it is required by the database system",
180                );
181            }
182            if let SchemaSpecifier::Temporary = schema_spec {
183                sql_bail!("cannot alter schema {name} because it is a temporary schema",)
184            }
185            Ok(Plan::AlterOwner(AlterOwnerPlan {
186                id: ObjectId::Schema((database_spec, schema_spec)),
187                object_type: ObjectType::Schema,
188                new_owner,
189            }))
190        }
191        None => {
192            scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
193                name: name.to_ast_string_simple(),
194                object_type: ObjectType::Schema,
195            });
196
197            Ok(Plan::AlterNoop(AlterNoopPlan {
198                object_type: ObjectType::Schema,
199            }))
200        }
201    }
202}
203
204fn plan_alter_item_owner(
205    scx: &StatementContext,
206    object_type: ObjectType,
207    if_exists: bool,
208    name: UnresolvedItemName,
209    new_owner: RoleId,
210) -> Result<Plan, PlanError> {
211    let resolved = match resolve_item_or_type(scx, object_type, name.clone(), if_exists) {
212        Ok(r) => r,
213        // Return a more helpful error on `DROP VIEW <materialized-view>`.
214        Err(PlanError::MismatchedObjectType {
215            name,
216            is_type: ObjectType::MaterializedView,
217            expected_type: ObjectType::View,
218        }) => {
219            return Err(PlanError::AlterViewOnMaterializedView(name.to_string()));
220        }
221        e => e?,
222    };
223
224    match resolved {
225        Some(item) => {
226            if item.id().is_system() {
227                sql_bail!(
228                    "cannot alter item {} because it is required by the database system",
229                    scx.catalog.resolve_full_name(item.name()),
230                );
231            }
232
233            Ok(Plan::AlterOwner(AlterOwnerPlan {
234                id: ObjectId::Item(item.id()),
235                object_type,
236                new_owner,
237            }))
238        }
239        None => {
240            scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
241                name: name.to_ast_string_simple(),
242                object_type,
243            });
244
245            Ok(Plan::AlterNoop(AlterNoopPlan { object_type }))
246        }
247    }
248}
249
250fn plan_alter_network_policy_owner(
251    scx: &StatementContext,
252    if_exists: bool,
253    name: Ident,
254    new_owner: RoleId,
255) -> Result<Plan, PlanError> {
256    match resolve_network_policy(scx, name.clone(), if_exists)? {
257        Some(policy_id) => Ok(Plan::AlterOwner(AlterOwnerPlan {
258            id: ObjectId::NetworkPolicy(policy_id.id),
259            object_type: ObjectType::NetworkPolicy,
260            new_owner,
261        })),
262        None => {
263            scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
264                name: name.to_ast_string_simple(),
265                object_type: ObjectType::NetworkPolicy,
266            });
267
268            Ok(Plan::AlterNoop(AlterNoopPlan {
269                object_type: ObjectType::NetworkPolicy,
270            }))
271        }
272    }
273}
274
275pub fn describe_grant_role(
276    _: &StatementContext,
277    _: GrantRoleStatement<Aug>,
278) -> Result<StatementDesc, PlanError> {
279    Ok(StatementDesc::new(None))
280}
281
282pub fn plan_grant_role(
283    scx: &StatementContext,
284    GrantRoleStatement {
285        role_names,
286        member_names,
287    }: GrantRoleStatement<Aug>,
288) -> Result<Plan, PlanError> {
289    // In PostgreSQL, the grantor must either be a role with ADMIN OPTION on the role being granted,
290    // or the bootstrap superuser. We do not have ADMIN OPTION implemented and 'mz_system' is our
291    // equivalent of the bootstrap superuser. Therefore the grantor is always 'mz_system'.
292    // For more details see:
293    // https://github.com/postgres/postgres/blob/064eb89e83ea0f59426c92906329f1e6c423dfa4/src/backend/commands/user.c#L2180-L2238
294    let grantor_id = scx
295        .catalog
296        .resolve_role(&SYSTEM_USER.name)
297        .expect("system user must exist")
298        .id();
299    Ok(Plan::GrantRole(GrantRolePlan {
300        role_ids: role_names
301            .into_iter()
302            .map(|role_name| role_name.id)
303            .collect(),
304        member_ids: member_names
305            .into_iter()
306            .map(|member_name| member_name.id)
307            .collect(),
308        grantor_id,
309    }))
310}
311
312pub fn describe_revoke_role(
313    _: &StatementContext,
314    _: RevokeRoleStatement<Aug>,
315) -> Result<StatementDesc, PlanError> {
316    Ok(StatementDesc::new(None))
317}
318
319pub fn plan_revoke_role(
320    scx: &StatementContext,
321    RevokeRoleStatement {
322        role_names,
323        member_names,
324    }: RevokeRoleStatement<Aug>,
325) -> Result<Plan, PlanError> {
326    // In PostgreSQL, the same role membership can be granted multiple times by different grantors.
327    // When revoking a role membership, only the membership granted by the specified grantor is
328    // revoked. The grantor must either be a role with ADMIN OPTION on the role being granted,
329    // or the bootstrap superuser. We do not have ADMIN OPTION implemented and 'mz_system' is our
330    // equivalent of the bootstrap superuser. Therefore the grantor is always 'mz_system'.
331    // For more details see:
332    // https://github.com/postgres/postgres/blob/064eb89e83ea0f59426c92906329f1e6c423dfa4/src/backend/commands/user.c#L2180-L2238
333    let grantor_id = scx
334        .catalog
335        .resolve_role(&SYSTEM_USER.name)
336        .expect("system user must exist")
337        .id();
338    Ok(Plan::RevokeRole(RevokeRolePlan {
339        role_ids: role_names
340            .into_iter()
341            .map(|role_name| role_name.id)
342            .collect(),
343        member_ids: member_names
344            .into_iter()
345            .map(|member_name| member_name.id)
346            .collect(),
347        grantor_id,
348    }))
349}
350
351pub fn describe_grant_privileges(
352    _: &StatementContext,
353    _: GrantPrivilegesStatement<Aug>,
354) -> Result<StatementDesc, PlanError> {
355    Ok(StatementDesc::new(None))
356}
357
358pub fn plan_grant_privileges(
359    scx: &StatementContext,
360    GrantPrivilegesStatement {
361        privileges,
362        target,
363        roles,
364    }: GrantPrivilegesStatement<Aug>,
365) -> Result<Plan, PlanError> {
366    let plan = plan_update_privilege(scx, privileges, target, roles)?;
367    Ok(Plan::GrantPrivileges(plan.into()))
368}
369
370pub fn describe_revoke_privileges(
371    _: &StatementContext,
372    _: RevokePrivilegesStatement<Aug>,
373) -> Result<StatementDesc, PlanError> {
374    Ok(StatementDesc::new(None))
375}
376
377pub fn plan_revoke_privileges(
378    scx: &StatementContext,
379    RevokePrivilegesStatement {
380        privileges,
381        target,
382        roles,
383    }: RevokePrivilegesStatement<Aug>,
384) -> Result<Plan, PlanError> {
385    let plan = plan_update_privilege(scx, privileges, target, roles)?;
386    Ok(Plan::RevokePrivileges(plan.into()))
387}
388
389struct UpdatePrivilegesPlan {
390    update_privileges: Vec<UpdatePrivilege>,
391    grantees: Vec<RoleId>,
392}
393
394impl From<UpdatePrivilegesPlan> for GrantPrivilegesPlan {
395    fn from(
396        UpdatePrivilegesPlan {
397            update_privileges,
398            grantees,
399        }: UpdatePrivilegesPlan,
400    ) -> GrantPrivilegesPlan {
401        GrantPrivilegesPlan {
402            update_privileges,
403            grantees,
404        }
405    }
406}
407
408impl From<UpdatePrivilegesPlan> for RevokePrivilegesPlan {
409    fn from(
410        UpdatePrivilegesPlan {
411            update_privileges,
412            grantees,
413        }: UpdatePrivilegesPlan,
414    ) -> RevokePrivilegesPlan {
415        RevokePrivilegesPlan {
416            update_privileges,
417            revokees: grantees,
418        }
419    }
420}
421
422fn plan_update_privilege(
423    scx: &StatementContext,
424    privileges: PrivilegeSpecification,
425    target: GrantTargetSpecification<Aug>,
426    roles: Vec<ResolvedRoleName>,
427) -> Result<UpdatePrivilegesPlan, PlanError> {
428    let (object_type, target_ids) = match target {
429        GrantTargetSpecification::Object {
430            object_type,
431            object_spec_inner,
432        } => {
433            fn object_type_filter(
434                object_id: &ObjectId,
435                object_type: &ObjectType,
436                scx: &StatementContext,
437            ) -> bool {
438                if object_type == &ObjectType::Table {
439                    scx.get_object_type(object_id).is_relation()
440                } else {
441                    object_type == &scx.get_object_type(object_id)
442                }
443            }
444            let object_type = object_type.into();
445            let object_ids: Vec<ObjectId> = match object_spec_inner {
446                GrantTargetSpecificationInner::All(GrantTargetAllSpecification::All) => {
447                    let cluster_ids = scx
448                        .catalog
449                        .get_clusters()
450                        .into_iter()
451                        .map(|cluster| cluster.id().into());
452                    let database_ids = scx
453                        .catalog
454                        .get_databases()
455                        .into_iter()
456                        .map(|database| database.id().into());
457                    let schema_ids = scx
458                        .catalog
459                        .get_schemas()
460                        .into_iter()
461                        .filter(|schema| !schema.id().is_temporary())
462                        .map(|schema| (schema.database().clone(), schema.id().clone()).into());
463                    let item_ids = scx
464                        .catalog
465                        .get_items()
466                        .into_iter()
467                        .map(|item| item.id().into());
468                    let network_policy_ids = scx
469                        .catalog
470                        .get_network_policies()
471                        .into_iter()
472                        .map(|network_policy| ObjectId::NetworkPolicy(network_policy.id()));
473                    cluster_ids
474                        .chain(database_ids)
475                        .chain(schema_ids)
476                        .chain(item_ids)
477                        .chain(network_policy_ids)
478                        .filter(|object_id| object_type_filter(object_id, &object_type, scx))
479                        .filter(|object_id| object_id.is_user())
480                        .collect()
481                }
482                GrantTargetSpecificationInner::All(GrantTargetAllSpecification::AllDatabases {
483                    databases,
484                }) => {
485                    let schema_ids = databases
486                        .iter()
487                        .map(|database| scx.get_database(database.database_id()))
488                        .flat_map(|database| database.schemas().into_iter())
489                        .filter(|schema| !schema.id().is_temporary())
490                        .map(|schema| (schema.database().clone(), schema.id().clone()).into());
491
492                    let item_ids = databases
493                        .iter()
494                        .map(|database| scx.get_database(database.database_id()))
495                        .flat_map(|database| database.schemas().into_iter())
496                        .flat_map(|schema| schema.item_ids())
497                        .map(|item_id| item_id.into());
498
499                    item_ids
500                        .chain(schema_ids)
501                        .filter(|object_id| object_type_filter(object_id, &object_type, scx))
502                        .collect()
503                }
504                GrantTargetSpecificationInner::All(GrantTargetAllSpecification::AllSchemas {
505                    schemas,
506                }) => schemas
507                    .into_iter()
508                    .map(|schema| scx.get_schema(schema.database_spec(), schema.schema_spec()))
509                    .flat_map(|schema| schema.item_ids())
510                    .map(|item_id| item_id.into())
511                    .filter(|object_id| object_type_filter(object_id, &object_type, scx))
512                    .collect(),
513                GrantTargetSpecificationInner::Objects { names } => {
514                    let mut ids = Vec::with_capacity(names.len());
515                    for name in names {
516                        ids.push(
517                            // Name resolution should have rejected invalid objects.
518                            name.try_into()
519                                .map_err(|e| internal_err!("invalid object name: {}", e))?,
520                        );
521                    }
522                    ids
523                }
524            };
525            let target_ids = object_ids.into_iter().map(|id| id.into()).collect();
526            (SystemObjectType::Object(object_type), target_ids)
527        }
528        GrantTargetSpecification::System => {
529            (SystemObjectType::System, vec![SystemObjectId::System])
530        }
531    };
532
533    let mut update_privileges = Vec::with_capacity(target_ids.len());
534
535    for target_id in target_ids {
536        // Temporary schemas cannot have privileges granted or revoked - they are
537        // connection-specific and transient. With lazy temporary schema creation,
538        // the temp schema may not exist yet, but we still need to return the correct error.
539        if let SystemObjectId::Object(ObjectId::Schema((_, SchemaSpecifier::Temporary))) =
540            &target_id
541        {
542            sql_bail!(
543                "cannot grant or revoke privileges on schema {} because it is a temporary schema",
544                mz_repr::namespaces::MZ_TEMP_SCHEMA
545            );
546        }
547
548        // The actual type of the object.
549        let actual_object_type = scx.get_system_object_type(&target_id);
550        // The type used for privileges, for example if the actual type is a view, the reference
551        // type is table.
552        let mut reference_object_type = actual_object_type.clone();
553
554        if let SystemObjectId::Object(ObjectId::Item(id)) = &target_id {
555            let item = scx.get_item(id);
556            let item_type: ObjectType = item.item_type().into();
557            if (item_type == ObjectType::View
558                || item_type == ObjectType::MaterializedView
559                || item_type == ObjectType::Source)
560                && object_type == SystemObjectType::Object(ObjectType::Table)
561            {
562                // This is an expected mis-match to match PostgreSQL semantics.
563                reference_object_type = SystemObjectType::Object(ObjectType::Table);
564            } else if SystemObjectType::Object(item_type) != object_type {
565                let object_name = scx.catalog.resolve_full_name(item.name()).to_string();
566                return Err(PlanError::InvalidObjectType {
567                    expected_type: object_type,
568                    actual_type: actual_object_type,
569                    object_name,
570                });
571            }
572        }
573
574        // Compute the ACL mode against the reference type so that, for example,
575        // `REVOKE ALL ON TABLE <view>` expands to the full TABLE privilege set
576        // (SELECT|INSERT|UPDATE|DELETE) rather than only the privileges valid
577        // for a view on its own (SELECT).
578        let acl_mode = privilege_spec_to_acl_mode(scx, &privileges, reference_object_type);
579        let acl_from_all = matches!(privileges, PrivilegeSpecification::All);
580
581        let all_object_privileges = scx.catalog.all_object_privileges(reference_object_type);
582        let invalid_privileges = acl_mode.difference(all_object_privileges);
583        if !invalid_privileges.is_empty() {
584            let object_description =
585                ErrorMessageObjectDescription::from_sys_id(&target_id, scx.catalog);
586            return Err(PlanError::InvalidPrivilegeTypes {
587                invalid_privileges,
588                object_description,
589            });
590        }
591
592        // In PostgreSQL, the grantor must always be either the object owner or some role that has been
593        // been explicitly granted grant options. In Materialize, we haven't implemented grant options
594        // so the grantor is always the object owner.
595        //
596        // For more details see:
597        // https://github.com/postgres/postgres/blob/78d5952dd0e66afc4447eec07f770991fa406cce/src/backend/utils/adt/acl.c#L5154-L5246
598        let grantor = match &target_id {
599            SystemObjectId::Object(object_id) => scx
600                .catalog
601                .get_owner_id(object_id)
602                .ok_or_else(|| sql_err!("cannot revoke privileges on objects without owners"))?,
603            SystemObjectId::System => scx.catalog.mz_system_role_id(),
604        };
605
606        update_privileges.push(UpdatePrivilege {
607            acl_mode,
608            target_id,
609            grantor,
610            acl_from_all,
611        });
612    }
613
614    let grantees = roles.into_iter().map(|role| role.id).collect();
615
616    Ok(UpdatePrivilegesPlan {
617        update_privileges,
618        grantees,
619    })
620}
621
622fn privilege_spec_to_acl_mode(
623    scx: &StatementContext,
624    privilege_spec: &PrivilegeSpecification,
625    object_type: SystemObjectType,
626) -> AclMode {
627    match privilege_spec {
628        PrivilegeSpecification::All => scx.catalog.all_object_privileges(object_type),
629        PrivilegeSpecification::Privileges(privileges) => privileges
630            .into_iter()
631            .map(|privilege| privilege_to_acl_mode(privilege.clone()))
632            // PostgreSQL doesn't care about duplicate privileges, so we don't either.
633            .fold(AclMode::empty(), |accum, acl_mode| accum.union(acl_mode)),
634    }
635}
636
637fn privilege_to_acl_mode(privilege: Privilege) -> AclMode {
638    match privilege {
639        Privilege::SELECT => AclMode::SELECT,
640        Privilege::INSERT => AclMode::INSERT,
641        Privilege::UPDATE => AclMode::UPDATE,
642        Privilege::DELETE => AclMode::DELETE,
643        Privilege::USAGE => AclMode::USAGE,
644        Privilege::CREATE => AclMode::CREATE,
645        Privilege::CREATEROLE => AclMode::CREATE_ROLE,
646        Privilege::CREATEDB => AclMode::CREATE_DB,
647        Privilege::CREATECLUSTER => AclMode::CREATE_CLUSTER,
648        Privilege::CREATENETWORKPOLICY => AclMode::CREATE_NETWORK_POLICY,
649    }
650}
651
652pub fn describe_alter_default_privileges(
653    _: &StatementContext,
654    _: AlterDefaultPrivilegesStatement<Aug>,
655) -> Result<StatementDesc, PlanError> {
656    Ok(StatementDesc::new(None))
657}
658
659pub fn plan_alter_default_privileges(
660    scx: &StatementContext,
661    AlterDefaultPrivilegesStatement {
662        target_roles,
663        target_objects,
664        grant_or_revoke,
665    }: AlterDefaultPrivilegesStatement<Aug>,
666) -> Result<Plan, PlanError> {
667    let object_type: ObjectType = (*grant_or_revoke.object_type()).into();
668    match object_type {
669        ObjectType::View | ObjectType::MaterializedView | ObjectType::Source => sql_bail!(
670            "{object_type}S is not valid for ALTER DEFAULT PRIVILEGES, use TABLES instead"
671        ),
672        ObjectType::Sink | ObjectType::ClusterReplica | ObjectType::Role | ObjectType::Func => {
673            sql_bail!("{object_type}S do not have privileges")
674        }
675        ObjectType::Cluster | ObjectType::Database
676            if matches!(
677                target_objects,
678                GrantTargetAllSpecification::AllDatabases { .. }
679            ) =>
680        {
681            sql_bail!("cannot specify {object_type}S and IN DATABASE")
682        }
683
684        ObjectType::Cluster | ObjectType::Database | ObjectType::Schema
685            if matches!(
686                target_objects,
687                GrantTargetAllSpecification::AllSchemas { .. }
688            ) =>
689        {
690            sql_bail!("cannot specify {object_type}S and IN SCHEMA")
691        }
692        ObjectType::Table
693        | ObjectType::Index
694        | ObjectType::Type
695        | ObjectType::Secret
696        | ObjectType::Connection
697        | ObjectType::Cluster
698        | ObjectType::Database
699        | ObjectType::Schema
700        | ObjectType::NetworkPolicy => {}
701    }
702
703    let acl_mode = privilege_spec_to_acl_mode(
704        scx,
705        grant_or_revoke.privileges(),
706        SystemObjectType::Object(object_type),
707    );
708    let all_object_privileges = scx
709        .catalog
710        .all_object_privileges(SystemObjectType::Object(object_type));
711    let invalid_privileges = acl_mode.difference(all_object_privileges);
712    if !invalid_privileges.is_empty() {
713        let object_description =
714            ErrorMessageObjectDescription::from_object_type(SystemObjectType::Object(object_type));
715        return Err(PlanError::InvalidPrivilegeTypes {
716            invalid_privileges,
717            object_description,
718        });
719    }
720
721    let target_roles = match target_roles {
722        TargetRoleSpecification::Roles(roles) => roles.into_iter().map(|role| role.id).collect(),
723        TargetRoleSpecification::AllRoles => vec![RoleId::Public],
724    };
725    let mut privilege_objects = Vec::with_capacity(target_roles.len() * target_objects.len());
726    for target_role in target_roles {
727        match &target_objects {
728            GrantTargetAllSpecification::All => privilege_objects.push(DefaultPrivilegeObject {
729                role_id: target_role,
730                database_id: None,
731                schema_id: None,
732                object_type,
733            }),
734            GrantTargetAllSpecification::AllDatabases { databases } => {
735                for database in databases {
736                    privilege_objects.push(DefaultPrivilegeObject {
737                        role_id: target_role,
738                        database_id: Some(*database.database_id()),
739                        schema_id: None,
740                        object_type,
741                    });
742                }
743            }
744            GrantTargetAllSpecification::AllSchemas { schemas } => {
745                for schema in schemas {
746                    privilege_objects.push(DefaultPrivilegeObject {
747                        role_id: target_role,
748                        database_id: schema.database_spec().id(),
749                        schema_id: Some(schema.schema_spec().into()),
750                        object_type,
751                    });
752                }
753            }
754        }
755    }
756
757    let privilege_acl_items = grant_or_revoke
758        .roles()
759        .into_iter()
760        .map(|grantee| DefaultPrivilegeAclItem {
761            grantee: grantee.id,
762            acl_mode,
763        })
764        .collect();
765
766    let is_grant = match grant_or_revoke {
767        AbbreviatedGrantOrRevokeStatement::Grant(_) => true,
768        AbbreviatedGrantOrRevokeStatement::Revoke(_) => false,
769    };
770
771    Ok(Plan::AlterDefaultPrivileges(AlterDefaultPrivilegesPlan {
772        privilege_objects,
773        privilege_acl_items,
774        is_grant,
775    }))
776}
777
778pub fn describe_reassign_owned(
779    _: &StatementContext,
780    _: ReassignOwnedStatement<Aug>,
781) -> Result<StatementDesc, PlanError> {
782    Ok(StatementDesc::new(None))
783}
784
785pub fn plan_reassign_owned(
786    scx: &StatementContext,
787    ReassignOwnedStatement {
788        old_roles,
789        new_role,
790    }: ReassignOwnedStatement<Aug>,
791) -> Result<Plan, PlanError> {
792    let old_roles: BTreeSet<_> = old_roles.into_iter().map(|role| role.id).collect();
793    let mut reassign_ids: Vec<ObjectId> = Vec::new();
794
795    // Replicas
796    for replica in scx.catalog.get_cluster_replicas() {
797        if old_roles.contains(&replica.owner_id()) {
798            reassign_ids.push((replica.cluster_id(), replica.replica_id()).into());
799        }
800    }
801    // Clusters
802    for cluster in scx.catalog.get_clusters() {
803        if old_roles.contains(&cluster.owner_id()) {
804            reassign_ids.push(cluster.id().into());
805        }
806    }
807    // Items
808    for item in scx.catalog.get_items() {
809        if old_roles.contains(&item.owner_id()) {
810            reassign_ids.push(item.id().into());
811        }
812    }
813    // Schemas
814    for schema in scx.catalog.get_schemas() {
815        if !schema.id().is_temporary() {
816            if old_roles.contains(&schema.owner_id()) {
817                reassign_ids.push((*schema.database(), *schema.id()).into())
818            }
819        }
820    }
821    // Databases
822    for database in scx.catalog.get_databases() {
823        if old_roles.contains(&database.owner_id()) {
824            reassign_ids.push(database.id().into());
825        }
826    }
827    // Network policies
828    for network_policy in scx.catalog.get_network_policies() {
829        if old_roles.contains(&network_policy.owner_id()) {
830            reassign_ids.push(ObjectId::NetworkPolicy(network_policy.id()));
831        }
832    }
833
834    let system_ids: Vec<_> = reassign_ids.iter().filter(|id| id.is_system()).collect();
835    if !system_ids.is_empty() {
836        let mut owners = system_ids
837            .into_iter()
838            .filter_map(|object_id| scx.catalog.get_owner_id(object_id))
839            .collect::<BTreeSet<_>>()
840            .into_iter()
841            .map(|role_id| scx.catalog.get_role(&role_id).name().quoted());
842        sql_bail!(
843            "cannot reassign objects owned by role {} because they are required by the database system",
844            owners.join(", "),
845        );
846    }
847
848    Ok(Plan::ReassignOwned(ReassignOwnedPlan {
849        old_roles: old_roles.into_iter().collect(),
850        new_role: new_role.id,
851        reassign_ids,
852    }))
853}