1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
//! Per-row permission hooks for the admin: one hook per
//! (model, add/change/delete/view) pair.
//!
//! The admin already gates routes by permission codename and by the
//! `Builder::read_only` allowlist. This module adds a per-row layer:
//! an inventory registry of
//! `(table, action, fn(&Parts, Option<&Value>) -> bool)` entries. The
//! create, detail, edit, update and delete handlers all consult it and
//! return 403 when a hook for that action denies.
//!
//! `row` is `None` for a collection-level check, such as "may this
//! user reach the add form?", and `Some(&json)` for a row check.
//!
//! ## Usage
//!
//! ```ignore
//! use axum::http::request::Parts;
//! use serde_json::Value;
//!
//! // Only allow the row's owner to edit / delete.
//! fn owner_only(parts: &Parts, row: Option<&Value>) -> bool {
//! let Some(row) = row else { return true; }; // no object → bail
//! let user_id = parts.extensions.get::<i64>().copied().unwrap_or(0);
//! row.get("owner_id").and_then(Value::as_i64) == Some(user_id)
//! }
//! rustango::register_admin_object_permission!("blog_post", "change", owner_only);
//! rustango::register_admin_object_permission!("blog_post", "delete", owner_only);
//! ```
//!
//! Now `/admin/blog_post/<id>/edit` and the delete POST both return
//! 403 when the user does not own the row. `"add"` and `"view"` stay
//! allowed, because no hook was registered for them.
//!
//! ## Compose
//!
//! Every hook on the same `(table, action)` must return `true` for the
//! action to be allowed. The first `false` wins and the rest are not
//! called. A hook still runs for a table hidden by `show_only`, as
//! long as the route is mounted.
//!
//! ## Action names
//!
//! Action names are plain strings, not an enum, so a new action such
//! as `"approve"` needs no change here. The built-in handlers use
//! `"add"`, `"change"`, `"delete"` and `"view"`. A custom view can
//! call [`is_allowed`] with any name of its own.
use Parts;
use Value;
/// An admin object-permission hook.
///
/// `row` is `Some(&json)` for a row-level action and `None` for a
/// collection-level one. Return `true` to allow, `false` to deny.
/// Hooks are ANDed together and the first `false` wins.
///
/// A plain `fn` pointer, not `Arc<dyn Fn>`, so the registration fits
/// in inventory's const storage, as in every other registry here.
pub type ObjectPermissionFn = fn ;
/// One hook registration for a table and action, collected by
/// inventory via [`crate::register_admin_object_permission!`].
collect!;
/// `true` when every hook for `(table, action)` returns `true`, or
/// when none is registered. The first `false` wins. The built-in
/// handlers call this before they let a write through.
/// Register a permission predicate for one model.
///
/// `$action` is usually `"add"`, `"change"`, `"delete"` or `"view"`,
/// which the built-in handlers check. Any other name works too: call
/// [`is_allowed`] with it from your own code.
///
/// ```ignore
/// fn allowed(_parts: &axum::http::request::Parts, _row: Option<&serde_json::Value>) -> bool {
/// true
/// }
/// rustango::register_admin_object_permission!("blog_post", "change", allowed);
/// ```