use std::collections::BTreeSet;
use crate::identifier::Identifier;
use crate::ir::catalog::Catalog;
use crate::ir::grant::{Grant, GrantTarget};
#[must_use]
pub fn diff_grants(
target: &[Grant],
source: &[Grant],
managed_roles: &BTreeSet<Identifier>,
) -> (Vec<Grant>, Vec<Grant>, Vec<Grant>) {
let target_set: BTreeSet<&Grant> = target.iter().collect();
let source_set: BTreeSet<&Grant> = source.iter().collect();
let to_add: Vec<Grant> = source_set
.difference(&target_set)
.map(|g| (*g).clone())
.collect();
let mut to_revoke = Vec::new();
let mut unmanaged_observed = Vec::new();
for g in target_set.difference(&source_set) {
if grantee_is_managed(&g.grantee, managed_roles) {
to_revoke.push((*g).clone());
} else {
unmanaged_observed.push((*g).clone());
}
}
(to_add, to_revoke, unmanaged_observed)
}
fn grantee_is_managed(target: &GrantTarget, managed_roles: &BTreeSet<Identifier>) -> bool {
match target {
GrantTarget::Public => true,
GrantTarget::Role(name) => managed_roles.contains(name),
}
}
#[must_use]
pub fn collect_managed_roles(cat: &Catalog) -> BTreeSet<Identifier> {
let mut out = BTreeSet::new();
for s in &cat.schemas {
collect_grants_into(&s.grants, &mut out);
if let Some(o) = &s.owner {
out.insert(o.clone());
}
}
for s in &cat.sequences {
collect_grants_into(&s.grants, &mut out);
if let Some(o) = &s.owner {
out.insert(o.clone());
}
}
for t in &cat.tables {
collect_grants_into(&t.grants, &mut out);
if let Some(o) = &t.owner {
out.insert(o.clone());
}
}
for v in &cat.views {
collect_grants_into(&v.grants, &mut out);
if let Some(o) = &v.owner {
out.insert(o.clone());
}
}
for m in &cat.materialized_views {
collect_grants_into(&m.grants, &mut out);
if let Some(o) = &m.owner {
out.insert(o.clone());
}
}
for f in &cat.functions {
collect_grants_into(&f.grants, &mut out);
if let Some(o) = &f.owner {
out.insert(o.clone());
}
}
for p in &cat.procedures {
collect_grants_into(&p.grants, &mut out);
if let Some(o) = &p.owner {
out.insert(o.clone());
}
}
for t in &cat.types {
collect_grants_into(&t.grants, &mut out);
if let Some(o) = &t.owner {
out.insert(o.clone());
}
}
for r in &cat.default_privileges {
out.insert(r.target_role.clone());
collect_grants_into(&r.grants, &mut out);
}
out
}
fn collect_grants_into(grants: &[Grant], set: &mut BTreeSet<Identifier>) {
for g in grants {
if let GrantTarget::Role(name) = &g.grantee {
set.insert(name.clone());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::grant::{Grant, GrantTarget, Privilege};
fn id(s: &str) -> Identifier {
Identifier::from_unquoted(s).unwrap()
}
fn grant_to_role(role: &str, priv_: Privilege) -> Grant {
Grant {
grantee: GrantTarget::Role(id(role)),
privilege: priv_,
with_grant_option: false,
columns: None,
}
}
fn grant_public(priv_: Privilege) -> Grant {
Grant {
grantee: GrantTarget::Public,
privilege: priv_,
with_grant_option: false,
columns: None,
}
}
fn managed(roles: &[&str]) -> BTreeSet<Identifier> {
roles.iter().map(|r| id(r)).collect()
}
#[test]
fn empty_lists_yield_empty_diff() {
let (add, rev, unmanaged) = diff_grants(&[], &[], &managed(&[]));
assert!(add.is_empty());
assert!(rev.is_empty());
assert!(unmanaged.is_empty());
}
#[test]
fn add_only_grant_in_source_not_in_target() {
let g = grant_to_role("alice", Privilege::Select);
let (add, rev, unmanaged) =
diff_grants(&[], std::slice::from_ref(&g), &managed(&["alice"]));
assert_eq!(add, vec![g]);
assert!(rev.is_empty());
assert!(unmanaged.is_empty());
}
#[test]
fn revoke_managed_grantee() {
let g = grant_to_role("alice", Privilege::Select);
let (add, rev, unmanaged) =
diff_grants(std::slice::from_ref(&g), &[], &managed(&["alice"]));
assert!(add.is_empty());
assert_eq!(rev, vec![g]);
assert!(unmanaged.is_empty());
}
#[test]
fn ignore_unmanaged_grantee() {
let g = grant_to_role("external_role", Privilege::Select);
let (add, rev, unmanaged) = diff_grants(std::slice::from_ref(&g), &[], &managed(&[]));
assert!(add.is_empty());
assert!(rev.is_empty());
assert_eq!(unmanaged, vec![g]);
}
#[test]
fn public_always_managed() {
let g = grant_public(Privilege::Usage);
let (add, rev, unmanaged) = diff_grants(std::slice::from_ref(&g), &[], &managed(&[]));
assert!(add.is_empty());
assert_eq!(rev, vec![g]);
assert!(unmanaged.is_empty());
}
#[test]
fn wgo_upgrade_produces_both_add_and_revoke() {
let target = Grant {
grantee: GrantTarget::Role(id("readers")),
privilege: Privilege::Select,
with_grant_option: false,
columns: None,
};
let source = Grant {
grantee: GrantTarget::Role(id("readers")),
privilege: Privilege::Select,
with_grant_option: true,
columns: None,
};
let (add, rev, unmanaged) = diff_grants(
std::slice::from_ref(&target),
std::slice::from_ref(&source),
&managed(&["readers"]),
);
assert_eq!(add.len(), 1, "expected exactly one grant to add: {add:?}");
assert!(add[0].with_grant_option, "added grant must have wgo=true");
assert_eq!(
rev.len(),
1,
"expected exactly one grant to revoke: {rev:?}"
);
assert!(
!rev[0].with_grant_option,
"revoked grant must have wgo=false"
);
assert!(unmanaged.is_empty());
}
#[test]
fn wgo_downgrade_produces_both_add_and_revoke() {
let target = Grant {
grantee: GrantTarget::Role(id("readers")),
privilege: Privilege::Select,
with_grant_option: true,
columns: None,
};
let source = Grant {
grantee: GrantTarget::Role(id("readers")),
privilege: Privilege::Select,
with_grant_option: false,
columns: None,
};
let (add, rev, unmanaged) = diff_grants(
std::slice::from_ref(&target),
std::slice::from_ref(&source),
&managed(&["readers"]),
);
assert_eq!(add.len(), 1, "expected one add (wgo=false)");
assert!(!add[0].with_grant_option, "added grant must have wgo=false");
assert_eq!(rev.len(), 1, "expected one revoke (wgo=true)");
assert!(rev[0].with_grant_option, "revoked grant must have wgo=true");
assert!(unmanaged.is_empty());
}
}