Skip to main content

uqa_sql/catalog/roles/
dependencies.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Role ownership and ACL dependency diagnostics in catalog and requested-role order.
8
9use 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;