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;