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
//! The one place arium's two authorization axes compose.
//!
//! Per-resource roles ([`crate::authz`], from `arium-authz`) and global RBAC
//! (flat permission tokens, from [`crate::auth`]) are deliberately blind to
//! each other. This bridge lives in the engine crate because it reads *both* —
//! the resource role via [`require_resource`] and the global token set via
//! [`crate::auth::list_permissions_for_user`]. `arium-authz` itself stays free
//! of any authn dependency.
use crate;
use cratePool;
use crateResourceRole;
/// Which axis authorized a [`require_resource_or_permission`] call.
///
/// Worth surfacing (e.g. in an audit row): a grant via [`Self::GlobalPermission`]
/// is an app-wide capability reaching *into* resource scope — a support agent
/// editing a board they don't belong to — and usually deserves louder logging
/// than the ordinary [`Self::Resource`] path.
/// Authorize on **either** axis: a sufficient per-resource role, **or** a global
/// permission token. The one place the two authorization stories compose.
///
/// arium's two axes are deliberately blind to each other ([`require_resource`]
/// never reads `User.permissions`; the global RBAC path never reads resource
/// state), which keeps each boundary simple but means *neither alone* answers
/// "can this user act?" when an app wants a global escape hatch — a super-admin
/// or support role that can touch resources they don't belong to. Rather than
/// have every call site re-derive "owner OR super-admin" (where the two drift),
/// express it once here.
///
/// Order and semantics: the resource check runs first; only on
/// [`ResourceAuthzError::Forbidden`] does it fall back to the global token set
/// ([`crate::auth::list_permissions_for_user`], which unions direct and
/// role-derived tokens). Default-deny is preserved — an absent role *and* a
/// missing token is [`ResourceAuthzError::Forbidden`] — and a storage failure on
/// **either** lookup surfaces as [`ResourceAuthzError::Lookup`], never a silent
/// deny. The return value names which axis let the caller through.
pub async
/// [`require_resource`] plus the standard audit-on-denial, driven by an
/// **already-resolved** `user_id`. The reusable kernel behind the framework
/// adapters' session guards (e.g. dioxus's `require_resource_dioxus`).
///
/// The session adapters bundle three things: resolving the caller, the
/// enforcement check, and writing a `resource.access.denied` row on refusal.
/// Only the first is framework-specific. An app that resolves the caller
/// through its *own* request context (not arium's session extractor) can't
/// reuse a guard that insists on the session — so it would re-implement the
/// audit-on-denial wrapper. This is that wrapper, with the caller resolution
/// hoisted out to a plain `user_id`.
///
/// On [`ResourceAuthzError::Forbidden`] it records a `resource.access.denied`
/// audit row — `actor_id = user_id`, details `{"kind","id","min_role"}` (the
/// canonical lowercase role via [`ResourceRole::as_str`]), stamped with
/// whatever IP/User-Agent the supplied [`AuditCtx`](crate::extract::AuditCtx)
/// carries — and then returns `Forbidden` unchanged. A [`Lookup`](ResourceAuthzError::Lookup) error passes
/// through untouched: a storage failure is never recast as a deny, and never
/// audited as one. The error is returned raw so each caller maps it to its own
/// surface (a `403`/`ServerFnError`, a `404` for an existence-hiding SSE path,
/// …). On success returns `user_id`, for reuse as the acting id.
pub async