]>
Commit | Line | Data |
---|---|---|
3c958429 | 1 | use std::collections::HashSet; |
ad3f78a3 LW |
2 | use std::error::Error as StdError; |
3 | use std::fmt::Display; | |
4 | ||
5 | use crate::Config; | |
6 | use serde::Serialize; | |
7 | ||
714ef277 | 8 | pub mod common; |
109a936b | 9 | pub mod filter; |
055db2d1 LW |
10 | #[cfg(feature = "gotify")] |
11 | pub mod gotify; | |
ee44fdca | 12 | pub mod group; |
21c5c9a0 LW |
13 | #[cfg(feature = "sendmail")] |
14 | pub mod sendmail; | |
714ef277 | 15 | |
ad3f78a3 LW |
16 | #[derive(Debug, Serialize)] |
17 | pub struct ApiError { | |
18 | /// HTTP Error code | |
19 | code: u16, | |
20 | /// Error message | |
21 | message: String, | |
22 | #[serde(skip_serializing)] | |
23 | /// The underlying cause of the error | |
24 | source: Option<Box<dyn StdError + Send + Sync + 'static>>, | |
25 | } | |
26 | ||
27 | impl ApiError { | |
28 | fn new<S: AsRef<str>>( | |
29 | message: S, | |
30 | code: u16, | |
31 | source: Option<Box<dyn StdError + Send + Sync + 'static>>, | |
32 | ) -> Self { | |
33 | Self { | |
34 | message: message.as_ref().into(), | |
35 | code, | |
36 | source, | |
37 | } | |
38 | } | |
39 | ||
40 | pub fn bad_request<S: AsRef<str>>( | |
41 | message: S, | |
42 | source: Option<Box<dyn StdError + Send + Sync + 'static>>, | |
43 | ) -> Self { | |
44 | Self::new(message, 400, source) | |
45 | } | |
46 | ||
47 | pub fn not_found<S: AsRef<str>>( | |
48 | message: S, | |
49 | source: Option<Box<dyn StdError + Send + Sync + 'static>>, | |
50 | ) -> Self { | |
51 | Self::new(message, 404, source) | |
52 | } | |
53 | ||
54 | pub fn internal_server_error<S: AsRef<str>>( | |
55 | message: S, | |
56 | source: Option<Box<dyn StdError + Send + Sync + 'static>>, | |
57 | ) -> Self { | |
58 | Self::new(message, 500, source) | |
59 | } | |
60 | } | |
61 | ||
62 | impl Display for ApiError { | |
63 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
64 | f.write_str(&format!("{} {}", self.code, self.message)) | |
65 | } | |
66 | } | |
67 | ||
68 | impl StdError for ApiError { | |
69 | fn source(&self) -> Option<&(dyn StdError + 'static)> { | |
70 | match &self.source { | |
71 | None => None, | |
72 | Some(source) => Some(&**source), | |
73 | } | |
74 | } | |
75 | } | |
76 | ||
77 | fn verify_digest(config: &Config, digest: Option<&[u8]>) -> Result<(), ApiError> { | |
78 | if let Some(digest) = digest { | |
79 | if config.digest != *digest { | |
80 | return Err(ApiError::bad_request( | |
81 | "detected modified configuration - file changed by other user? Try again.", | |
82 | None, | |
83 | )); | |
84 | } | |
85 | } | |
86 | ||
87 | Ok(()) | |
88 | } | |
89 | ||
f6fa851d LW |
90 | fn ensure_endpoint_exists(#[allow(unused)] config: &Config, name: &str) -> Result<(), ApiError> { |
91 | #[allow(unused_mut)] | |
ad3f78a3 LW |
92 | let mut exists = false; |
93 | ||
21c5c9a0 LW |
94 | #[cfg(feature = "sendmail")] |
95 | { | |
96 | exists = exists || sendmail::get_endpoint(config, name).is_ok(); | |
97 | } | |
055db2d1 LW |
98 | #[cfg(feature = "gotify")] |
99 | { | |
100 | exists = exists || gotify::get_endpoint(config, name).is_ok(); | |
101 | } | |
21c5c9a0 | 102 | |
2756c11c LW |
103 | if !exists { |
104 | Err(ApiError::not_found( | |
105 | format!("endpoint '{name}' does not exist"), | |
106 | None, | |
107 | )) | |
108 | } else { | |
109 | Ok(()) | |
110 | } | |
111 | } | |
112 | ||
113 | fn ensure_endpoints_exist<T: AsRef<str>>(config: &Config, endpoints: &[T]) -> Result<(), ApiError> { | |
114 | for endpoint in endpoints { | |
115 | ensure_endpoint_exists(config, endpoint.as_ref())?; | |
116 | } | |
117 | ||
118 | Ok(()) | |
119 | } | |
120 | ||
121 | fn ensure_unique(config: &Config, entity: &str) -> Result<(), ApiError> { | |
122 | if config.config.sections.contains_key(entity) { | |
123 | return Err(ApiError::bad_request( | |
124 | format!("Cannot create '{entity}', an entity with the same name already exists"), | |
125 | None, | |
126 | )); | |
127 | } | |
128 | ||
129 | Ok(()) | |
ad3f78a3 LW |
130 | } |
131 | ||
62ae1cf9 LW |
132 | fn get_referrers(config: &Config, entity: &str) -> Result<HashSet<String>, ApiError> { |
133 | let mut referrers = HashSet::new(); | |
134 | ||
135 | for group in group::get_groups(config)? { | |
136 | if group.endpoint.iter().any(|endpoint| endpoint == entity) { | |
137 | referrers.insert(group.name.clone()); | |
138 | } | |
139 | ||
140 | if let Some(filter) = group.filter { | |
141 | if filter == entity { | |
142 | referrers.insert(group.name); | |
143 | } | |
144 | } | |
145 | } | |
146 | ||
147 | #[cfg(feature = "sendmail")] | |
148 | for endpoint in sendmail::get_endpoints(config)? { | |
149 | if let Some(filter) = endpoint.filter { | |
150 | if filter == entity { | |
151 | referrers.insert(endpoint.name); | |
152 | } | |
153 | } | |
154 | } | |
155 | ||
156 | #[cfg(feature = "gotify")] | |
157 | for endpoint in gotify::get_endpoints(config)? { | |
158 | if let Some(filter) = endpoint.filter { | |
159 | if filter == entity { | |
160 | referrers.insert(endpoint.name); | |
161 | } | |
162 | } | |
163 | } | |
164 | ||
165 | Ok(referrers) | |
166 | } | |
167 | ||
168 | fn ensure_unused(config: &Config, entity: &str) -> Result<(), ApiError> { | |
169 | let referrers = get_referrers(config, entity)?; | |
170 | ||
171 | if !referrers.is_empty() { | |
172 | let used_by = referrers.into_iter().collect::<Vec<_>>().join(", "); | |
173 | ||
174 | return Err(ApiError::bad_request( | |
175 | format!("cannot delete '{entity}', referenced by: {used_by}"), | |
176 | None, | |
177 | )); | |
178 | } | |
179 | ||
180 | Ok(()) | |
181 | } | |
182 | ||
3c958429 LW |
183 | fn get_referenced_entities(config: &Config, entity: &str) -> HashSet<String> { |
184 | let mut to_expand = HashSet::new(); | |
185 | let mut expanded = HashSet::new(); | |
186 | to_expand.insert(entity.to_string()); | |
187 | ||
188 | let expand = |entities: &HashSet<String>| -> HashSet<String> { | |
189 | let mut new = HashSet::new(); | |
190 | ||
191 | for entity in entities { | |
192 | if let Ok(group) = group::get_group(config, entity) { | |
193 | for target in group.endpoint { | |
194 | new.insert(target.clone()); | |
195 | } | |
196 | } | |
197 | ||
198 | #[cfg(feature = "sendmail")] | |
199 | if let Ok(target) = sendmail::get_endpoint(config, entity) { | |
200 | if let Some(filter) = target.filter { | |
201 | new.insert(filter.clone()); | |
202 | } | |
203 | } | |
204 | ||
205 | #[cfg(feature = "gotify")] | |
206 | if let Ok(target) = gotify::get_endpoint(config, entity) { | |
207 | if let Some(filter) = target.filter { | |
208 | new.insert(filter.clone()); | |
209 | } | |
210 | } | |
211 | } | |
212 | ||
213 | new | |
214 | }; | |
215 | ||
216 | while !to_expand.is_empty() { | |
217 | let new = expand(&to_expand); | |
218 | expanded.extend(to_expand); | |
219 | to_expand = new; | |
220 | } | |
221 | ||
222 | expanded | |
223 | } | |
224 | ||
ad3f78a3 LW |
225 | #[cfg(test)] |
226 | mod test_helpers { | |
227 | use crate::Config; | |
228 | ||
a1cbaea7 | 229 | #[allow(unused)] |
ad3f78a3 LW |
230 | pub fn empty_config() -> Config { |
231 | Config::new("", "").unwrap() | |
232 | } | |
233 | } | |
3c958429 | 234 | |
a1cbaea7 | 235 | #[cfg(all(test, gotify, sendmail))] |
3c958429 LW |
236 | mod tests { |
237 | use super::*; | |
238 | use crate::endpoints::gotify::{GotifyConfig, GotifyPrivateConfig}; | |
239 | use crate::endpoints::sendmail::SendmailConfig; | |
240 | use crate::filter::FilterConfig; | |
241 | use crate::group::GroupConfig; | |
242 | ||
62ae1cf9 | 243 | fn prepare_config() -> Result<Config, ApiError> { |
3c958429 LW |
244 | let mut config = super::test_helpers::empty_config(); |
245 | ||
246 | filter::add_filter( | |
247 | &mut config, | |
248 | &FilterConfig { | |
249 | name: "filter".to_string(), | |
250 | ..Default::default() | |
251 | }, | |
62ae1cf9 | 252 | )?; |
3c958429 LW |
253 | |
254 | sendmail::add_endpoint( | |
255 | &mut config, | |
256 | &SendmailConfig { | |
257 | name: "sendmail".to_string(), | |
258 | mailto: Some(vec!["foo@example.com".to_string()]), | |
259 | filter: Some("filter".to_string()), | |
260 | ..Default::default() | |
261 | }, | |
62ae1cf9 | 262 | )?; |
3c958429 LW |
263 | |
264 | gotify::add_endpoint( | |
265 | &mut config, | |
266 | &GotifyConfig { | |
267 | name: "gotify".to_string(), | |
268 | server: "localhost".to_string(), | |
269 | filter: Some("filter".to_string()), | |
270 | ..Default::default() | |
271 | }, | |
272 | &GotifyPrivateConfig { | |
273 | name: "gotify".to_string(), | |
274 | token: "foo".to_string(), | |
275 | }, | |
62ae1cf9 | 276 | )?; |
3c958429 LW |
277 | |
278 | group::add_group( | |
279 | &mut config, | |
280 | &GroupConfig { | |
281 | name: "group".to_string(), | |
282 | endpoint: vec!["gotify".to_string(), "sendmail".to_string()], | |
283 | filter: Some("filter".to_string()), | |
284 | ..Default::default() | |
285 | }, | |
62ae1cf9 LW |
286 | )?; |
287 | ||
288 | Ok(config) | |
289 | } | |
290 | ||
291 | #[test] | |
292 | fn test_get_referenced_entities() { | |
293 | let config = prepare_config().unwrap(); | |
3c958429 LW |
294 | |
295 | assert_eq!( | |
296 | get_referenced_entities(&config, "filter"), | |
297 | HashSet::from(["filter".to_string()]) | |
298 | ); | |
299 | assert_eq!( | |
300 | get_referenced_entities(&config, "sendmail"), | |
301 | HashSet::from(["filter".to_string(), "sendmail".to_string()]) | |
302 | ); | |
303 | assert_eq!( | |
304 | get_referenced_entities(&config, "gotify"), | |
305 | HashSet::from(["filter".to_string(), "gotify".to_string()]) | |
306 | ); | |
307 | assert_eq!( | |
308 | get_referenced_entities(&config, "group"), | |
309 | HashSet::from([ | |
310 | "filter".to_string(), | |
311 | "gotify".to_string(), | |
312 | "sendmail".to_string(), | |
313 | "group".to_string() | |
314 | ]) | |
315 | ); | |
316 | } | |
62ae1cf9 LW |
317 | |
318 | #[test] | |
319 | fn test_get_referrers_for_entity() -> Result<(), ApiError> { | |
320 | let config = prepare_config().unwrap(); | |
321 | ||
322 | assert_eq!( | |
323 | get_referrers(&config, "filter")?, | |
324 | HashSet::from([ | |
325 | "gotify".to_string(), | |
326 | "sendmail".to_string(), | |
327 | "group".to_string() | |
328 | ]) | |
329 | ); | |
330 | ||
331 | assert_eq!( | |
332 | get_referrers(&config, "sendmail")?, | |
333 | HashSet::from(["group".to_string()]) | |
334 | ); | |
335 | ||
336 | assert_eq!( | |
337 | get_referrers(&config, "gotify")?, | |
338 | HashSet::from(["group".to_string()]) | |
339 | ); | |
340 | ||
341 | assert!(get_referrers(&config, "group")?.is_empty(),); | |
342 | ||
343 | Ok(()) | |
344 | } | |
345 | ||
346 | #[test] | |
347 | fn test_ensure_unused() { | |
348 | let config = prepare_config().unwrap(); | |
349 | ||
350 | assert!(ensure_unused(&config, "filter").is_err()); | |
351 | assert!(ensure_unused(&config, "gotify").is_err()); | |
352 | assert!(ensure_unused(&config, "sendmail").is_err()); | |
353 | assert!(ensure_unused(&config, "group").is_ok()); | |
354 | } | |
2756c11c LW |
355 | |
356 | #[test] | |
357 | fn test_ensure_unique() { | |
358 | let config = prepare_config().unwrap(); | |
359 | ||
360 | assert!(ensure_unique(&config, "sendmail").is_err()); | |
361 | assert!(ensure_unique(&config, "group").is_err()); | |
362 | assert!(ensure_unique(&config, "new").is_ok()); | |
363 | } | |
364 | ||
365 | #[test] | |
366 | fn test_ensure_endpoints_exist() { | |
367 | let config = prepare_config().unwrap(); | |
368 | ||
369 | assert!(ensure_endpoints_exist(&config, &vec!["sendmail", "gotify"]).is_ok()); | |
370 | assert!(ensure_endpoints_exist(&config, &vec!["group", "filter"]).is_err()); | |
371 | } | |
3c958429 | 372 | } |