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