uqa_sql/catalog/roles/
dependencies.rs1use crate::{
10 catalog::{
11 security::{database::DatabaseSecurity, SchemaSecurity, SequenceSecurity, TableSecurity},
12 view::StoredViewKind,
13 },
14 SQLError,
15};
16use std::collections::BTreeMap;
17use uqa_core::RelationIdentity;
18pub mod context;
19use context::RoleDependencyCatalog;
20
21pub fn ensure_roles_have_no_object_dependencies(
22 catalog: &dyn RoleDependencyCatalog,
23 names: &[String],
24) -> Result<(), SQLError> {
25 let database_security = catalog.database();
26 for name in names {
27 if database_depends_on_role(&database_security, name) {
28 return Err(SQLError::Routine {
29 sqlstate: "2BP01".into(),
30 message: format!(
31 "role \"{name}\" cannot be dropped because some objects depend on it: database uqa"
32 ),
33 });
34 }
35 }
36 drop(database_security);
37
38 let schema_security = catalog.schemas();
39 for name in names {
40 if let Some(schema) = dependent_schema_for_role(&schema_security, name) {
41 return Err(SQLError::Routine {
42 sqlstate: "2BP01".into(),
43 message: format!(
44 "role \"{name}\" cannot be dropped because some objects depend on it: schema {schema}"
45 ),
46 });
47 }
48 }
49 drop(schema_security);
50
51 let tables = catalog.tables();
52 for name in names {
53 if let Some(relation) = tables.iter().find_map(|(relation, table)| {
54 table_security_depends_on_role(&table.security(), name).then_some(relation)
55 }) {
56 return Err(SQLError::Routine {
57 sqlstate: "2BP01".into(),
58 message: format!(
59 "role \"{name}\" cannot be dropped because some objects depend on it: table {}",
60 relation.qualified_name()
61 ),
62 });
63 }
64 }
65 drop(tables);
66
67 let views = catalog.views();
68 for name in names {
69 if let Some((relation, view)) = views
70 .iter()
71 .find(|(_, view)| table_security_depends_on_role(&view.security(), name))
72 {
73 let kind = match view.kind {
74 StoredViewKind::View => "view",
75 StoredViewKind::Materialized => "materialized view",
76 };
77 return Err(SQLError::Routine {
78 sqlstate: "2BP01".into(),
79 message: format!(
80 "role \"{name}\" cannot be dropped because some objects depend on it: {kind} {}",
81 relation.qualified_name()
82 ),
83 });
84 }
85 }
86 drop(views);
87
88 ensure_roles_have_no_foreign_table_dependencies(catalog, names)?;
89
90 let sequence_security = catalog.sequences();
91 for name in names {
92 if let Some(relation) = dependent_sequence_for_role(&sequence_security, name) {
93 return Err(SQLError::Routine {
94 sqlstate: "2BP01".into(),
95 message: format!(
96 "role \"{name}\" cannot be dropped because some objects depend on it: sequence {}",
97 relation.qualified_name()
98 ),
99 });
100 }
101 }
102 drop(sequence_security);
103
104 let routines = catalog.routines();
105 for name in names {
106 if let Some(dependent) = routines.values().flatten().find_map(|function| {
107 let owns = function.def.owner == *name;
108 let has_acl = function.def.execute_acl.as_ref().is_some_and(|acl| {
109 acl.iter().any(|entry| {
110 entry.role == *name
111 || entry.grantor.as_deref().unwrap_or(&function.def.owner) == name
112 })
113 });
114 (owns || has_acl).then(|| format!("routine {}", function.def.name))
115 }) {
116 return Err(SQLError::Routine {
117 sqlstate: "2BP01".into(),
118 message: format!("role \"{name}\" cannot be dropped because some objects depend on it: {dependent}"),
119 });
120 }
121 }
122 Ok(())
123}
124
125fn ensure_roles_have_no_foreign_table_dependencies(
126 catalog: &dyn RoleDependencyCatalog,
127 names: &[String],
128) -> Result<(), SQLError> {
129 let foreign_tables = catalog.foreign_tables();
130 for name in names {
131 if let Some((relation, _)) = foreign_tables
132 .iter()
133 .find(|(_, security)| table_security_depends_on_role(security, name))
134 {
135 return Err(SQLError::Routine {
136 sqlstate: "2BP01".into(),
137 message: format!(
138 "role \"{name}\" cannot be dropped because some objects depend on it: foreign table {}",
139 relation.qualified_name()
140 ),
141 });
142 }
143 }
144 Ok(())
145}
146
147fn table_security_depends_on_role(security: &TableSecurity, role: &str) -> bool {
148 let acl_dependency = security.acl.as_ref().is_some_and(|acl| {
149 acl.iter().any(|entry| {
150 entry.role == role || entry.grantor.as_deref().unwrap_or(&security.role_owner) == role
151 })
152 });
153 let column_acl_dependency = security.column_acls.values().any(|acl| {
154 acl.iter().any(|entry| {
155 entry.role == role || entry.grantor.as_deref().unwrap_or(&security.role_owner) == role
156 })
157 });
158 security.role_owner == role || acl_dependency || column_acl_dependency
159}
160
161fn dependent_sequence_for_role<'a>(
162 sequences: &'a BTreeMap<RelationIdentity, SequenceSecurity>,
163 role: &str,
164) -> Option<&'a RelationIdentity> {
165 sequences.iter().find_map(|(relation, security)| {
166 let acl_dependency = security.acl.as_ref().is_some_and(|acl| {
167 acl.iter().any(|entry| {
168 entry.role == role
169 || entry.grantor.as_deref().unwrap_or(&security.role_owner) == role
170 })
171 });
172 (security.role_owner == role || acl_dependency).then_some(relation)
173 })
174}
175
176fn dependent_schema_for_role<'a>(
177 schemas: &'a BTreeMap<String, SchemaSecurity>,
178 role: &str,
179) -> Option<&'a String> {
180 schemas.iter().find_map(|(name, security)| {
181 let acl_dependency = security.acl.as_ref().is_some_and(|acl| {
182 acl.iter().any(|entry| {
183 entry.role == role
184 || entry.grantor.as_deref().unwrap_or(&security.role_owner) == role
185 })
186 });
187 (security.role_owner == role || acl_dependency).then_some(name)
188 })
189}
190
191fn database_depends_on_role(security: &DatabaseSecurity, role: &str) -> bool {
192 let acl_dependency = security.acl.as_ref().is_some_and(|acl| {
193 acl.iter().any(|entry| {
194 entry.role == role || entry.grantor.as_deref().unwrap_or(&security.role_owner) == role
195 })
196 });
197 security.role_owner == role || acl_dependency
198}
199
200#[cfg(test)]
201mod tests;