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;