Skip to main content

sofie_http/gate/
mod.rs

1//! Without `flags`, `permissions` and `entitlements` the gates are empty and let everything through.
2
3#[cfg(feature = "entitlements")]
4mod entitled;
5#[cfg(feature = "flags")]
6mod flags;
7#[cfg(any(feature = "listing", feature = "menu", feature = "groups"))]
8pub(crate) mod optional;
9#[cfg(feature = "permissions")]
10mod permit;
11
12#[cfg(any(feature = "menu", feature = "entitlements"))]
13use std::sync::Arc;
14
15use sofie_core::Result;
16use sofie_db::Db;
17#[cfg(feature = "engine")]
18use sofie_db::Reply;
19
20#[cfg(feature = "entitlements")]
21pub use entitled::{Entitled, Entitlement};
22#[cfg(feature = "flags")]
23pub use flags::{FlagRules, RouteFeature};
24#[cfg(any(feature = "listing", feature = "menu", feature = "groups"))]
25pub use optional::*;
26#[cfg(feature = "permissions")]
27pub use permit::{NeedsPermission, Permission, Permissions, RoutePermission, is_permission};
28
29use crate::answer::Response;
30use crate::caller::Caller;
31use crate::session::RequestSession;
32use crate::target::Target;
33
34#[derive(Default)]
35pub struct Gates {
36    #[cfg(feature = "flags")]
37    flags: flags::FlagGate,
38    #[cfg(feature = "permissions")]
39    pub(crate) permits: permit::Permits,
40    #[cfg(feature = "menu")]
41    pub(crate) menus: Arc<optional::menu::Menus>,
42    #[cfg(feature = "entitlements")]
43    entitled: Option<Arc<dyn Entitled>>,
44}
45
46#[derive(Clone, Default)]
47pub(crate) struct RequestGates {
48    #[cfg(feature = "flags")]
49    pub(crate) features: Option<sofie_flags::Features>,
50    #[cfg(feature = "flags")]
51    pub(crate) flags: Option<sofie_flags::Flags>,
52    #[cfg(feature = "permissions")]
53    pub(crate) permissions: crate::Permissions,
54    #[cfg(feature = "menu")]
55    pub(crate) menus: Arc<optional::menu::Menus>,
56    #[cfg(feature = "entitlements")]
57    #[cfg_attr(
58        not(feature = "engine"),
59        expect(dead_code, reason = "only screens and menus read it")
60    )]
61    pub(crate) entitled: Arc<[&'static str]>,
62}
63
64impl Gates {
65    #[cfg(all(feature = "flags", feature = "cors"))]
66    pub(crate) fn hides_from_anyone(&self, target: &Target) -> bool {
67        self.flags.hides_from_anyone(target)
68    }
69
70    #[cfg_attr(
71        not(any(feature = "permissions", feature = "entitlements")),
72        expect(
73            clippy::unused_async_trait_impl,
74            reason = "only grants and entitlements are read from the database"
75        )
76    )]
77    pub(crate) async fn open(
78        &self,
79        target: &Target,
80        session: &RequestSession,
81        db: &Db,
82        caller: Caller<'_>,
83    ) -> Result<RequestGates, Box<Response>> {
84        let _ = (target, session, db, caller);
85        Ok(RequestGates {
86            #[cfg(feature = "flags")]
87            features: self.flags.open(target, session, caller)?,
88            #[cfg(feature = "flags")]
89            flags: self.flags.held(),
90            #[cfg(feature = "permissions")]
91            permissions: self.permits.open(target, session, db, caller).await?,
92            #[cfg(feature = "menu")]
93            menus: Arc::clone(&self.menus),
94            #[cfg(feature = "entitlements")]
95            entitled: self.entitled(target, session, db, caller).await?,
96        })
97    }
98}
99
100/// Must run before the router is built.
101#[cfg_attr(
102    not(any(
103        feature = "flags",
104        feature = "permissions",
105        feature = "resources",
106        feature = "menu"
107    )),
108    expect(
109        clippy::unnecessary_wraps,
110        reason = "only flags, permissions, resources and menus are prepared"
111    )
112)]
113pub(crate) fn prepare(app: &mut crate::App) -> Result<()> {
114    let _ = &app;
115    #[cfg(feature = "flags")]
116    app.gates.flags.prepare(&mut app.routes)?;
117    #[cfg(feature = "permissions")]
118    app.gates
119        .permits
120        .prepare(&mut app.routes, app.members.as_ref())?;
121    #[cfg(any(feature = "resources", feature = "menu"))]
122    optional::prepare(app)?;
123    Ok(())
124}
125
126impl RequestGates {
127    /// An action refreshing another route's page, or showing it refused, shows only what that
128    /// page's own gates would.
129    #[cfg(feature = "engine")]
130    #[cfg_attr(
131        not(any(feature = "flags", feature = "permissions", feature = "entitlements")),
132        expect(clippy::unused_self, reason = "no gate is compiled in")
133    )]
134    pub(crate) fn admit(&self, page: &crate::target::ScreenRoute) -> bool {
135        let _ = page;
136        #[cfg(feature = "flags")]
137        if let Some(feature) = page.feature
138            && !self
139                .features
140                .as_ref()
141                .is_some_and(|features| features.enabled(feature))
142        {
143            return false;
144        }
145        #[cfg(feature = "permissions")]
146        if let Some(permission) = page.permission
147            && !self.permissions.can(permission)
148        {
149            return false;
150        }
151        #[cfg(feature = "entitlements")]
152        if let Some(entitlement) = page.entitlement
153            && !self.entitled.contains(&entitlement.name())
154        {
155            return false;
156        }
157        true
158    }
159
160    #[cfg_attr(
161        not(feature = "flags"),
162        expect(clippy::unused_self, reason = "no flags are compiled in")
163    )]
164    pub(crate) fn record(&self) {
165        #[cfg(feature = "flags")]
166        flags::record(self.features.as_ref());
167    }
168}
169
170/// Answered as the routes were: nothing outside a `Permitted` area.
171#[cfg(feature = "engine")]
172impl sofie_engine::rendering::Gates for RequestGates {
173    fn can(&self, permission: &str) -> bool {
174        #[cfg(feature = "permissions")]
175        return self.permissions.can_named(permission);
176        #[cfg(not(feature = "permissions"))]
177        {
178            let _ = permission;
179            false
180        }
181    }
182
183    fn enabled(&self, flag: &str) -> bool {
184        #[cfg(feature = "flags")]
185        return self
186            .features
187            .as_ref()
188            .is_some_and(|features| features.enabled_named(flag));
189        #[cfg(not(feature = "flags"))]
190        {
191            let _ = flag;
192            false
193        }
194    }
195
196    #[cfg(feature = "entitlements")]
197    fn entitled(&self, entitlement: &str) -> bool {
198        self.entitled.contains(&entitlement)
199    }
200}
201
202#[cfg(feature = "engine")]
203impl crate::screen::ScreenPage<'_> {
204    pub(crate) fn rendering(&self) -> Gated<'_> {
205        let gates = self.answer.gates;
206        #[cfg(feature = "i18n")]
207        let rendering = self.language.screen(self.template).gates(gates);
208        #[cfg(not(feature = "i18n"))]
209        let rendering = sofie_engine::rendering::Rendering::new(self.template).gates(gates);
210        #[cfg(feature = "idempotency")]
211        let rendering = rendering.keyed_forms();
212        Gated {
213            rendering,
214            #[cfg(feature = "menu")]
215            navigation: gates.menus.navigation(
216                &self.answer.target.audience,
217                self.answer.asked.request.uri.path(),
218                gates,
219            ),
220        }
221    }
222}
223
224#[cfg(feature = "engine")]
225impl crate::screen::ScreenPage<'_> {
226    /// Answered as the page would be shown: without what this viewer is shown nothing of.
227    pub(crate) fn withheld(&self, reply: Result<Reply>) -> Result<Reply> {
228        let withheld = self.withheld_paths();
229        if withheld.is_empty() || matches!(reply, Ok(Reply::Empty)) {
230            return reply;
231        }
232        let mut data = reply?.into_json()?;
233        withheld.apply_to(&mut data);
234        Ok(Reply::Json(data))
235    }
236
237    #[cfg(feature = "clients")]
238    pub(crate) fn withhold(&self, data: &mut serde_json::Value) {
239        self.withheld_paths().apply_to(data);
240    }
241
242    fn withheld_paths(&self) -> sofie_engine::rendering::Withheld<'_> {
243        sofie_engine::rendering::Rendering::new(self.template)
244            .gates(self.answer.gates)
245            .withheld()
246    }
247}
248
249#[cfg(feature = "clients")]
250impl crate::screen::ScreenPage<'_> {
251    /// The area's menu as this viewer may follow it.
252    #[cfg_attr(
253        not(feature = "menu"),
254        expect(clippy::unused_self, reason = "only an app with menus has entries")
255    )]
256    pub(crate) fn navigation_entries(&self) -> serde_json::Value {
257        #[cfg(feature = "menu")]
258        return self.answer.gates.menus.entries(
259            &self.answer.target.audience,
260            self.answer.asked.request.uri.path(),
261            self.answer.gates,
262        );
263        #[cfg(not(feature = "menu"))]
264        serde_json::Value::Array(Vec::new())
265    }
266}
267
268#[cfg(feature = "engine")]
269pub(crate) struct Gated<'a> {
270    rendering: sofie_engine::rendering::Rendering<'a>,
271    #[cfg(feature = "menu")]
272    navigation: String,
273}
274
275#[cfg(feature = "engine")]
276impl Gated<'_> {
277    #[cfg(feature = "clients")]
278    pub(crate) fn client_view(&self) -> sofie_engine::ClientView {
279        self.rendering.client_view()
280    }
281
282    #[cfg(all(feature = "clients", feature = "listing"))]
283    pub(crate) fn translate_listing_errors(&self, data: &mut serde_json::Value) {
284        self.rendering.translate_listing_errors(data);
285    }
286
287    pub(crate) fn render(
288        &self,
289        data: &dyn sofie_core::Show,
290    ) -> Result<String, sofie_engine::TemplateError> {
291        self.rendering
292            .render(data)
293            .map(|body| self.under_menu(body))
294    }
295
296    pub(crate) fn render_submitted(
297        &self,
298        data: &dyn sofie_core::Show,
299        submitted: &sofie_engine::Submitted<'_>,
300    ) -> Result<String, sofie_engine::TemplateError> {
301        self.rendering
302            .render_submitted(data, submitted)
303            .map(|body| self.under_menu(body))
304    }
305
306    pub(crate) fn render_fragment(
307        &self,
308        data: &dyn sofie_core::Show,
309        id: &str,
310    ) -> Result<String, sofie_engine::RenderError> {
311        self.rendering.render_fragment(data, id)
312    }
313
314    pub(crate) fn render_out_of_band(
315        &self,
316        data: &dyn sofie_core::Show,
317        id: &str,
318    ) -> Result<String, sofie_engine::RenderError> {
319        self.rendering.render_out_of_band(data, id)
320    }
321
322    #[cfg_attr(
323        not(feature = "menu"),
324        expect(clippy::unused_self, unused_mut, reason = "only a menu comes first")
325    )]
326    fn under_menu(&self, mut body: String) -> String {
327        #[cfg(feature = "menu")]
328        body.insert_str(0, &self.navigation);
329        body
330    }
331}