Skip to main content

pgevolve_core/ir/
policy.rs

1//! Row-level security policies — `Policy`, `PolicyCommand`.
2//!
3//! Policies embed on [`crate::ir::table::Table`] — there's no orphan
4//! shape possible in PG. USING / WITH CHECK expressions reuse the
5//! [`NormalizedExpr`] canonicalization shared with check constraints.
6
7use serde::{Deserialize, Serialize};
8
9use crate::identifier::Identifier;
10use crate::ir::default_expr::NormalizedExpr;
11use crate::ir::grant::GrantTarget;
12
13/// A row-level security policy attached to a table.
14#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
15pub struct Policy {
16    /// Policy name. Unique per table.
17    pub name: Identifier,
18    /// `AS PERMISSIVE` (true; PG default) vs `AS RESTRICTIVE` (false).
19    pub permissive: bool,
20    /// Which command(s) this policy applies to. `All` covers all DML.
21    pub command: PolicyCommand,
22    /// `TO roles` list. Source omission canonicalizes to
23    /// `vec![GrantTarget::Public]` at parse time so source and catalog
24    /// round-trip equally.
25    pub roles: Vec<GrantTarget>,
26    /// `USING (expr)` — row-visibility filter. PG default: absent.
27    pub using: Option<NormalizedExpr>,
28    /// `WITH CHECK (expr)` — write-time filter. Valid only on commands
29    /// that write rows (Insert/Update/All); parser rejects on Select/Delete.
30    pub with_check: Option<NormalizedExpr>,
31}
32
33/// The command kind a policy applies to.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
35#[serde(rename_all = "snake_case")]
36pub enum PolicyCommand {
37    /// `FOR ALL` — covers SELECT, INSERT, UPDATE, DELETE.
38    All,
39    /// `FOR SELECT`.
40    Select,
41    /// `FOR INSERT`.
42    Insert,
43    /// `FOR UPDATE`.
44    Update,
45    /// `FOR DELETE`.
46    Delete,
47}
48
49impl PolicyCommand {
50    /// SQL keyword used in CREATE POLICY rendering.
51    #[must_use]
52    pub const fn sql_keyword(self) -> &'static str {
53        match self {
54            Self::All => "ALL",
55            Self::Select => "SELECT",
56            Self::Insert => "INSERT",
57            Self::Update => "UPDATE",
58            Self::Delete => "DELETE",
59        }
60    }
61
62    /// `pg_policies.cmd` text value. PG emits one of these strings.
63    #[must_use]
64    pub fn from_pg_text(s: &str) -> Option<Self> {
65        match s {
66            "ALL" => Some(Self::All),
67            "SELECT" => Some(Self::Select),
68            "INSERT" => Some(Self::Insert),
69            "UPDATE" => Some(Self::Update),
70            "DELETE" => Some(Self::Delete),
71            _ => None,
72        }
73    }
74
75    /// Whether `WITH CHECK` is valid for this command. PG rejects WITH CHECK
76    /// on FOR SELECT and FOR DELETE policies.
77    #[must_use]
78    pub const fn allows_with_check(self) -> bool {
79        matches!(self, Self::All | Self::Insert | Self::Update)
80    }
81
82    /// Whether `USING` is valid for this command. PG rejects USING on FOR
83    /// INSERT policies (`only WITH CHECK expression allowed for INSERT`).
84    #[must_use]
85    pub const fn allows_using(self) -> bool {
86        matches!(self, Self::All | Self::Select | Self::Update | Self::Delete)
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    fn id(s: &str) -> Identifier {
95        Identifier::from_unquoted(s).unwrap()
96    }
97
98    #[test]
99    fn pg_text_roundtrips() {
100        for cmd in [
101            PolicyCommand::All,
102            PolicyCommand::Select,
103            PolicyCommand::Insert,
104            PolicyCommand::Update,
105            PolicyCommand::Delete,
106        ] {
107            assert_eq!(PolicyCommand::from_pg_text(cmd.sql_keyword()), Some(cmd));
108        }
109    }
110
111    #[test]
112    fn from_pg_text_rejects_unknown() {
113        assert_eq!(PolicyCommand::from_pg_text("BOGUS"), None);
114    }
115
116    #[test]
117    fn select_and_delete_reject_with_check() {
118        assert!(!PolicyCommand::Select.allows_with_check());
119        assert!(!PolicyCommand::Delete.allows_with_check());
120        assert!(PolicyCommand::All.allows_with_check());
121        assert!(PolicyCommand::Insert.allows_with_check());
122        assert!(PolicyCommand::Update.allows_with_check());
123    }
124
125    #[test]
126    fn insert_rejects_using() {
127        assert!(!PolicyCommand::Insert.allows_using());
128        assert!(PolicyCommand::Select.allows_using());
129        assert!(PolicyCommand::Delete.allows_using());
130        assert!(PolicyCommand::Update.allows_using());
131        assert!(PolicyCommand::All.allows_using());
132    }
133
134    #[test]
135    fn policy_sort_by_name() {
136        let a = Policy {
137            name: id("alpha"),
138            permissive: true,
139            command: PolicyCommand::All,
140            roles: vec![GrantTarget::Public],
141            using: None,
142            with_check: None,
143        };
144        let b = Policy {
145            name: id("beta"),
146            permissive: true,
147            command: PolicyCommand::All,
148            roles: vec![GrantTarget::Public],
149            using: None,
150            with_check: None,
151        };
152        let mut policies = vec![b.clone(), a.clone()];
153        policies.sort();
154        assert_eq!(policies, vec![a, b]);
155    }
156}