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;