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
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    fn id(s: &str) -> Identifier {
88        Identifier::from_unquoted(s).unwrap()
89    }
90
91    #[test]
92    fn pg_text_roundtrips() {
93        for cmd in [
94            PolicyCommand::All,
95            PolicyCommand::Select,
96            PolicyCommand::Insert,
97            PolicyCommand::Update,
98            PolicyCommand::Delete,
99        ] {
100            assert_eq!(PolicyCommand::from_pg_text(cmd.sql_keyword()), Some(cmd));
101        }
102    }
103
104    #[test]
105    fn from_pg_text_rejects_unknown() {
106        assert_eq!(PolicyCommand::from_pg_text("BOGUS"), None);
107    }
108
109    #[test]
110    fn select_and_delete_reject_with_check() {
111        assert!(!PolicyCommand::Select.allows_with_check());
112        assert!(!PolicyCommand::Delete.allows_with_check());
113        assert!(PolicyCommand::All.allows_with_check());
114        assert!(PolicyCommand::Insert.allows_with_check());
115        assert!(PolicyCommand::Update.allows_with_check());
116    }
117
118    #[test]
119    fn policy_sort_by_name() {
120        let a = Policy {
121            name: id("alpha"),
122            permissive: true,
123            command: PolicyCommand::All,
124            roles: vec![GrantTarget::Public],
125            using: None,
126            with_check: None,
127        };
128        let b = Policy {
129            name: id("beta"),
130            permissive: true,
131            command: PolicyCommand::All,
132            roles: vec![GrantTarget::Public],
133            using: None,
134            with_check: None,
135        };
136        let mut policies = vec![b.clone(), a.clone()];
137        policies.sort();
138        assert_eq!(policies, vec![a, b]);
139    }
140}