use std::collections::{BTreeMap, BTreeSet};
use crate::identifier::Identifier;
use crate::ir::default_privileges::{DefaultPrivObjectType, DefaultPrivilegeRule};
use crate::ir::grant::{Grant, GrantTarget};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DefaultPrivilegeChange {
pub target_role: Identifier,
pub schema: Option<Identifier>,
pub object_type: DefaultPrivObjectType,
pub is_grant: bool,
pub grant: Grant,
}
type RuleKey = (Identifier, Option<Identifier>, DefaultPrivObjectType);
#[must_use]
pub fn diff_default_privileges(
target: &[DefaultPrivilegeRule],
source: &[DefaultPrivilegeRule],
managed_roles: &BTreeSet<Identifier>,
) -> Vec<DefaultPrivilegeChange> {
let key = |r: &DefaultPrivilegeRule| -> RuleKey {
(r.target_role.clone(), r.schema.clone(), r.object_type)
};
let target_map: BTreeMap<RuleKey, &DefaultPrivilegeRule> =
target.iter().map(|r| (key(r), r)).collect();
let source_map: BTreeMap<RuleKey, &DefaultPrivilegeRule> =
source.iter().map(|r| (key(r), r)).collect();
let mut all_keys: BTreeSet<RuleKey> = target_map.keys().cloned().collect();
all_keys.extend(source_map.keys().cloned());
let mut out = Vec::new();
for k in all_keys {
let target_grants = target_map
.get(&k)
.map_or(&[] as &[Grant], |r| r.grants.as_slice());
let source_grants = source_map
.get(&k)
.map_or(&[] as &[Grant], |r| r.grants.as_slice());
let extended;
let effective_managed: &BTreeSet<Identifier> = if source_map.contains_key(&k) {
extended = target_grants
.iter()
.filter_map(|g| {
if let GrantTarget::Role(name) = &g.grantee {
Some(name.clone())
} else {
None
}
})
.fold(managed_roles.clone(), |mut acc, name| {
acc.insert(name);
acc
});
&extended
} else {
managed_roles
};
let (to_add, to_revoke, _unmanaged) =
super::grants::diff_grants(target_grants, source_grants, effective_managed);
for g in to_revoke {
out.push(DefaultPrivilegeChange {
target_role: k.0.clone(),
schema: k.1.clone(),
object_type: k.2,
is_grant: false,
grant: g,
});
}
for g in to_add {
out.push(DefaultPrivilegeChange {
target_role: k.0.clone(),
schema: k.1.clone(),
object_type: k.2,
is_grant: true,
grant: g,
});
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identifier::Identifier;
use crate::ir::default_privileges::{DefaultPrivObjectType, DefaultPrivilegeRule};
use crate::ir::grant::{Grant, GrantTarget, Privilege};
fn id(s: &str) -> Identifier {
Identifier::from_unquoted(s).unwrap()
}
fn grant_to(role: &str, priv_: Privilege) -> Grant {
Grant {
grantee: GrantTarget::Role(id(role)),
privilege: priv_,
with_grant_option: false,
columns: None,
}
}
fn rule(
target_role: &str,
schema: Option<&str>,
object_type: DefaultPrivObjectType,
grants: Vec<Grant>,
) -> DefaultPrivilegeRule {
DefaultPrivilegeRule {
target_role: id(target_role),
schema: schema.map(id),
object_type,
grants,
}
}
fn managed(roles: &[&str]) -> BTreeSet<Identifier> {
roles.iter().map(|r| id(r)).collect()
}
#[test]
fn add_only() {
let src = rule(
"app_owner",
None,
DefaultPrivObjectType::Tables,
vec![grant_to("web_anon", Privilege::Select)],
);
let changes = diff_default_privileges(&[], &[src], &managed(&["app_owner", "web_anon"]));
assert_eq!(changes.len(), 1);
assert!(changes[0].is_grant, "expected is_grant=true");
assert_eq!(changes[0].grant.privilege, Privilege::Select);
}
#[test]
fn revoke_only() {
let tgt = rule(
"app_owner",
None,
DefaultPrivObjectType::Tables,
vec![grant_to("web_anon", Privilege::Select)],
);
let changes = diff_default_privileges(&[tgt], &[], &managed(&["app_owner", "web_anon"]));
assert_eq!(changes.len(), 1);
assert!(!changes[0].is_grant, "expected is_grant=false");
assert_eq!(changes[0].grant.privilege, Privilege::Select);
}
#[test]
fn mixed_add_and_revoke() {
let tgt = rule(
"app_owner",
Some("app"),
DefaultPrivObjectType::Sequences,
vec![grant_to("alice", Privilege::Select)],
);
let src = rule(
"app_owner",
Some("app"),
DefaultPrivObjectType::Sequences,
vec![grant_to("alice", Privilege::Usage)],
);
let changes = diff_default_privileges(&[tgt], &[src], &managed(&["app_owner", "alice"]));
assert_eq!(changes.len(), 2, "expected 2 changes, got: {changes:?}");
let adds: Vec<_> = changes.iter().filter(|c| c.is_grant).collect();
let revs: Vec<_> = changes.iter().filter(|c| !c.is_grant).collect();
assert_eq!(adds.len(), 1);
assert_eq!(revs.len(), 1);
assert_eq!(adds[0].grant.privilege, Privilege::Usage);
assert_eq!(revs[0].grant.privilege, Privilege::Select);
}
#[test]
fn revoke_unmanaged_grantee_when_source_owns_the_rule() {
use crate::ir::grant::GrantTarget;
let tgt = rule(
"app",
None,
DefaultPrivObjectType::Schemas,
vec![
Grant {
grantee: GrantTarget::Public,
privilege: Privilege::Usage,
with_grant_option: false,
columns: None,
},
Grant {
grantee: GrantTarget::Role(id("app")),
privilege: Privilege::Usage,
with_grant_option: false,
columns: None,
},
Grant {
grantee: GrantTarget::Role(id("app")),
privilege: Privilege::Create,
with_grant_option: false,
columns: None,
},
],
);
let src = rule(
"app",
None,
DefaultPrivObjectType::Schemas,
vec![Grant {
grantee: GrantTarget::Public,
privilege: Privilege::Usage,
with_grant_option: false,
columns: None,
}],
);
let changes = diff_default_privileges(&[tgt], &[src], &managed(&["app_owner"]));
let revokes: Vec<_> = changes.iter().filter(|c| !c.is_grant).collect();
assert_eq!(
revokes.len(),
2,
"expected 2 revokes for Role(app) grants, got: {changes:?}"
);
let priv_set: std::collections::BTreeSet<Privilege> =
revokes.iter().map(|c| c.grant.privilege).collect();
assert!(
priv_set.contains(&Privilege::Usage),
"expected REVOKE Usage"
);
assert!(
priv_set.contains(&Privilege::Create),
"expected REVOKE Create"
);
let grants_count = changes.iter().filter(|c| c.is_grant).count();
assert_eq!(grants_count, 0, "expected no new grants, got: {changes:?}");
}
#[test]
fn revoke_target_role_as_grantee_not_in_source_grants() {
let tgt = rule(
"ops",
None,
DefaultPrivObjectType::Schemas,
vec![
grant_to("ops", Privilege::Usage),
grant_to("ops", Privilege::Create),
grant_to("readers", Privilege::Create),
],
);
let src = rule(
"ops",
None,
DefaultPrivObjectType::Schemas,
vec![grant_to("readers", Privilege::Create)],
);
let changes = diff_default_privileges(&[tgt], &[src], &managed(&["readers"]));
let revokes: Vec<_> = changes.iter().filter(|c| !c.is_grant).collect();
assert_eq!(
revokes.len(),
2,
"expected 2 REVOKEs for the ops grants, got: {changes:?}"
);
let priv_set: std::collections::BTreeSet<Privilege> =
revokes.iter().map(|c| c.grant.privilege).collect();
assert!(
priv_set.contains(&Privilege::Usage),
"expected REVOKE Usage for ops"
);
assert!(
priv_set.contains(&Privilege::Create),
"expected REVOKE Create for ops"
);
let grants_count = changes.iter().filter(|c| c.is_grant).count();
assert_eq!(grants_count, 0, "expected no new grants, got: {changes:?}");
}
}