]>
Commit | Line | Data |
---|---|---|
1a75668d | 1 | use std::collections::HashSet; |
ad3f78a3 | 2 | |
50fa98e2 LW |
3 | use serde::{Deserialize, Serialize}; |
4 | ||
7cb339df | 5 | use proxmox_http_error::HttpError; |
87f7dfa1 | 6 | use proxmox_schema::api; |
7cb339df | 7 | |
50fa98e2 | 8 | use crate::{Config, Origin}; |
7cb339df | 9 | |
714ef277 | 10 | pub mod common; |
055db2d1 LW |
11 | #[cfg(feature = "gotify")] |
12 | pub mod gotify; | |
b421a7ca | 13 | pub mod matcher; |
21c5c9a0 LW |
14 | #[cfg(feature = "sendmail")] |
15 | pub mod sendmail; | |
20b29089 LW |
16 | #[cfg(feature = "smtp")] |
17 | pub mod smtp; | |
714ef277 | 18 | |
1a75668d LW |
19 | // We have our own, local versions of http_err and http_bail, because |
20 | // we don't want to wrap the error in anyhow::Error. If we were to do that, | |
21 | // we would need to downcast in the perlmod bindings, since we need | |
22 | // to return `HttpError` from there. | |
23 | #[macro_export] | |
24 | macro_rules! http_err { | |
25 | ($status:ident, $($fmt:tt)+) => {{ | |
26 | proxmox_http_error::HttpError::new( | |
27 | proxmox_http_error::StatusCode::$status, | |
28 | format!($($fmt)+) | |
29 | ) | |
30 | }}; | |
ad3f78a3 LW |
31 | } |
32 | ||
1a75668d LW |
33 | #[macro_export] |
34 | macro_rules! http_bail { | |
35 | ($status:ident, $($fmt:tt)+) => {{ | |
36 | return Err($crate::api::http_err!($status, $($fmt)+)); | |
37 | }}; | |
ad3f78a3 LW |
38 | } |
39 | ||
1a75668d LW |
40 | pub use http_bail; |
41 | pub use http_err; | |
ad3f78a3 | 42 | |
87f7dfa1 LW |
43 | #[api] |
44 | #[derive(Clone, Debug, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd)] | |
45 | #[serde(rename_all = "kebab-case")] | |
46 | /// Type of the endpoint. | |
47 | pub enum EndpointType { | |
48 | /// Sendmail endpoint | |
49 | #[cfg(feature = "sendmail")] | |
50 | Sendmail, | |
51 | /// SMTP endpoint | |
52 | #[cfg(feature = "smtp")] | |
53 | Smtp, | |
54 | /// Gotify endpoint | |
55 | #[cfg(feature = "gotify")] | |
56 | Gotify, | |
57 | } | |
58 | ||
59 | #[api] | |
60 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd)] | |
61 | #[serde(rename_all = "kebab-case")] | |
62 | /// Target information | |
63 | pub struct Target { | |
64 | /// Name of the endpoint | |
65 | name: String, | |
66 | /// Origin of the endpoint | |
67 | origin: Origin, | |
68 | /// Type of the endpoint | |
69 | #[serde(rename = "type")] | |
70 | endpoint_type: EndpointType, | |
71 | /// Target is disabled | |
72 | #[serde(skip_serializing_if = "Option::is_none")] | |
73 | disable: Option<bool>, | |
74 | /// Comment | |
75 | #[serde(skip_serializing_if = "Option::is_none")] | |
76 | comment: Option<String>, | |
77 | } | |
78 | ||
79 | /// Get a list of all notification targets. | |
80 | pub fn get_targets(config: &Config) -> Result<Vec<Target>, HttpError> { | |
81 | let mut targets = Vec::new(); | |
82 | ||
83 | #[cfg(feature = "gotify")] | |
84 | for endpoint in gotify::get_endpoints(config)? { | |
85 | targets.push(Target { | |
86 | name: endpoint.name, | |
87 | origin: endpoint.origin.unwrap_or(Origin::UserCreated), | |
88 | endpoint_type: EndpointType::Gotify, | |
89 | disable: endpoint.disable, | |
90 | comment: endpoint.comment, | |
91 | }) | |
92 | } | |
93 | ||
94 | #[cfg(feature = "sendmail")] | |
95 | for endpoint in sendmail::get_endpoints(config)? { | |
96 | targets.push(Target { | |
97 | name: endpoint.name, | |
98 | origin: endpoint.origin.unwrap_or(Origin::UserCreated), | |
99 | endpoint_type: EndpointType::Sendmail, | |
100 | disable: endpoint.disable, | |
101 | comment: endpoint.comment, | |
102 | }) | |
103 | } | |
104 | ||
105 | #[cfg(feature = "smtp")] | |
106 | for endpoint in smtp::get_endpoints(config)? { | |
107 | targets.push(Target { | |
108 | name: endpoint.name, | |
109 | origin: endpoint.origin.unwrap_or(Origin::UserCreated), | |
110 | endpoint_type: EndpointType::Smtp, | |
111 | disable: endpoint.disable, | |
112 | comment: endpoint.comment, | |
113 | }) | |
114 | } | |
115 | ||
116 | Ok(targets) | |
117 | } | |
118 | ||
1a75668d | 119 | fn verify_digest(config: &Config, digest: Option<&[u8]>) -> Result<(), HttpError> { |
ad3f78a3 LW |
120 | if let Some(digest) = digest { |
121 | if config.digest != *digest { | |
1a75668d LW |
122 | http_bail!( |
123 | BAD_REQUEST, | |
124 | "detected modified configuration - file changed by other user? Try again." | |
125 | ); | |
ad3f78a3 LW |
126 | } |
127 | } | |
128 | ||
129 | Ok(()) | |
130 | } | |
131 | ||
1a75668d | 132 | fn ensure_endpoint_exists(#[allow(unused)] config: &Config, name: &str) -> Result<(), HttpError> { |
f6fa851d | 133 | #[allow(unused_mut)] |
ad3f78a3 LW |
134 | let mut exists = false; |
135 | ||
21c5c9a0 LW |
136 | #[cfg(feature = "sendmail")] |
137 | { | |
138 | exists = exists || sendmail::get_endpoint(config, name).is_ok(); | |
139 | } | |
055db2d1 LW |
140 | #[cfg(feature = "gotify")] |
141 | { | |
142 | exists = exists || gotify::get_endpoint(config, name).is_ok(); | |
143 | } | |
20b29089 LW |
144 | #[cfg(feature = "smtp")] |
145 | { | |
146 | exists = exists || smtp::get_endpoint(config, name).is_ok(); | |
147 | } | |
21c5c9a0 | 148 | |
2756c11c | 149 | if !exists { |
1a75668d | 150 | http_bail!(NOT_FOUND, "endpoint '{name}' does not exist") |
2756c11c LW |
151 | } else { |
152 | Ok(()) | |
153 | } | |
154 | } | |
155 | ||
1a75668d LW |
156 | fn ensure_endpoints_exist<T: AsRef<str>>( |
157 | config: &Config, | |
158 | endpoints: &[T], | |
159 | ) -> Result<(), HttpError> { | |
2756c11c LW |
160 | for endpoint in endpoints { |
161 | ensure_endpoint_exists(config, endpoint.as_ref())?; | |
162 | } | |
163 | ||
164 | Ok(()) | |
165 | } | |
166 | ||
1a75668d | 167 | fn ensure_unique(config: &Config, entity: &str) -> Result<(), HttpError> { |
2756c11c | 168 | if config.config.sections.contains_key(entity) { |
1a75668d LW |
169 | http_bail!( |
170 | BAD_REQUEST, | |
171 | "Cannot create '{entity}', an entity with the same name already exists" | |
172 | ); | |
2756c11c LW |
173 | } |
174 | ||
175 | Ok(()) | |
ad3f78a3 LW |
176 | } |
177 | ||
1a75668d | 178 | fn get_referrers(config: &Config, entity: &str) -> Result<HashSet<String>, HttpError> { |
62ae1cf9 LW |
179 | let mut referrers = HashSet::new(); |
180 | ||
b421a7ca | 181 | for matcher in matcher::get_matchers(config)? { |
d61e3fc7 LW |
182 | if matcher.target.iter().any(|target| target == entity) { |
183 | referrers.insert(matcher.name.clone()); | |
62ae1cf9 LW |
184 | } |
185 | } | |
20b29089 | 186 | |
62ae1cf9 LW |
187 | Ok(referrers) |
188 | } | |
189 | ||
50fa98e2 LW |
190 | fn ensure_safe_to_delete(config: &Config, entity: &str) -> Result<(), HttpError> { |
191 | if let Some(entity_config) = config.config.sections.get(entity) { | |
192 | if let Ok(origin) = Origin::deserialize(&entity_config.1["origin"]) { | |
193 | // Built-ins are never actually removed, only reset to their default | |
194 | // It is thus safe to do the reset if another entity depends | |
195 | // on it | |
196 | if origin == Origin::Builtin || origin == Origin::ModifiedBuiltin { | |
197 | return Ok(()); | |
198 | } | |
199 | } | |
200 | } else { | |
201 | http_bail!(NOT_FOUND, "entity '{entity}' does not exist"); | |
202 | } | |
203 | ||
62ae1cf9 LW |
204 | let referrers = get_referrers(config, entity)?; |
205 | ||
206 | if !referrers.is_empty() { | |
207 | let used_by = referrers.into_iter().collect::<Vec<_>>().join(", "); | |
208 | ||
1a75668d LW |
209 | http_bail!( |
210 | BAD_REQUEST, | |
211 | "cannot delete '{entity}', referenced by: {used_by}" | |
212 | ); | |
62ae1cf9 LW |
213 | } |
214 | ||
215 | Ok(()) | |
216 | } | |
217 | ||
3c958429 LW |
218 | fn get_referenced_entities(config: &Config, entity: &str) -> HashSet<String> { |
219 | let mut to_expand = HashSet::new(); | |
220 | let mut expanded = HashSet::new(); | |
221 | to_expand.insert(entity.to_string()); | |
222 | ||
223 | let expand = |entities: &HashSet<String>| -> HashSet<String> { | |
224 | let mut new = HashSet::new(); | |
225 | ||
226 | for entity in entities { | |
d61e3fc7 LW |
227 | if let Ok(matcher) = matcher::get_matcher(config, entity) { |
228 | for target in matcher.target { | |
229 | new.insert(target.clone()); | |
3c958429 LW |
230 | } |
231 | } | |
232 | } | |
233 | ||
234 | new | |
235 | }; | |
236 | ||
237 | while !to_expand.is_empty() { | |
238 | let new = expand(&to_expand); | |
239 | expanded.extend(to_expand); | |
240 | to_expand = new; | |
241 | } | |
242 | ||
243 | expanded | |
244 | } | |
245 | ||
20b29089 LW |
246 | #[allow(unused)] |
247 | fn set_private_config_entry<T: Serialize>( | |
248 | config: &mut Config, | |
a4d55947 | 249 | private_config: T, |
20b29089 LW |
250 | typename: &str, |
251 | name: &str, | |
252 | ) -> Result<(), HttpError> { | |
253 | config | |
254 | .private_config | |
a4d55947 | 255 | .set_data(name, typename, &private_config) |
20b29089 LW |
256 | .map_err(|e| { |
257 | http_err!( | |
258 | INTERNAL_SERVER_ERROR, | |
259 | "could not save private config for endpoint '{}': {e}", | |
260 | name | |
261 | ) | |
262 | }) | |
263 | } | |
264 | ||
265 | #[allow(unused)] | |
266 | fn remove_private_config_entry(config: &mut Config, name: &str) -> Result<(), HttpError> { | |
267 | config.private_config.sections.remove(name); | |
268 | Ok(()) | |
269 | } | |
270 | ||
ad3f78a3 LW |
271 | #[cfg(test)] |
272 | mod test_helpers { | |
273 | use crate::Config; | |
274 | ||
a1cbaea7 | 275 | #[allow(unused)] |
ad3f78a3 LW |
276 | pub fn empty_config() -> Config { |
277 | Config::new("", "").unwrap() | |
278 | } | |
279 | } | |
3c958429 | 280 | |
50fa98e2 | 281 | #[cfg(all(test, feature = "gotify", feature = "sendmail"))] |
3c958429 LW |
282 | mod tests { |
283 | use super::*; | |
284 | use crate::endpoints::gotify::{GotifyConfig, GotifyPrivateConfig}; | |
285 | use crate::endpoints::sendmail::SendmailConfig; | |
50fa98e2 | 286 | use crate::matcher::MatcherConfig; |
3c958429 | 287 | |
1a75668d | 288 | fn prepare_config() -> Result<Config, HttpError> { |
50fa98e2 | 289 | let mut config = test_helpers::empty_config(); |
3c958429 | 290 | |
50fa98e2 | 291 | sendmail::add_endpoint( |
3c958429 | 292 | &mut config, |
a4d55947 | 293 | SendmailConfig { |
50fa98e2 | 294 | name: "sendmail".to_string(), |
d61e3fc7 | 295 | mailto: vec!["foo@example.com".to_string()], |
50fa98e2 | 296 | ..Default::default() |
3c958429 | 297 | }, |
62ae1cf9 | 298 | )?; |
3c958429 LW |
299 | |
300 | sendmail::add_endpoint( | |
301 | &mut config, | |
a4d55947 | 302 | SendmailConfig { |
50fa98e2 | 303 | name: "builtin".to_string(), |
d61e3fc7 | 304 | mailto: vec!["foo@example.com".to_string()], |
50fa98e2 | 305 | origin: Some(Origin::Builtin), |
3c958429 LW |
306 | ..Default::default() |
307 | }, | |
62ae1cf9 | 308 | )?; |
3c958429 LW |
309 | |
310 | gotify::add_endpoint( | |
311 | &mut config, | |
a4d55947 | 312 | GotifyConfig { |
3c958429 LW |
313 | name: "gotify".to_string(), |
314 | server: "localhost".to_string(), | |
3c958429 LW |
315 | ..Default::default() |
316 | }, | |
a4d55947 | 317 | GotifyPrivateConfig { |
3c958429 LW |
318 | name: "gotify".to_string(), |
319 | token: "foo".to_string(), | |
320 | }, | |
62ae1cf9 | 321 | )?; |
3c958429 | 322 | |
50fa98e2 LW |
323 | matcher::add_matcher( |
324 | &mut config, | |
a4d55947 | 325 | MatcherConfig { |
50fa98e2 | 326 | name: "matcher".to_string(), |
d61e3fc7 | 327 | target: vec![ |
50fa98e2 LW |
328 | "sendmail".to_string(), |
329 | "gotify".to_string(), | |
330 | "builtin".to_string(), | |
d61e3fc7 | 331 | ], |
50fa98e2 LW |
332 | ..Default::default() |
333 | }, | |
334 | )?; | |
335 | ||
62ae1cf9 LW |
336 | Ok(config) |
337 | } | |
338 | ||
339 | #[test] | |
340 | fn test_get_referenced_entities() { | |
341 | let config = prepare_config().unwrap(); | |
3c958429 LW |
342 | |
343 | assert_eq!( | |
b421a7ca | 344 | get_referenced_entities(&config, "matcher"), |
3c958429 | 345 | HashSet::from([ |
b421a7ca | 346 | "matcher".to_string(), |
3c958429 | 347 | "sendmail".to_string(), |
50fa98e2 | 348 | "builtin".to_string(), |
b421a7ca | 349 | "gotify".to_string() |
3c958429 LW |
350 | ]) |
351 | ); | |
352 | } | |
62ae1cf9 LW |
353 | |
354 | #[test] | |
1a75668d | 355 | fn test_get_referrers_for_entity() -> Result<(), HttpError> { |
62ae1cf9 LW |
356 | let config = prepare_config().unwrap(); |
357 | ||
62ae1cf9 LW |
358 | assert_eq!( |
359 | get_referrers(&config, "sendmail")?, | |
b421a7ca | 360 | HashSet::from(["matcher".to_string()]) |
62ae1cf9 LW |
361 | ); |
362 | ||
363 | assert_eq!( | |
364 | get_referrers(&config, "gotify")?, | |
b421a7ca | 365 | HashSet::from(["matcher".to_string()]) |
62ae1cf9 LW |
366 | ); |
367 | ||
62ae1cf9 LW |
368 | Ok(()) |
369 | } | |
370 | ||
371 | #[test] | |
50fa98e2 | 372 | fn test_ensure_safe_to_delete() { |
62ae1cf9 LW |
373 | let config = prepare_config().unwrap(); |
374 | ||
50fa98e2 LW |
375 | assert!(ensure_safe_to_delete(&config, "gotify").is_err()); |
376 | assert!(ensure_safe_to_delete(&config, "sendmail").is_err()); | |
377 | assert!(ensure_safe_to_delete(&config, "matcher").is_ok()); | |
378 | ||
379 | // built-ins are always safe to delete, since there is no way to actually | |
380 | // delete them... they will only be reset to their default settings and | |
381 | // will thus continue to exist | |
382 | assert!(ensure_safe_to_delete(&config, "builtin").is_ok()); | |
62ae1cf9 | 383 | } |
2756c11c LW |
384 | |
385 | #[test] | |
386 | fn test_ensure_unique() { | |
387 | let config = prepare_config().unwrap(); | |
388 | ||
389 | assert!(ensure_unique(&config, "sendmail").is_err()); | |
50fa98e2 | 390 | assert!(ensure_unique(&config, "matcher").is_err()); |
2756c11c LW |
391 | assert!(ensure_unique(&config, "new").is_ok()); |
392 | } | |
393 | ||
394 | #[test] | |
395 | fn test_ensure_endpoints_exist() { | |
396 | let config = prepare_config().unwrap(); | |
397 | ||
50fa98e2 | 398 | assert!(ensure_endpoints_exist(&config, &["sendmail", "gotify", "builtin"]).is_ok()); |
2756c11c | 399 | } |
3c958429 | 400 | } |