1use std::collections::{BTreeMap, BTreeSet};
19
20use crate::identifier::Identifier;
21use crate::ir::default_privileges::{DefaultPrivObjectType, DefaultPrivilegeRule};
22use crate::ir::grant::{Grant, GrantTarget};
23
24#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct DefaultPrivilegeChange {
27 pub target_role: Identifier,
29 pub schema: Option<Identifier>,
31 pub object_type: DefaultPrivObjectType,
33 pub is_grant: bool,
35 pub grant: Grant,
37}
38
39type RuleKey = (Identifier, Option<Identifier>, DefaultPrivObjectType);
40
41#[must_use]
48pub fn diff_default_privileges(
49 target: &[DefaultPrivilegeRule],
50 source: &[DefaultPrivilegeRule],
51 managed_roles: &BTreeSet<Identifier>,
52) -> Vec<DefaultPrivilegeChange> {
53 let key = |r: &DefaultPrivilegeRule| -> RuleKey {
54 (r.target_role.clone(), r.schema.clone(), r.object_type)
55 };
56
57 let target_map: BTreeMap<RuleKey, &DefaultPrivilegeRule> =
58 target.iter().map(|r| (key(r), r)).collect();
59 let source_map: BTreeMap<RuleKey, &DefaultPrivilegeRule> =
60 source.iter().map(|r| (key(r), r)).collect();
61
62 let mut all_keys: BTreeSet<RuleKey> = target_map.keys().cloned().collect();
63 all_keys.extend(source_map.keys().cloned());
64
65 let mut out = Vec::new();
66 for k in all_keys {
67 let target_grants = target_map
68 .get(&k)
69 .map_or(&[] as &[Grant], |r| r.grants.as_slice());
70 let source_grants = source_map
71 .get(&k)
72 .map_or(&[] as &[Grant], |r| r.grants.as_slice());
73
74 let extended;
79 let effective_managed: &BTreeSet<Identifier> = if source_map.contains_key(&k) {
80 extended = target_grants
81 .iter()
82 .filter_map(|g| {
83 if let GrantTarget::Role(name) = &g.grantee {
84 Some(name.clone())
85 } else {
86 None
87 }
88 })
89 .fold(managed_roles.clone(), |mut acc, name| {
90 acc.insert(name);
91 acc
92 });
93 &extended
94 } else {
95 managed_roles
96 };
97
98 let (to_add, to_revoke, _unmanaged) =
99 super::grants::diff_grants(target_grants, source_grants, effective_managed);
100
101 for g in to_revoke {
105 out.push(DefaultPrivilegeChange {
106 target_role: k.0.clone(),
107 schema: k.1.clone(),
108 object_type: k.2,
109 is_grant: false,
110 grant: g,
111 });
112 }
113 for g in to_add {
114 out.push(DefaultPrivilegeChange {
115 target_role: k.0.clone(),
116 schema: k.1.clone(),
117 object_type: k.2,
118 is_grant: true,
119 grant: g,
120 });
121 }
122 }
123 out
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use crate::identifier::Identifier;
130 use crate::ir::default_privileges::{DefaultPrivObjectType, DefaultPrivilegeRule};
131 use crate::ir::grant::{Grant, GrantTarget, Privilege};
132
133 fn id(s: &str) -> Identifier {
134 Identifier::from_unquoted(s).unwrap()
135 }
136
137 fn grant_to(role: &str, priv_: Privilege) -> Grant {
138 Grant {
139 grantee: GrantTarget::Role(id(role)),
140 privilege: priv_,
141 with_grant_option: false,
142 columns: None,
143 }
144 }
145
146 fn rule(
147 target_role: &str,
148 schema: Option<&str>,
149 object_type: DefaultPrivObjectType,
150 grants: Vec<Grant>,
151 ) -> DefaultPrivilegeRule {
152 DefaultPrivilegeRule {
153 target_role: id(target_role),
154 schema: schema.map(id),
155 object_type,
156 grants,
157 }
158 }
159
160 fn managed(roles: &[&str]) -> BTreeSet<Identifier> {
161 roles.iter().map(|r| id(r)).collect()
162 }
163
164 #[test]
165 fn add_only() {
166 let src = rule(
168 "app_owner",
169 None,
170 DefaultPrivObjectType::Tables,
171 vec![grant_to("web_anon", Privilege::Select)],
172 );
173 let changes = diff_default_privileges(&[], &[src], &managed(&["app_owner", "web_anon"]));
174 assert_eq!(changes.len(), 1);
175 assert!(changes[0].is_grant, "expected is_grant=true");
176 assert_eq!(changes[0].grant.privilege, Privilege::Select);
177 }
178
179 #[test]
180 fn revoke_only() {
181 let tgt = rule(
184 "app_owner",
185 None,
186 DefaultPrivObjectType::Tables,
187 vec![grant_to("web_anon", Privilege::Select)],
188 );
189 let changes = diff_default_privileges(&[tgt], &[], &managed(&["app_owner", "web_anon"]));
190 assert_eq!(changes.len(), 1);
191 assert!(!changes[0].is_grant, "expected is_grant=false");
192 assert_eq!(changes[0].grant.privilege, Privilege::Select);
193 }
194
195 #[test]
196 fn mixed_add_and_revoke() {
197 let tgt = rule(
200 "app_owner",
201 Some("app"),
202 DefaultPrivObjectType::Sequences,
203 vec![grant_to("alice", Privilege::Select)],
204 );
205 let src = rule(
206 "app_owner",
207 Some("app"),
208 DefaultPrivObjectType::Sequences,
209 vec![grant_to("alice", Privilege::Usage)],
210 );
211 let changes = diff_default_privileges(&[tgt], &[src], &managed(&["app_owner", "alice"]));
212 assert_eq!(changes.len(), 2, "expected 2 changes, got: {changes:?}");
213 let adds: Vec<_> = changes.iter().filter(|c| c.is_grant).collect();
214 let revs: Vec<_> = changes.iter().filter(|c| !c.is_grant).collect();
215 assert_eq!(adds.len(), 1);
216 assert_eq!(revs.len(), 1);
217 assert_eq!(adds[0].grant.privilege, Privilege::Usage);
218 assert_eq!(revs[0].grant.privilege, Privilege::Select);
219 }
220
221 #[test]
229 fn revoke_unmanaged_grantee_when_source_owns_the_rule() {
230 use crate::ir::grant::GrantTarget;
231
232 let tgt = rule(
234 "app",
235 None,
236 DefaultPrivObjectType::Schemas,
237 vec![
238 Grant {
239 grantee: GrantTarget::Public,
240 privilege: Privilege::Usage,
241 with_grant_option: false,
242 columns: None,
243 },
244 Grant {
245 grantee: GrantTarget::Role(id("app")),
246 privilege: Privilege::Usage,
247 with_grant_option: false,
248 columns: None,
249 },
250 Grant {
251 grantee: GrantTarget::Role(id("app")),
252 privilege: Privilege::Create,
253 with_grant_option: false,
254 columns: None,
255 },
256 ],
257 );
258
259 let src = rule(
262 "app",
263 None,
264 DefaultPrivObjectType::Schemas,
265 vec![Grant {
266 grantee: GrantTarget::Public,
267 privilege: Privilege::Usage,
268 with_grant_option: false,
269 columns: None,
270 }],
271 );
272
273 let changes = diff_default_privileges(&[tgt], &[src], &managed(&["app_owner"]));
275
276 let revokes: Vec<_> = changes.iter().filter(|c| !c.is_grant).collect();
278 assert_eq!(
279 revokes.len(),
280 2,
281 "expected 2 revokes for Role(app) grants, got: {changes:?}"
282 );
283 let priv_set: std::collections::BTreeSet<Privilege> =
284 revokes.iter().map(|c| c.grant.privilege).collect();
285 assert!(
286 priv_set.contains(&Privilege::Usage),
287 "expected REVOKE Usage"
288 );
289 assert!(
290 priv_set.contains(&Privilege::Create),
291 "expected REVOKE Create"
292 );
293 let grants_count = changes.iter().filter(|c| c.is_grant).count();
295 assert_eq!(grants_count, 0, "expected no new grants, got: {changes:?}");
296 }
297
298 #[test]
312 fn revoke_target_role_as_grantee_not_in_source_grants() {
313 let tgt = rule(
315 "ops",
316 None,
317 DefaultPrivObjectType::Schemas,
318 vec![
319 grant_to("ops", Privilege::Usage),
320 grant_to("ops", Privilege::Create),
321 grant_to("readers", Privilege::Create),
322 ],
323 );
324
325 let src = rule(
327 "ops",
328 None,
329 DefaultPrivObjectType::Schemas,
330 vec![grant_to("readers", Privilege::Create)],
331 );
332
333 let changes = diff_default_privileges(&[tgt], &[src], &managed(&["readers"]));
335
336 let revokes: Vec<_> = changes.iter().filter(|c| !c.is_grant).collect();
337 assert_eq!(
338 revokes.len(),
339 2,
340 "expected 2 REVOKEs for the ops grants, got: {changes:?}"
341 );
342 let priv_set: std::collections::BTreeSet<Privilege> =
343 revokes.iter().map(|c| c.grant.privilege).collect();
344 assert!(
345 priv_set.contains(&Privilege::Usage),
346 "expected REVOKE Usage for ops"
347 );
348 assert!(
349 priv_set.contains(&Privilege::Create),
350 "expected REVOKE Create for ops"
351 );
352 let grants_count = changes.iter().filter(|c| c.is_grant).count();
354 assert_eq!(grants_count, 0, "expected no new grants, got: {changes:?}");
355 }
356}