Skip to main content

sofie_http/gate/
entitled.rs

1use std::sync::Arc;
2
3use http::StatusCode;
4use sofie_core::{Result, Uuid};
5use sofie_db::{BoxFuture, Db, Read};
6
7use super::Gates;
8use crate::Access;
9use crate::answer::{self, Response};
10use crate::caller::Caller;
11#[cfg(feature = "engine")]
12use crate::routes::View;
13use crate::session::RequestSession;
14use crate::target::Target;
15
16/// Named `<module>.<entitlement>` like a flag; what grants one is the app's billing.
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub struct Entitlement(&'static str);
19
20impl Entitlement {
21    #[must_use]
22    pub const fn new(name: &'static str) -> Self {
23        Self(name)
24    }
25
26    #[must_use]
27    pub const fn name(self) -> &'static str {
28        self.0
29    }
30}
31
32/// What a principal's plan holds, read once per request that needs it.
33pub trait Entitled: Send + Sync + 'static {
34    fn held<'a>(
35        &'a self,
36        read: &'a Read,
37        area: &'static str,
38        principal: Uuid,
39    ) -> BoxFuture<'a, Result<Arc<[&'static str]>>>;
40}
41
42impl Gates {
43    #[must_use]
44    pub fn entitlements(mut self, source: impl Entitled) -> Self {
45        self.entitled = Some(Arc::new(source));
46        self
47    }
48
49    /// Read for a route needing an entitlement and for any page, whose screen or menu may ask.
50    pub(super) async fn entitled(
51        &self,
52        target: &Target,
53        session: &RequestSession,
54        db: &Db,
55        caller: Caller<'_>,
56    ) -> Result<Arc<[&'static str]>, Box<Response>> {
57        let needed = target.admission.rules.entitlement;
58        let Some(source) = self
59            .entitled
60            .as_ref()
61            .filter(|_| needed.is_some() || renders(target))
62        else {
63            return Ok(Arc::from([]));
64        };
65        let audience = target.audience;
66        let held = match session.principal_in(audience.name) {
67            Some(principal) => source
68                .held(&db.read(), audience.name, principal)
69                .await
70                .map_err(|error| Box::new(answer::failure(error, caller)))?,
71            // A signed-in area's guard sends the visitor to sign in first.
72            None if audience.access != Access::Public => return Ok(Arc::from([])),
73            None => Arc::from([]),
74        };
75        if let Some(needed) = needed
76            && !held.contains(&needed.name())
77        {
78            tracing::info!(
79                entitlement = needed.name(),
80                http.route = target.pattern.as_str(),
81                "refused a principal whose plan lacks the route's entitlement"
82            );
83            let refusal = answer::refusal(
84                StatusCode::FORBIDDEN,
85                "Your plan does not include this.",
86                caller,
87            );
88            return Err(Box::new(refusal));
89        }
90        Ok(held)
91    }
92}
93
94#[cfg(feature = "engine")]
95fn renders(target: &Target) -> bool {
96    target.page.is_some()
97        || !matches!(
98            target.view,
99            View::Json
100                | View::Document(_)
101                | View::Redirect(_)
102                | View::Follow
103                | View::RedirectOrFollow(_)
104        )
105}
106
107#[cfg(not(feature = "engine"))]
108fn renders(_target: &Target) -> bool {
109    false
110}