objectstore_server/
killswitches.rs

1use std::collections::BTreeMap;
2
3use objectstore_service::id::ObjectContext;
4use serde::{Deserialize, Serialize};
5
6/// A list of killswitches that may disable access to certain object contexts.
7#[derive(Debug, Default, Deserialize, Serialize)]
8pub struct Killswitches(pub Vec<Killswitch>);
9
10impl Killswitches {
11    /// Returns `true` if any of the contained killswitches matches the given context.
12    pub fn matches(&self, context: &ObjectContext) -> bool {
13        self.0.iter().any(|s| s.matches(context))
14    }
15}
16
17/// A killswitch that may disable access to certain object contexts.
18///
19/// Note that at least one of the fields should be set, or else the killswitch will match all
20/// contexts and discard all requests.
21#[derive(Debug, PartialEq, Deserialize, Serialize)]
22pub struct Killswitch {
23    /// Optional usecase to match.
24    ///
25    /// If `None`, matches any usecase.
26    #[serde(default)]
27    pub usecase: Option<String>,
28
29    /// Scopes to match.
30    ///
31    /// If empty, matches any scopes. Additional scopes in the context are ignored, so a killswitch
32    /// matches if all of the specified scopes are present in the request with matching values.
33    #[serde(default)]
34    pub scopes: BTreeMap<String, String>,
35}
36
37impl Killswitch {
38    /// Returns `true` if this killswitch matches the given context.
39    pub fn matches(&self, context: &ObjectContext) -> bool {
40        if let Some(ref switch_usecase) = self.usecase
41            && switch_usecase != &context.usecase
42        {
43            return false;
44        }
45
46        for (scope_name, scope_value) in &self.scopes {
47            match context.scopes.get_value(scope_name) {
48                Some(value) if value == scope_value => (),
49                _ => return false,
50            }
51        }
52
53        true
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use objectstore_types::scope::{Scope, Scopes};
60
61    use super::*;
62
63    #[test]
64    fn test_matches_empty() {
65        let switch = Killswitch {
66            usecase: None,
67            scopes: BTreeMap::new(),
68        };
69
70        let context = ObjectContext {
71            usecase: "any".to_string(),
72            scopes: Scopes::from_iter([Scope::create("any", "value").unwrap()]),
73        };
74
75        assert!(switch.matches(&context));
76    }
77
78    #[test]
79    fn test_matches_usecase() {
80        let switch = Killswitch {
81            usecase: Some("test".to_string()),
82            scopes: BTreeMap::new(),
83        };
84
85        let context = ObjectContext {
86            usecase: "test".to_string(),
87            scopes: Scopes::from_iter([Scope::create("any", "value").unwrap()]),
88        };
89        assert!(switch.matches(&context));
90
91        // usecase differs
92        let context = ObjectContext {
93            usecase: "other".to_string(),
94            scopes: Scopes::from_iter([Scope::create("any", "value").unwrap()]),
95        };
96        assert!(!switch.matches(&context));
97    }
98
99    #[test]
100    fn test_matches_scopes() {
101        let switch = Killswitch {
102            usecase: None,
103            scopes: BTreeMap::from([
104                ("org".to_string(), "123".to_string()),
105                ("project".to_string(), "456".to_string()),
106            ]),
107        };
108
109        // match, ignoring extra scope
110        let context = ObjectContext {
111            usecase: "any".to_string(),
112            scopes: Scopes::from_iter([
113                Scope::create("org", "123").unwrap(),
114                Scope::create("project", "456").unwrap(),
115                Scope::create("extra", "789").unwrap(),
116            ]),
117        };
118        assert!(switch.matches(&context));
119
120        // project differs
121        let context = ObjectContext {
122            usecase: "any".to_string(),
123            scopes: Scopes::from_iter([
124                Scope::create("org", "123").unwrap(),
125                Scope::create("project", "999").unwrap(),
126            ]),
127        };
128        assert!(!switch.matches(&context));
129
130        // missing project
131        let context = ObjectContext {
132            usecase: "any".to_string(),
133            scopes: Scopes::from_iter([Scope::create("org", "123").unwrap()]),
134        };
135        assert!(!switch.matches(&context));
136    }
137
138    #[test]
139    fn test_matches_full() {
140        let switch = Killswitch {
141            usecase: Some("test".to_string()),
142            scopes: BTreeMap::from([("org".to_string(), "123".to_string())]),
143        };
144
145        // match
146        let context = ObjectContext {
147            usecase: "test".to_string(),
148            scopes: Scopes::from_iter([Scope::create("org", "123").unwrap()]),
149        };
150        assert!(switch.matches(&context));
151
152        // usecase differs
153        let context = ObjectContext {
154            usecase: "other".to_string(),
155            scopes: Scopes::from_iter([Scope::create("org", "123").unwrap()]),
156        };
157        assert!(!switch.matches(&context));
158
159        // scope differs
160        let context = ObjectContext {
161            usecase: "test".to_string(),
162            scopes: Scopes::from_iter([Scope::create("org", "999").unwrap()]),
163        };
164        assert!(!switch.matches(&context));
165    }
166}