sofie_http/gate/
entitled.rs1use 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#[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
32pub 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 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 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}