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        Gated {
211            rendering,
212            #[cfg(feature = "menu")]
213            navigation: gates.menus.navigation(
214                &self.answer.target.audience,
215                self.answer.asked.request.uri.path(),
216                gates,
217            ),
218        }
219    }
220}
221
222#[cfg(feature = "engine")]
223impl crate::screen::ScreenPage<'_> {
224    /// Answered as the page would be shown: without what this viewer is shown nothing of.
225    pub(crate) fn withheld(&self, reply: Result<Reply>) -> Result<Reply> {
226        let withheld = self.withheld_paths();
227        if withheld.is_empty() || matches!(reply, Ok(Reply::Empty)) {
228            return reply;
229        }
230        let mut data = reply?.into_json()?;
231        withheld.apply_to(&mut data);
232        Ok(Reply::Json(data))
233    }
234
235    #[cfg(feature = "clients")]
236    pub(crate) fn withhold(&self, data: &mut serde_json::Value) {
237        self.withheld_paths().apply_to(data);
238    }
239
240    fn withheld_paths(&self) -> sofie_engine::rendering::Withheld<'_> {
241        sofie_engine::rendering::Rendering::new(self.template)
242            .gates(self.answer.gates)
243            .withheld()
244    }
245}
246
247#[cfg(feature = "clients")]
248impl crate::screen::ScreenPage<'_> {
249    /// The area's menu as this viewer may follow it.
250    #[cfg_attr(
251        not(feature = "menu"),
252        expect(clippy::unused_self, reason = "only an app with menus has entries")
253    )]
254    pub(crate) fn navigation_entries(&self) -> serde_json::Value {
255        #[cfg(feature = "menu")]
256        return self.answer.gates.menus.entries(
257            &self.answer.target.audience,
258            self.answer.asked.request.uri.path(),
259            self.answer.gates,
260        );
261        #[cfg(not(feature = "menu"))]
262        serde_json::Value::Array(Vec::new())
263    }
264}
265
266#[cfg(feature = "engine")]
267pub(crate) struct Gated<'a> {
268    rendering: sofie_engine::rendering::Rendering<'a>,
269    #[cfg(feature = "menu")]
270    navigation: String,
271}
272
273#[cfg(feature = "engine")]
274impl Gated<'_> {
275    #[cfg(feature = "clients")]
276    pub(crate) fn client_view(&self) -> sofie_engine::ClientView {
277        self.rendering.client_view()
278    }
279
280    #[cfg(all(feature = "clients", feature = "listing"))]
281    pub(crate) fn translate_listing_errors(&self, data: &mut serde_json::Value) {
282        self.rendering.translate_listing_errors(data);
283    }
284
285    pub(crate) fn render(
286        &self,
287        data: &dyn sofie_core::Show,
288    ) -> Result<String, sofie_engine::TemplateError> {
289        self.rendering
290            .render(data)
291            .map(|body| self.under_menu(body))
292    }
293
294    pub(crate) fn render_submitted(
295        &self,
296        data: &dyn sofie_core::Show,
297        submitted: &sofie_engine::Submitted<'_>,
298    ) -> Result<String, sofie_engine::TemplateError> {
299        self.rendering
300            .render_submitted(data, submitted)
301            .map(|body| self.under_menu(body))
302    }
303
304    pub(crate) fn render_fragment(
305        &self,
306        data: &dyn sofie_core::Show,
307        id: &str,
308    ) -> Result<String, sofie_engine::RenderError> {
309        self.rendering.render_fragment(data, id)
310    }
311
312    pub(crate) fn render_out_of_band(
313        &self,
314        data: &dyn sofie_core::Show,
315        id: &str,
316    ) -> Result<String, sofie_engine::RenderError> {
317        self.rendering.render_out_of_band(data, id)
318    }
319
320    #[cfg_attr(
321        not(feature = "menu"),
322        expect(clippy::unused_self, unused_mut, reason = "only a menu comes first")
323    )]
324    fn under_menu(&self, mut body: String) -> String {
325        #[cfg(feature = "menu")]
326        body.insert_str(0, &self.navigation);
327        body
328    }
329}