Skip to main content

sofie_http/gate/
permit.rs

1use std::sync::Arc;
2
3use http::StatusCode;
4use sofie_core::{Error, Result};
5use sofie_db::{Db, Resolve};
6
7use crate::answer::{self, Response};
8use crate::caller::Caller;
9use crate::routes::Route;
10use crate::session::RequestSession;
11use crate::target::Target;
12use crate::{Access, Area, AreaMembers, RequestContext};
13
14/// Derived by `sofie gen`, never written by hand; a `*` axis stands for every value.
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub struct Permission(&'static str);
17
18impl Permission {
19    #[must_use]
20    pub const fn new(name: &'static str) -> Self {
21        Self(name)
22    }
23
24    #[must_use]
25    pub const fn name(self) -> &'static str {
26        self.0
27    }
28}
29
30#[must_use]
31pub fn is_permission(text: &str) -> bool {
32    let axis = |part: &str| {
33        part == "*"
34            || (part.starts_with(|first: char| first.is_ascii_lowercase())
35                && part.chars().all(|character| {
36                    character.is_ascii_lowercase() || character.is_ascii_digit() || character == '_'
37                }))
38    };
39    text.split('.').count() == 4 && text.split('.').all(axis)
40}
41
42/// A needed `*` (the whole record) is covered only by a `*`.
43fn covers(grant: &str, needed: &str) -> bool {
44    let mut needed_axes = needed.split('.');
45    let covered = grant.split('.').all(|axis| {
46        needed_axes
47            .next()
48            .is_some_and(|needed| axis == "*" || axis == needed)
49    });
50    covered && needed_axes.next().is_none()
51}
52
53pub struct RoutePermission {
54    pub method: &'static str,
55    pub path: &'static str,
56    pub permission: Permission,
57}
58
59/// Nothing outside a `Permitted` area.
60#[derive(Clone, Default, Hash)]
61pub struct Permissions {
62    granted: Arc<[String]>,
63    /// A bearer token's, narrowing what its principal was granted.
64    #[cfg(feature = "tokens")]
65    scopes: Option<Arc<[String]>>,
66}
67
68impl Permissions {
69    #[must_use]
70    pub fn can(&self, permission: Permission) -> bool {
71        self.can_named(permission.name())
72    }
73
74    #[must_use]
75    pub fn can_named(&self, permission: &str) -> bool {
76        let granted = self.holds(permission);
77        #[cfg(all(feature = "toolbar", debug_assertions))]
78        record_check(permission, granted);
79        granted
80    }
81
82    /// Unrecorded: the route's own check is a field of the request's span.
83    fn holds(&self, permission: &str) -> bool {
84        let granted = self.granted.iter().any(|grant| covers(grant, permission));
85        #[cfg(feature = "tokens")]
86        let granted = granted
87            && self
88                .scopes
89                .as_ref()
90                .is_none_or(|scopes| scopes.iter().any(|scope| covers(scope, permission)));
91        granted
92    }
93}
94
95/// A check a handler or screen makes shows in the development toolbar's span tree.
96#[cfg(all(feature = "toolbar", debug_assertions))]
97fn record_check(permission: &str, granted: bool) {
98    let outcome = if granted { "granted" } else { "refused" };
99    drop(tracing::info_span!("permission", otel.name = permission, permission = outcome).entered());
100}
101
102impl<A: Area, M> RequestContext<A, M> {
103    /// For a check the route's permission cannot make, such as granting no more than one holds.
104    #[must_use]
105    pub fn permissions(&self) -> &Permissions {
106        &self.matched.gates.permissions
107    }
108}
109
110impl<A: Area, M> Resolve<RequestContext<A, M>> for Permissions {
111    fn resolve(context: &mut RequestContext<A, M>) -> impl Future<Output = Result<Self>> + Send {
112        std::future::ready(Ok(context.permissions().clone()))
113    }
114}
115
116#[derive(Default)]
117pub(crate) struct Permits {
118    members: Option<Arc<dyn AreaMembers>>,
119    table: &'static [RoutePermission],
120}
121
122impl crate::Gates {
123    #[must_use]
124    pub fn route_permissions(mut self, table: &'static [RoutePermission]) -> Self {
125        self.permits.table = table;
126        self
127    }
128}
129
130/// For a framework crate's own routes, which `sofie gen` does not read.
131pub trait NeedsPermission {
132    fn permission(&mut self, permission: Permission) -> &mut Self;
133}
134
135impl<A: Area> NeedsPermission for crate::PageRoute<'_, A> {
136    fn permission(&mut self, permission: Permission) -> &mut Self {
137        self.rule(|rules| rules.permission = Some(permission))
138    }
139}
140
141impl<A: Area> NeedsPermission for crate::ActionRoute<'_, A> {
142    fn permission(&mut self, permission: Permission) -> &mut Self {
143        self.rule(|rules| rules.permission = Some(permission))
144    }
145}
146
147impl Permits {
148    pub(crate) async fn open(
149        &self,
150        target: &Target,
151        session: &RequestSession,
152        db: &Db,
153        caller: Caller<'_>,
154    ) -> Result<Permissions, Box<Response>> {
155        let audience = target.audience;
156        let principal = session.principal_in(audience.name);
157        let (Some(principal), Some(members), Access::Permitted) =
158            (principal, &self.members, audience.access)
159        else {
160            return Ok(Permissions::default());
161        };
162        let granted = members
163            .grants(&db.read(), audience.name, principal)
164            .await
165            .map_err(|error| Box::new(answer::failure(error, caller)))?;
166        let permissions = Permissions {
167            granted: granted.into(),
168            #[cfg(feature = "tokens")]
169            scopes: session.scopes(),
170        };
171        let needed = target.admission.rules.permission;
172        if needed.is_some_and(|needed| permissions.holds(needed.name())) {
173            tracing::Span::current().record("permission", "granted");
174            return Ok(permissions);
175        }
176        tracing::Span::current().record("permission", "refused");
177        tracing::warn!(
178            http.route = target.pattern.as_str(),
179            area = audience.name,
180            "refused a principal without the route's permission"
181        );
182        let refusal = answer::refusal(StatusCode::FORBIDDEN, "You may not do this.", caller);
183        Err(Box::new(refusal))
184    }
185
186    /// Every route of a `Permitted` area must need a permission, or nobody could be let in by rule.
187    pub(crate) fn prepare(
188        &mut self,
189        routes: &mut [Route],
190        members: Option<&Arc<dyn AreaMembers>>,
191    ) -> Result<()> {
192        self.members = members.cloned();
193        #[cfg(feature = "tokens")]
194        refuse_unscoped_tokens(routes)?;
195        refuse_unread_permissions(routes)?;
196        for route in routes
197            .iter_mut()
198            .filter(|route| route.audience.access == Access::Permitted)
199        {
200            let listed = self
201                .table
202                .iter()
203                .find(|entry| entry.method == route.method.as_str() && entry.path == route.path)
204                .map(|entry| entry.permission);
205            let permission = route.rules.permission.or(listed).ok_or_else(|| {
206                Error::internal_message(format!(
207                    "route {} {} of area `{}` needs a permission, but the wiring names none; run `sofie gen`",
208                    route.method, route.path, route.audience.name
209                ))
210            })?;
211            if self.members.is_none() {
212                return Err(Error::internal_message(format!(
213                    "area `{}` admits by permission, but the app has no members to hold grants; give `App::membership` the identity's memberships",
214                    route.audience.name
215                )));
216            }
217            route.rules.permission = Some(permission);
218        }
219        Ok(())
220    }
221}
222
223/// Only a `Permitted` area reads grants, so a permission named elsewhere would check nobody.
224fn refuse_unread_permissions(routes: &[Route]) -> Result<()> {
225    let unread = routes.iter().find_map(|route| {
226        let permission = route.rules.permission?;
227        (route.audience.access != Access::Permitted).then_some((route, permission))
228    });
229    let Some((route, permission)) = unread else {
230        return Ok(());
231    };
232    Err(Error::internal_message(format!(
233        "route {} {} needs `{}`, but area `{}` does not admit by permission, so no grant would be checked; mount it in a `Permitted` area",
234        route.method,
235        route.path,
236        permission.name(),
237        route.audience.name
238    )))
239}
240
241/// A token's scopes narrow grants, which only a `Permitted` area reads.
242#[cfg(feature = "tokens")]
243fn refuse_unscoped_tokens(routes: &[Route]) -> Result<()> {
244    let unscoped = routes
245        .iter()
246        .find(|route| route.audience.tokens && route.audience.access != Access::Permitted);
247    match unscoped {
248        Some(route) => Err(Error::internal_message(format!(
249            "area `{}` takes bearer tokens, but only a `Permitted` area holds a token to its scopes",
250            route.audience.name
251        ))),
252        None => Ok(()),
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn a_grant_covers_what_matches_it_axis_by_axis() {
262        assert!(covers("order.order.*.cancel", "order.order.*.cancel"));
263        assert!(covers("order.*.*.*", "order.order.total.read"));
264        assert!(covers("*.*.*.*", "order.order.*.cancel"));
265        assert!(!covers("order.order.total.cancel", "order.order.*.cancel"));
266        assert!(!covers("order.order.*.read", "order.order.*.cancel"));
267        assert!(!covers("order.order.*", "order.order.*.read"));
268    }
269
270    #[cfg(all(feature = "toolbar", debug_assertions))]
271    #[test]
272    fn a_check_made_in_a_handler_is_recorded_for_the_toolbar() {
273        let telemetry = sofie_telemetry::capture();
274        let permissions = Permissions {
275            granted: Arc::from(["order.*.*.*".to_owned()]),
276            #[cfg(feature = "tokens")]
277            scopes: None,
278        };
279
280        assert!(permissions.can(Permission::new("order.order.*.cancel")));
281        assert!(!permissions.can_named("user.user.*.read"));
282
283        let checks: Vec<(String, String)> = telemetry
284            .spans()
285            .into_iter()
286            .filter(|span| span.name == "permission")
287            .map(|span| {
288                (
289                    span.fields["otel.name"].clone(),
290                    span.fields["permission"].clone(),
291                )
292            })
293            .collect();
294        assert_eq!(
295            checks,
296            [
297                ("order.order.*.cancel".to_owned(), "granted".to_owned()),
298                ("user.user.*.read".to_owned(), "refused".to_owned()),
299            ]
300        );
301    }
302
303    #[test]
304    fn a_permission_has_four_named_or_wildcard_axes() {
305        assert!(is_permission("order.order.*.cancel"));
306        assert!(is_permission("*.*.*.*"));
307        for refused in [
308            "order.order.cancel",
309            "order.Order.*.read",
310            "order..*.read",
311            "a.b.c.d.e",
312            "order.order.**.read",
313        ] {
314            assert!(!is_permission(refused), "{refused}");
315        }
316    }
317}