Skip to main content

tablo_core/
policy.rs

1//! Authorizes resource abilities, denying by default.
2//!
3//! Combines [`Allow`], [`Deny`], [`ReadOnly`], [`when`], and closures with
4//! `and`/`or`.
5//!
6//! ```rust
7//! # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
8//! # struct Post { #[key] #[auto] id: uuid::Uuid, locked: bool }
9//! # use tablo_core::{Ability, Policy, when};
10//! # use topcoat::context::Cx;
11//! # fn not_suspended(cx: &Cx) -> bool { true }
12//! fn post_policy() -> impl Policy<Post> {
13//!     when(not_suspended).and(|_cx: &Cx, ability: Ability<'_, Post>| match ability {
14//!         Ability::Update(post) | Ability::Delete(post) | Ability::Run { record: post, .. } => {
15//!             !post.locked
16//!         }
17//!         _ => true,
18//!     })
19//! }
20//! ```
21//!
22//! [`can`](crate::can) asks a resource's policy from app code, and [`can_list`](crate::can_list)
23//! answers whether the current request may open a resource's list at all.
24
25use derive_where::derive_where;
26use topcoat::context::Cx;
27
28/// One thing a policy is asked to allow; record abilities are asked once per
29/// loaded row, and a record that cannot be viewed cannot be written by guessing
30/// its key.
31#[derive(Debug)]
32#[derive_where(Clone, Copy)]
33pub enum Ability<'a, M> {
34    /// Open the list, export it, and offer the records as relationship options.
35    ViewAny,
36    /// See one record.
37    View(&'a M),
38    /// Open the create form and submit it.
39    Create,
40    /// Edit one record.
41    Update(&'a M),
42    /// Delete at all.
43    DeleteAny,
44    /// Delete one record.
45    Delete(&'a M),
46    /// Run the custom [`Action`](crate::Action) whose [`NAME`](crate::Action::NAME) is `action`
47    /// at all, before [`Run`](Self::Run) is asked of each record.
48    RunAny {
49        /// The action's [`NAME`](crate::Action::NAME).
50        action: &'static str,
51    },
52    /// Run the custom [`Action`](crate::Action) whose [`NAME`](crate::Action::NAME) is `action`
53    /// on one record.
54    Run {
55        /// The action's [`NAME`](crate::Action::NAME).
56        action: &'static str,
57        /// The record it runs on.
58        record: &'a M,
59    },
60    /// Run the [`HeaderAction`](crate::HeaderAction) whose `NAME` is `action` from the list's
61    /// header. It acts on no record, so nothing else of the policy is asked but `ViewAny`.
62    RunHeader {
63        /// The action's [`NAME`](crate::HeaderAction::NAME).
64        action: &'static str,
65    },
66}
67
68impl<'a, M> Ability<'a, M> {
69    /// The record the ability names, if any.
70    pub fn record(self) -> Option<&'a M> {
71        match self {
72            Self::View(record)
73            | Self::Update(record)
74            | Self::Delete(record)
75            | Self::Run { record, .. } => Some(record),
76            Self::ViewAny
77            | Self::Create
78            | Self::DeleteAny
79            | Self::RunAny { .. }
80            | Self::RunHeader { .. } => None,
81        }
82    }
83
84    /// Whether the ability only reads.
85    pub fn is_read(self) -> bool {
86        matches!(self, Self::ViewAny | Self::View(_))
87    }
88}
89
90/// Decides which [`Ability`]s the current user has over a resource's records.
91///
92/// A closure `|cx: &Cx, ability: Ability<'_, M>| -> bool` is a policy, and the
93/// building blocks combine with `and` and `or`.
94/// [`ResourceDef::policy_fn`](crate::ResourceDef::policy_fn) takes a closure without naming its
95/// argument types.
96pub trait Policy<M>: Send + Sync + 'static {
97    /// Whether the current user may do `ability`.
98    fn allows(&self, cx: &Cx, ability: Ability<'_, M>) -> bool;
99}
100
101macro_rules! combinators {
102    ($($ty:ident $(<$($param:ident),+>)?),+ $(,)?) => {$(
103        impl$(<$($param),+>)? $ty$(<$($param),+>)? {
104            /// Allows what both `self` and `other` allow.
105            pub fn and<P>(self, other: P) -> And<Self, P> {
106                And(self, other)
107            }
108
109            /// Allows what either `self` or `other` allows.
110            pub fn or<P>(self, other: P) -> Or<Self, P> {
111                Or(self, other)
112            }
113        }
114    )+};
115}
116
117combinators!(Allow, Deny, ReadOnly, When<F>, And<A, B>, Or<A, B>);
118
119impl<M, F> Policy<M> for F
120where
121    F: Fn(&Cx, Ability<'_, M>) -> bool + Send + Sync + 'static,
122{
123    fn allows(&self, cx: &Cx, ability: Ability<'_, M>) -> bool {
124        self(cx, ability)
125    }
126}
127
128/// Allows every ability.
129#[derive(Debug, Clone, Copy, Default)]
130pub struct Allow;
131
132impl<M> Policy<M> for Allow {
133    fn allows(&self, _cx: &Cx, _ability: Ability<'_, M>) -> bool {
134        true
135    }
136}
137
138/// Allows nothing: the default policy.
139#[derive(Debug, Clone, Copy, Default)]
140pub struct Deny;
141
142impl<M> Policy<M> for Deny {
143    fn allows(&self, _cx: &Cx, _ability: Ability<'_, M>) -> bool {
144        false
145    }
146}
147
148/// Allows listing and viewing, and no write: no create, update, delete or custom action.
149#[derive(Debug, Clone, Copy, Default)]
150pub struct ReadOnly;
151
152impl<M> Policy<M> for ReadOnly {
153    fn allows(&self, _cx: &Cx, ability: Ability<'_, M>) -> bool {
154        ability.is_read()
155    }
156}
157
158/// Allows every ability while `predicate` holds for the request.
159///
160/// ```rust
161/// # #[derive(Debug, Clone, toasty::Model)]
162/// # struct Staff { #[key] #[auto] id: uuid::Uuid, editor: bool }
163/// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
164/// # struct Post { #[key] #[auto] id: uuid::Uuid }
165/// # use tablo_core::{PanelUser, Policy, ReadOnly, auth, when};
166/// # use topcoat::context::Cx;
167/// # impl PanelUser for Staff {
168/// #     fn user_id(&self) -> String { String::new() }
169/// #     fn display_name(&self) -> &str { "" }
170/// # }
171/// fn editor(cx: &Cx) -> bool {
172///     auth::user::<Staff>(cx).is_some_and(|staff| staff.editor)
173/// }
174///
175/// fn post_policy() -> impl Policy<Post> {
176///     ReadOnly.or(when(editor))
177/// }
178/// ```
179pub fn when<F>(predicate: F) -> When<F>
180where
181    F: Fn(&Cx) -> bool + Send + Sync + 'static,
182{
183    When(predicate)
184}
185
186/// The policy [`when`] returns.
187#[derive(Debug, Clone, Copy)]
188pub struct When<F>(F);
189
190impl<M, F> Policy<M> for When<F>
191where
192    F: Fn(&Cx) -> bool + Send + Sync + 'static,
193{
194    fn allows(&self, cx: &Cx, _ability: Ability<'_, M>) -> bool {
195        (self.0)(cx)
196    }
197}
198
199/// The policy `and` returns: allows what both sides allow.
200#[derive(Debug, Clone, Copy)]
201pub struct And<A, B>(A, B);
202
203impl<M, A: Policy<M>, B: Policy<M>> Policy<M> for And<A, B> {
204    fn allows(&self, cx: &Cx, ability: Ability<'_, M>) -> bool {
205        self.0.allows(cx, ability) && self.1.allows(cx, ability)
206    }
207}
208
209/// The policy `or` returns: allows what either side allows.
210#[derive(Debug, Clone, Copy)]
211pub struct Or<A, B>(A, B);
212
213impl<M, A: Policy<M>, B: Policy<M>> Policy<M> for Or<A, B> {
214    fn allows(&self, cx: &Cx, ability: Ability<'_, M>) -> bool {
215        self.0.allows(cx, ability) || self.1.allows(cx, ability)
216    }
217}
218
219#[cfg(test)]
220mod tests;