Skip to main content

drizzle_types/postgres/ddl/
policy.rs

1//! `PostgreSQL` row-level security policies: [`PolicyDef`] (const) and
2//! [`Policy`] (runtime).
3
4use crate::alloc_prelude::*;
5
6#[cfg(feature = "serde")]
7use crate::serde_helpers::{cow_from_string, cow_option_from_string, cow_option_vec_from_strings};
8
9// =============================================================================
10// Const-friendly Definition Type
11// =============================================================================
12
13/// A row-level security policy that can be built in a `const`.
14///
15/// Clause values are SQL as written: `as_clause` is `PERMISSIVE` or
16/// `RESTRICTIVE`, `for_clause` is `ALL`, `SELECT`, `INSERT`, `UPDATE` or
17/// `DELETE`, and `using` / `with_check` are boolean expressions.
18///
19/// # Examples
20///
21/// ```
22/// use drizzle_types::postgres::ddl::PolicyDef;
23///
24/// const OWNER_ONLY: PolicyDef = PolicyDef::new("public", "posts", "owner_only")
25///     .for_clause("select")
26///     .to(&["authenticated"])
27///     .using("author_id = current_user_id()");
28///
29/// assert_eq!(
30///     OWNER_ONLY.into_policy().create_policy_sql(),
31///     "CREATE POLICY \"owner_only\" ON \"posts\" AS PERMISSIVE FOR SELECT \
32///      TO \"authenticated\" USING (author_id = current_user_id());"
33/// );
34/// ```
35#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
36pub struct PolicyDef {
37    /// Schema name
38    pub schema: &'static str,
39    /// Table name
40    pub table: &'static str,
41    /// Policy name
42    pub name: &'static str,
43    /// `AS` clause: `PERMISSIVE` (the default when `None`) or `RESTRICTIVE`
44    pub as_clause: Option<&'static str>,
45    /// `FOR` clause: `ALL`, `SELECT`, `INSERT`, `UPDATE` or `DELETE`
46    pub for_clause: Option<&'static str>,
47    /// `TO` role names; `public` means `PUBLIC`
48    pub to: Option<&'static [&'static str]>,
49    /// USING expression
50    pub using: Option<&'static str>,
51    /// WITH CHECK expression
52    pub with_check: Option<&'static str>,
53}
54
55impl PolicyDef {
56    /// Creates a policy with no clauses set.
57    #[must_use]
58    pub const fn new(schema: &'static str, table: &'static str, name: &'static str) -> Self {
59        Self {
60            schema,
61            table,
62            name,
63            as_clause: None,
64            for_clause: None,
65            to: None,
66            using: None,
67            with_check: None,
68        }
69    }
70
71    /// Sets the `AS` clause.
72    #[must_use]
73    pub const fn as_clause(self, clause: &'static str) -> Self {
74        Self {
75            as_clause: Some(clause),
76            ..self
77        }
78    }
79
80    /// Sets the `FOR` clause.
81    #[must_use]
82    pub const fn for_clause(self, clause: &'static str) -> Self {
83        Self {
84            for_clause: Some(clause),
85            ..self
86        }
87    }
88
89    /// Sets the `TO` role names.
90    #[must_use]
91    pub const fn to(self, roles: &'static [&'static str]) -> Self {
92        Self {
93            to: Some(roles),
94            ..self
95        }
96    }
97
98    /// Sets the `USING` expression.
99    #[must_use]
100    pub const fn using(self, expr: &'static str) -> Self {
101        Self {
102            using: Some(expr),
103            ..self
104        }
105    }
106
107    /// Sets the `WITH CHECK` expression.
108    #[must_use]
109    pub const fn with_check(self, expr: &'static str) -> Self {
110        Self {
111            with_check: Some(expr),
112            ..self
113        }
114    }
115
116    /// Converts to the runtime [`Policy`].
117    #[must_use]
118    pub fn into_policy(self) -> Policy {
119        Policy {
120            schema: Cow::Borrowed(self.schema),
121            table: Cow::Borrowed(self.table),
122            name: Cow::Borrowed(self.name),
123            as_clause: self.as_clause.map(Cow::Borrowed),
124            for_clause: self.for_clause.map(Cow::Borrowed),
125            to: self
126                .to
127                .map(|roles| roles.iter().copied().map(Cow::Borrowed).collect()),
128            using: self.using.map(Cow::Borrowed),
129            with_check: self.with_check.map(Cow::Borrowed),
130        }
131    }
132}
133
134// =============================================================================
135// Runtime Type for Serde
136// =============================================================================
137
138/// A policy, as stored in migration snapshots.
139#[derive(Clone, Debug, PartialEq, Eq)]
140#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
141#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
142pub struct Policy {
143    /// Schema name
144    #[cfg_attr(feature = "serde", serde(deserialize_with = "cow_from_string"))]
145    pub schema: Cow<'static, str>,
146
147    /// Table name
148    #[cfg_attr(feature = "serde", serde(deserialize_with = "cow_from_string"))]
149    pub table: Cow<'static, str>,
150
151    /// Policy name
152    #[cfg_attr(feature = "serde", serde(deserialize_with = "cow_from_string"))]
153    pub name: Cow<'static, str>,
154
155    /// AS clause (PERMISSIVE/RESTRICTIVE)
156    #[cfg_attr(
157        feature = "serde",
158        serde(
159            rename = "as",
160            default,
161            skip_serializing_if = "Option::is_none",
162            deserialize_with = "cow_option_from_string"
163        )
164    )]
165    pub as_clause: Option<Cow<'static, str>>,
166
167    /// FOR clause (ALL/SELECT/INSERT/UPDATE/DELETE)
168    #[cfg_attr(
169        feature = "serde",
170        serde(
171            rename = "for",
172            default,
173            skip_serializing_if = "Option::is_none",
174            deserialize_with = "cow_option_from_string"
175        )
176    )]
177    pub for_clause: Option<Cow<'static, str>>,
178
179    /// TO roles
180    #[cfg_attr(
181        feature = "serde",
182        serde(
183            default,
184            skip_serializing_if = "Option::is_none",
185            deserialize_with = "cow_option_vec_from_strings"
186        )
187    )]
188    pub to: Option<Vec<Cow<'static, str>>>,
189
190    /// USING expression
191    #[cfg_attr(
192        feature = "serde",
193        serde(
194            default,
195            skip_serializing_if = "Option::is_none",
196            deserialize_with = "cow_option_from_string"
197        )
198    )]
199    pub using: Option<Cow<'static, str>>,
200
201    /// WITH CHECK expression
202    #[cfg_attr(
203        feature = "serde",
204        serde(
205            default,
206            skip_serializing_if = "Option::is_none",
207            deserialize_with = "cow_option_from_string"
208        )
209    )]
210    pub with_check: Option<Cow<'static, str>>,
211}
212
213impl Policy {
214    /// Creates a policy.
215    #[must_use]
216    pub fn new(
217        schema: impl Into<Cow<'static, str>>,
218        table: impl Into<Cow<'static, str>>,
219        name: impl Into<Cow<'static, str>>,
220    ) -> Self {
221        Self {
222            schema: schema.into(),
223            table: table.into(),
224            name: name.into(),
225            as_clause: None,
226            for_clause: None,
227            to: None,
228            using: None,
229            with_check: None,
230        }
231    }
232
233    /// Returns the schema name.
234    #[inline]
235    #[must_use]
236    pub fn schema(&self) -> &str {
237        &self.schema
238    }
239
240    /// Returns the table name.
241    #[inline]
242    #[must_use]
243    pub fn table(&self) -> &str {
244        &self.table
245    }
246
247    /// Returns the policy name.
248    #[inline]
249    #[must_use]
250    pub fn name(&self) -> &str {
251        &self.name
252    }
253}
254
255impl From<PolicyDef> for Policy {
256    fn from(def: PolicyDef) -> Self {
257        def.into_policy()
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    #[test]
266    fn test_const_policy_def() {
267        const POLICY: PolicyDef = PolicyDef::new("public", "users", "users_policy")
268            .for_clause("SELECT")
269            .using("user_id = current_user_id()");
270
271        assert_eq!(POLICY.schema, "public");
272        assert_eq!(POLICY.table, "users");
273        assert_eq!(POLICY.name, "users_policy");
274    }
275
276    /// Optional fields are skipped when serializing; deserialization must
277    /// treat the missing keys as `None` instead of erroring.
278    #[cfg(feature = "serde")]
279    #[test]
280    fn test_serde_roundtrip_with_all_none_optionals() {
281        let policy = Policy::new("public", "users", "users_policy");
282        assert!(policy.as_clause.is_none());
283        assert!(policy.using.is_none());
284
285        let json = serde_json::to_string(&policy).expect("serialize");
286        let parsed: Policy = serde_json::from_str(&json).expect("deserialize");
287        assert_eq!(parsed, policy);
288    }
289
290    #[test]
291    fn test_policy_def_to_policy() {
292        const DEF: PolicyDef = PolicyDef::new("public", "users", "policy");
293        let policy = DEF.into_policy();
294        assert_eq!(policy.schema(), "public");
295        assert_eq!(policy.table(), "users");
296        assert_eq!(policy.name(), "policy");
297    }
298}