1#[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#[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 #[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#[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 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 #[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}