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