Skip to content

Commit 4c68b16

Browse files
authored
sql: Ensure role member grantor is always valid (#18781)
This commit prevents dropping a role, if that role is a grantor for some other role membership. This helps ensure that the grantor references in the catalog always remain valid. As of this commit, the only valid grantor is mz_system which is impossible to delete. So it's impossible to hit this error scenario. However, this is more future-proof for when we implement ADMIN OPTION and allow other grantors. This is based off of the equivalent commit in PostgreSQL here: postgres/postgres@6566133 That commit will be part of PostgreSQL v16 and was not included in v15. Part of #11579
1 parent d2ddcc1 commit 4c68b16

6 files changed

Lines changed: 59 additions & 12 deletions

File tree

src/adapter/src/catalog.rs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -622,10 +622,10 @@ impl CatalogState {
622622
membership.insert(cur_id.clone());
623623
let role = self.get_role(cur_id);
624624
soft_assert!(
625-
!role.membership().contains(id),
625+
!role.membership().keys().contains(id),
626626
"circular membership exists in the catalog"
627627
);
628-
queue.extend(role.membership().into_iter());
628+
queue.extend(role.membership().keys());
629629
}
630630
}
631631
membership
@@ -7015,6 +7015,16 @@ impl SessionCatalog for ConnCatalog<'_> {
70157015
self.state.get_role(id)
70167016
}
70177017

7018+
fn get_roles(&self) -> Vec<&dyn CatalogRole> {
7019+
// `as` is ok to use to cast to a trait object.
7020+
#[allow(clippy::as_conversions)]
7021+
self.state
7022+
.roles_by_id
7023+
.values()
7024+
.map(|role| role as &dyn CatalogRole)
7025+
.collect()
7026+
}
7027+
70187028
fn collect_role_membership(&self, id: &RoleId) -> BTreeSet<RoleId> {
70197029
self.state.collect_role_membership(id)
70207030
}
@@ -7222,12 +7232,8 @@ impl mz_sql::catalog::CatalogRole for Role {
72227232
self.attributes.create_cluster
72237233
}
72247234

7225-
fn membership(&self) -> BTreeSet<&RoleId> {
7226-
self.membership
7227-
.map
7228-
.iter()
7229-
.map(|(role_id, _grantor_id)| role_id)
7230-
.collect()
7235+
fn membership(&self) -> &BTreeMap<RoleId, RoleId> {
7236+
&self.membership.map
72317237
}
72327238
}
72337239

src/adapter/src/coord/sequencer/inner.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3672,7 +3672,8 @@ impl Coordinator {
36723672
let catalog = self.catalog();
36733673
let mut ops = Vec::new();
36743674
for member_id in member_ids {
3675-
let member_membership: BTreeSet<_> = catalog.get_role(&member_id).membership();
3675+
let member_membership: BTreeSet<_> =
3676+
catalog.get_role(&member_id).membership().keys().collect();
36763677
if member_membership.contains(&role_id) {
36773678
let role_name = catalog.get_role(&role_id).name().to_string();
36783679
let member_name = catalog.get_role(&member_id).name().to_string();
@@ -3713,7 +3714,8 @@ impl Coordinator {
37133714
let catalog = self.catalog();
37143715
let mut ops = Vec::new();
37153716
for member_id in member_ids {
3716-
let member_membership: BTreeSet<_> = catalog.get_role(&member_id).membership();
3717+
let member_membership: BTreeSet<_> =
3718+
catalog.get_role(&member_id).membership().keys().collect();
37173719
if !member_membership.contains(&role_id) {
37183720
let role_name = catalog.get_role(&role_id).name().to_string();
37193721
let member_name = catalog.get_role(&member_id).name().to_string();

src/sql/src/catalog.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,9 @@ pub trait SessionCatalog: fmt::Debug + ExprHumanizer + Send + Sync {
155155
/// Panics if `id` does not specify a valid role.
156156
fn get_role(&self, id: &RoleId) -> &dyn CatalogRole;
157157

158+
/// Gets all roles.
159+
fn get_roles(&self) -> Vec<&dyn CatalogRole>;
160+
158161
/// Collects all role IDs that `id` is transitively a member of.
159162
fn collect_role_membership(&self, id: &RoleId) -> BTreeSet<RoleId>;
160163

@@ -458,8 +461,11 @@ pub trait CatalogRole {
458461
/// Indicates whether the role has the cluster creation attribute.
459462
fn create_cluster(&self) -> bool;
460463

461-
/// Returns all role IDs that this role is an immediate a member of.
462-
fn membership(&self) -> BTreeSet<&RoleId>;
464+
/// Returns all role IDs that this role is an immediate a member of, and the grantor of that
465+
/// membership.
466+
///
467+
/// Key is the role that some role is a member of, value is the grantor role ID.
468+
fn membership(&self) -> &BTreeMap<RoleId, RoleId>;
463469
}
464470

465471
/// A cluster in a [`SessionCatalog`].

src/sql/src/plan/statement/ddl.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3443,6 +3443,17 @@ fn plan_drop_role(
34433443
if &id == scx.catalog.active_role_id() {
34443444
sql_bail!("current role cannot be dropped");
34453445
}
3446+
for role in scx.catalog.get_roles() {
3447+
for (member_id, grantor_id) in role.membership() {
3448+
if &id == grantor_id {
3449+
let member_role = scx.catalog.get_role(member_id);
3450+
sql_bail!(
3451+
"cannot drop role {}: still depended up by membership of role {} in role {}",
3452+
name.as_str(), role.name(), member_role.name()
3453+
);
3454+
}
3455+
}
3456+
}
34463457
Ok(Some(role.id()))
34473458
}
34483459
Err(_) if if_exists => {
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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 at the root of this repository.
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+
> SELECT role.name AS role, member.name AS member, grantor.name AS grantor FROM mz_role_members membership LEFT JOIN mz_roles role ON membership.role_id = role.id LEFT JOIN mz_roles member ON membership.member = member.id LEFT JOIN mz_roles grantor ON membership.grantor = grantor.id;
11+
group joe mz_system
12+
13+
> DROP ROLE superuser_login;
14+
15+
> DROP ROLE "space role";
16+
17+
> DROP ROLE joe;
18+
19+
> DROP ROLE group;

test/upgrade/check-from-v0.47.0-role.td

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ SELECT 1;
2727
joe
2828
group
2929

30+
$ skip-if
31+
SELECT mz_version_num() >= 5200;
32+
3033
> SELECT role.name AS role, member.name AS member, grantor.name AS grantor FROM mz_role_members membership LEFT JOIN mz_roles role ON membership.role_id = role.id LEFT JOIN mz_roles member ON membership.member = member.id LEFT JOIN mz_roles grantor ON membership.grantor = grantor.id;
3134
group joe mz_system
3235

0 commit comments

Comments
 (0)