1use std::collections::BTreeSet;
9
10use crate::identifier::Identifier;
11use crate::ir::catalog::Catalog;
12use crate::ir::grant::{Grant, GrantTarget};
13
14#[must_use]
37pub fn diff_grants(
38 target: &[Grant],
39 source: &[Grant],
40 managed_roles: &BTreeSet<Identifier>,
41) -> (Vec<Grant>, Vec<Grant>, Vec<Grant>) {
42 let target_set: BTreeSet<&Grant> = target.iter().collect();
43 let source_set: BTreeSet<&Grant> = source.iter().collect();
44
45 let to_add: Vec<Grant> = source_set
46 .difference(&target_set)
47 .map(|g| (*g).clone())
48 .collect();
49
50 let mut to_revoke = Vec::new();
51 let mut unmanaged_observed = Vec::new();
52 for g in target_set.difference(&source_set) {
53 if grantee_is_managed(&g.grantee, managed_roles) {
54 to_revoke.push((*g).clone());
55 } else {
56 unmanaged_observed.push((*g).clone());
57 }
58 }
59 (to_add, to_revoke, unmanaged_observed)
60}
61
62fn grantee_is_managed(target: &GrantTarget, managed_roles: &BTreeSet<Identifier>) -> bool {
63 match target {
64 GrantTarget::Public => true,
65 GrantTarget::Role(name) => managed_roles.contains(name),
66 }
67}
68
69#[must_use]
73pub fn collect_managed_roles(cat: &Catalog) -> BTreeSet<Identifier> {
74 let mut out = BTreeSet::new();
75
76 for s in &cat.schemas {
77 collect_grants_into(&s.grants, &mut out);
78 if let Some(o) = &s.owner {
79 out.insert(o.clone());
80 }
81 }
82 for s in &cat.sequences {
83 collect_grants_into(&s.grants, &mut out);
84 if let Some(o) = &s.owner {
85 out.insert(o.clone());
86 }
87 }
88 for t in &cat.tables {
89 collect_grants_into(&t.grants, &mut out);
90 if let Some(o) = &t.owner {
91 out.insert(o.clone());
92 }
93 }
94 for v in &cat.views {
95 collect_grants_into(&v.grants, &mut out);
96 if let Some(o) = &v.owner {
97 out.insert(o.clone());
98 }
99 }
100 for m in &cat.materialized_views {
101 collect_grants_into(&m.grants, &mut out);
102 if let Some(o) = &m.owner {
103 out.insert(o.clone());
104 }
105 }
106 for f in &cat.functions {
107 collect_grants_into(&f.grants, &mut out);
108 if let Some(o) = &f.owner {
109 out.insert(o.clone());
110 }
111 }
112 for p in &cat.procedures {
113 collect_grants_into(&p.grants, &mut out);
114 if let Some(o) = &p.owner {
115 out.insert(o.clone());
116 }
117 }
118 for t in &cat.types {
119 collect_grants_into(&t.grants, &mut out);
120 if let Some(o) = &t.owner {
121 out.insert(o.clone());
122 }
123 }
124 for r in &cat.default_privileges {
125 out.insert(r.target_role.clone());
126 collect_grants_into(&r.grants, &mut out);
127 }
128
129 out
130}
131
132fn collect_grants_into(grants: &[Grant], set: &mut BTreeSet<Identifier>) {
134 for g in grants {
135 if let GrantTarget::Role(name) = &g.grantee {
136 set.insert(name.clone());
137 }
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use crate::ir::grant::{Grant, GrantTarget, Privilege};
145
146 fn id(s: &str) -> Identifier {
147 Identifier::from_unquoted(s).unwrap()
148 }
149
150 fn grant_to_role(role: &str, priv_: Privilege) -> Grant {
151 Grant {
152 grantee: GrantTarget::Role(id(role)),
153 privilege: priv_,
154 with_grant_option: false,
155 columns: None,
156 }
157 }
158
159 fn grant_public(priv_: Privilege) -> Grant {
160 Grant {
161 grantee: GrantTarget::Public,
162 privilege: priv_,
163 with_grant_option: false,
164 columns: None,
165 }
166 }
167
168 fn managed(roles: &[&str]) -> BTreeSet<Identifier> {
169 roles.iter().map(|r| id(r)).collect()
170 }
171
172 #[test]
173 fn empty_lists_yield_empty_diff() {
174 let (add, rev, unmanaged) = diff_grants(&[], &[], &managed(&[]));
175 assert!(add.is_empty());
176 assert!(rev.is_empty());
177 assert!(unmanaged.is_empty());
178 }
179
180 #[test]
181 fn add_only_grant_in_source_not_in_target() {
182 let g = grant_to_role("alice", Privilege::Select);
183 let (add, rev, unmanaged) =
184 diff_grants(&[], std::slice::from_ref(&g), &managed(&["alice"]));
185 assert_eq!(add, vec![g]);
186 assert!(rev.is_empty());
187 assert!(unmanaged.is_empty());
188 }
189
190 #[test]
191 fn revoke_managed_grantee() {
192 let g = grant_to_role("alice", Privilege::Select);
194 let (add, rev, unmanaged) =
195 diff_grants(std::slice::from_ref(&g), &[], &managed(&["alice"]));
196 assert!(add.is_empty());
197 assert_eq!(rev, vec![g]);
198 assert!(unmanaged.is_empty());
199 }
200
201 #[test]
202 fn ignore_unmanaged_grantee() {
203 let g = grant_to_role("external_role", Privilege::Select);
205 let (add, rev, unmanaged) = diff_grants(std::slice::from_ref(&g), &[], &managed(&[]));
206 assert!(add.is_empty());
207 assert!(rev.is_empty());
208 assert_eq!(unmanaged, vec![g]);
209 }
210
211 #[test]
212 fn public_always_managed() {
213 let g = grant_public(Privilege::Usage);
215 let (add, rev, unmanaged) = diff_grants(std::slice::from_ref(&g), &[], &managed(&[]));
216 assert!(add.is_empty());
217 assert_eq!(rev, vec![g]);
218 assert!(unmanaged.is_empty());
219 }
220
221 #[test]
234 fn wgo_upgrade_produces_both_add_and_revoke() {
235 let target = Grant {
238 grantee: GrantTarget::Role(id("readers")),
239 privilege: Privilege::Select,
240 with_grant_option: false,
241 columns: None,
242 };
243 let source = Grant {
244 grantee: GrantTarget::Role(id("readers")),
245 privilege: Privilege::Select,
246 with_grant_option: true,
247 columns: None,
248 };
249 let (add, rev, unmanaged) = diff_grants(
250 std::slice::from_ref(&target),
251 std::slice::from_ref(&source),
252 &managed(&["readers"]),
253 );
254 assert_eq!(add.len(), 1, "expected exactly one grant to add: {add:?}");
256 assert!(add[0].with_grant_option, "added grant must have wgo=true");
257 assert_eq!(
259 rev.len(),
260 1,
261 "expected exactly one grant to revoke: {rev:?}"
262 );
263 assert!(
264 !rev[0].with_grant_option,
265 "revoked grant must have wgo=false"
266 );
267 assert!(unmanaged.is_empty());
268 }
276
277 #[test]
278 fn wgo_downgrade_produces_both_add_and_revoke() {
279 let target = Grant {
282 grantee: GrantTarget::Role(id("readers")),
283 privilege: Privilege::Select,
284 with_grant_option: true,
285 columns: None,
286 };
287 let source = Grant {
288 grantee: GrantTarget::Role(id("readers")),
289 privilege: Privilege::Select,
290 with_grant_option: false,
291 columns: None,
292 };
293 let (add, rev, unmanaged) = diff_grants(
294 std::slice::from_ref(&target),
295 std::slice::from_ref(&source),
296 &managed(&["readers"]),
297 );
298 assert_eq!(add.len(), 1, "expected one add (wgo=false)");
299 assert!(!add[0].with_grant_option, "added grant must have wgo=false");
300 assert_eq!(rev.len(), 1, "expected one revoke (wgo=true)");
301 assert!(rev[0].with_grant_option, "revoked grant must have wgo=true");
302 assert!(unmanaged.is_empty());
303 }
304}