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