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