Skip to main content

sofie_http/gate/
flags.rs

1use sofie_core::{Error, Result};
2use sofie_flags::{Feature, Features, Flags, Rule, Subject};
3
4use super::Gates;
5use crate::answer::Response;
6use crate::caller::Caller;
7use crate::routes::Route;
8use crate::session::RequestSession;
9use crate::target::Target;
10use crate::{Area, RequestContext};
11use sofie_db::Resolve;
12
13pub struct RouteFeature {
14    pub method: &'static str,
15    pub path: &'static str,
16    pub feature: Feature,
17}
18
19#[derive(Default)]
20pub(super) struct FlagGate {
21    flags: Option<Flags>,
22    gated: &'static [RouteFeature],
23}
24
25impl Gates {
26    #[must_use]
27    pub fn features(mut self, flags: Flags) -> Self {
28        self.flags.flags = Some(flags);
29        self
30    }
31
32    /// Each is not found while its feature is off, as `.feature(…)` makes it.
33    #[must_use]
34    pub fn route_features(mut self, gated: &'static [RouteFeature]) -> Self {
35        self.flags.gated = gated;
36        self
37    }
38}
39
40impl FlagGate {
41    pub(super) fn held(&self) -> Option<Flags> {
42        self.flags.clone()
43    }
44
45    pub(super) fn prepare(&self, routes: &mut [Route]) -> Result<()> {
46        for gate in self.gated {
47            let route = routes
48                .iter_mut()
49                .find(|route| route.method.as_str() == gate.method && route.path == gate.path)
50                .ok_or_else(|| {
51                    Error::internal_message(format!(
52                        "the wiring gates route {} {}, which no module routes; run `sofie gen`",
53                        gate.method, gate.path
54                    ))
55                })?;
56            route.rules.feature = Some(gate.feature);
57        }
58        let flagged = routes.iter().find(|route| route.rules.feature.is_some());
59        if let Some(route) = flagged.filter(|_| self.flags.is_none()) {
60            return Err(no_flags(&format!(
61                "route {} {} has a feature flag",
62                route.method, route.path
63            )));
64        }
65        Ok(())
66    }
67
68    /// As a caller without a session sees it, which is all a CORS preflight carries.
69    #[cfg(feature = "cors")]
70    pub(super) fn hides_from_anyone(&self, target: &Target) -> bool {
71        let (Some(flags), Some(feature)) = (&self.flags, target.admission.rules.feature) else {
72            return false;
73        };
74        let anyone = Subject {
75            area: Some(target.audience.name),
76            principal: None,
77        };
78        !flags.evaluate(anyone).enabled(feature)
79    }
80
81    /// A route whose flag is off is not found, so a hidden feature is not revealed.
82    pub(super) fn open(
83        &self,
84        target: &Target,
85        session: &RequestSession,
86        caller: Caller<'_>,
87    ) -> Result<Option<Features>, Box<Response>> {
88        let Some(flags) = &self.flags else {
89            return Ok(None);
90        };
91        let area = target.audience.name;
92        let features = flags.evaluate(Subject {
93            area: Some(area),
94            principal: session.principal_in(area),
95        });
96        #[cfg(all(feature = "toolbar", debug_assertions))]
97        let features = features.overridden(std::sync::Arc::clone(&session.toolbar.flags));
98        let evaluated = features.clone();
99        sofie_telemetry::attach_feature_flags(&tracing::Span::current(), move || {
100            evaluated.evaluated()
101        });
102        if let Some(feature) = target.admission.rules.feature
103            && !features.enabled(feature)
104        {
105            tracing::info!(
106                feature_flag.key = feature.name(),
107                http.route = target.pattern.as_str(),
108                "not found: the route's feature is off"
109            );
110            return Err(Box::new(crate::answer::failure(Error::NotFound, caller)));
111        }
112        Ok(Some(features))
113    }
114}
115
116pub(super) fn record(features: Option<&Features>) {
117    let evaluated = features.map(Features::evaluated).unwrap_or_default();
118    if !evaluated.is_empty() {
119        tracing::Span::current().record("feature_flags", evaluated);
120    }
121}
122
123impl<A: Area, M> Resolve<RequestContext<A, M>> for Features {
124    fn resolve(context: &mut RequestContext<A, M>) -> impl Future<Output = Result<Self>> + Send {
125        let features = context
126            .matched
127            .gates
128            .features
129            .clone()
130            .ok_or_else(|| no_flags("a handler asks for `Features`"));
131        std::future::ready(features)
132    }
133}
134
135fn flags<A: Area, M>(context: &RequestContext<A, M>) -> Result<Flags> {
136    context
137        .matched
138        .gates
139        .flags
140        .clone()
141        .ok_or_else(|| no_flags("a handler asks for `Flags`"))
142}
143
144fn no_flags(asking: &str) -> Error {
145    Error::internal_message(format!(
146        "{asking}, but the app has no flags; give `Gates::features` the app's `Flags`"
147    ))
148}
149
150impl<A: Area> Resolve<RequestContext<A>> for Flags {
151    fn resolve(context: &mut RequestContext<A>) -> impl Future<Output = Result<Self>> + Send {
152        std::future::ready(flags(context))
153    }
154}
155
156/// What a GET may take: setting a flag needs `Flags`, in a POST.
157pub struct FlagRules(Flags);
158
159impl FlagRules {
160    #[must_use]
161    pub fn rules(&self) -> Vec<(String, Rule)> {
162        self.0.rules()
163    }
164}
165
166impl<A: Area, M> Resolve<RequestContext<A, M>> for FlagRules {
167    fn resolve(context: &mut RequestContext<A, M>) -> impl Future<Output = Result<Self>> + Send {
168        std::future::ready(flags(context).map(Self))
169    }
170}