Skip to main content

pgevolve_core/diff/
default_privileges.rs

1//! Diff `ALTER DEFAULT PRIVILEGES` rules.
2//!
3//! Rules are paired by `(target_role, schema, object_type)`. Within each pair
4//! the grant list is diffed with the standard lenient policy from
5//! [`super::grants::diff_grants`].
6
7use std::collections::{BTreeMap, BTreeSet};
8
9use crate::identifier::Identifier;
10use crate::ir::default_privileges::{DefaultPrivObjectType, DefaultPrivilegeRule};
11use crate::ir::grant::Grant;
12
13/// One ADD or REVOKE step for a default-privilege rule.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct DefaultPrivilegeChange {
16    /// `FOR ROLE x` — the grantor role this rule targets.
17    pub target_role: Identifier,
18    /// `IN SCHEMA y` — schema scope. `None` = global scope.
19    pub schema: Option<Identifier>,
20    /// Object-type discriminant.
21    pub object_type: DefaultPrivObjectType,
22    /// `true` = GRANT, `false` = REVOKE.
23    pub is_grant: bool,
24    /// The specific grant being added or revoked.
25    pub grant: Grant,
26}
27
28type RuleKey = (Identifier, Option<Identifier>, DefaultPrivObjectType);
29
30/// Compute `ALTER DEFAULT PRIVILEGES` changes needed to converge `target`
31/// (live database) toward `source` (desired state).
32///
33/// `managed_roles` is passed to the inner [`super::grants::diff_grants`] call
34/// so that the lenient policy applies: unmanaged grantees in the live catalog
35/// never produce REVOKE steps.
36#[must_use]
37pub fn diff_default_privileges(
38    target: &[DefaultPrivilegeRule],
39    source: &[DefaultPrivilegeRule],
40    managed_roles: &BTreeSet<Identifier>,
41) -> Vec<DefaultPrivilegeChange> {
42    let key = |r: &DefaultPrivilegeRule| -> RuleKey {
43        (r.target_role.clone(), r.schema.clone(), r.object_type)
44    };
45
46    let target_map: BTreeMap<RuleKey, &DefaultPrivilegeRule> =
47        target.iter().map(|r| (key(r), r)).collect();
48    let source_map: BTreeMap<RuleKey, &DefaultPrivilegeRule> =
49        source.iter().map(|r| (key(r), r)).collect();
50
51    let mut all_keys: BTreeSet<RuleKey> = target_map.keys().cloned().collect();
52    all_keys.extend(source_map.keys().cloned());
53
54    let mut out = Vec::new();
55    for k in all_keys {
56        let target_grants = target_map
57            .get(&k)
58            .map_or(&[] as &[Grant], |r| r.grants.as_slice());
59        let source_grants = source_map
60            .get(&k)
61            .map_or(&[] as &[Grant], |r| r.grants.as_slice());
62
63        let (to_add, to_revoke, _unmanaged) =
64            super::grants::diff_grants(target_grants, source_grants, managed_roles);
65
66        for g in to_add {
67            out.push(DefaultPrivilegeChange {
68                target_role: k.0.clone(),
69                schema: k.1.clone(),
70                object_type: k.2,
71                is_grant: true,
72                grant: g,
73            });
74        }
75        for g in to_revoke {
76            out.push(DefaultPrivilegeChange {
77                target_role: k.0.clone(),
78                schema: k.1.clone(),
79                object_type: k.2,
80                is_grant: false,
81                grant: g,
82            });
83        }
84    }
85    out
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use crate::identifier::Identifier;
92    use crate::ir::default_privileges::{DefaultPrivObjectType, DefaultPrivilegeRule};
93    use crate::ir::grant::{Grant, GrantTarget, Privilege};
94
95    fn id(s: &str) -> Identifier {
96        Identifier::from_unquoted(s).unwrap()
97    }
98
99    fn grant_to(role: &str, priv_: Privilege) -> Grant {
100        Grant {
101            grantee: GrantTarget::Role(id(role)),
102            privilege: priv_,
103            with_grant_option: false,
104            columns: None,
105        }
106    }
107
108    fn rule(
109        target_role: &str,
110        schema: Option<&str>,
111        object_type: DefaultPrivObjectType,
112        grants: Vec<Grant>,
113    ) -> DefaultPrivilegeRule {
114        DefaultPrivilegeRule {
115            target_role: id(target_role),
116            schema: schema.map(id),
117            object_type,
118            grants,
119        }
120    }
121
122    fn managed(roles: &[&str]) -> BTreeSet<Identifier> {
123        roles.iter().map(|r| id(r)).collect()
124    }
125
126    #[test]
127    fn add_only() {
128        // Source has a rule; target does not → all grants in source become ADDs.
129        let src = rule(
130            "app_owner",
131            None,
132            DefaultPrivObjectType::Tables,
133            vec![grant_to("web_anon", Privilege::Select)],
134        );
135        let changes = diff_default_privileges(&[], &[src], &managed(&["app_owner", "web_anon"]));
136        assert_eq!(changes.len(), 1);
137        assert!(changes[0].is_grant, "expected is_grant=true");
138        assert_eq!(changes[0].grant.privilege, Privilege::Select);
139    }
140
141    #[test]
142    fn revoke_only() {
143        // Target has a rule; source does not → grants in target become REVOKEs
144        // (for managed grantees).
145        let tgt = rule(
146            "app_owner",
147            None,
148            DefaultPrivObjectType::Tables,
149            vec![grant_to("web_anon", Privilege::Select)],
150        );
151        let changes = diff_default_privileges(&[tgt], &[], &managed(&["app_owner", "web_anon"]));
152        assert_eq!(changes.len(), 1);
153        assert!(!changes[0].is_grant, "expected is_grant=false");
154        assert_eq!(changes[0].grant.privilege, Privilege::Select);
155    }
156
157    #[test]
158    fn mixed_add_and_revoke() {
159        // Target has SELECT; source has INSERT.
160        // SELECT → REVOKE; INSERT → ADD.
161        let tgt = rule(
162            "app_owner",
163            Some("app"),
164            DefaultPrivObjectType::Sequences,
165            vec![grant_to("alice", Privilege::Select)],
166        );
167        let src = rule(
168            "app_owner",
169            Some("app"),
170            DefaultPrivObjectType::Sequences,
171            vec![grant_to("alice", Privilege::Usage)],
172        );
173        let changes = diff_default_privileges(&[tgt], &[src], &managed(&["app_owner", "alice"]));
174        assert_eq!(changes.len(), 2, "expected 2 changes, got: {changes:?}");
175        let adds: Vec<_> = changes.iter().filter(|c| c.is_grant).collect();
176        let revs: Vec<_> = changes.iter().filter(|c| !c.is_grant).collect();
177        assert_eq!(adds.len(), 1);
178        assert_eq!(revs.len(), 1);
179        assert_eq!(adds[0].grant.privilege, Privilege::Usage);
180        assert_eq!(revs[0].grant.privilege, Privilege::Select);
181    }
182}