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