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