]> git.proxmox.com Git - proxmox.git/blame - proxmox-notify/src/api/mod.rs
notify: api: add get_targets
[proxmox.git] / proxmox-notify / src / api / mod.rs
CommitLineData
1a75668d 1use std::collections::HashSet;
ad3f78a3 2
50fa98e2
LW
3use serde::{Deserialize, Serialize};
4
7cb339df 5use proxmox_http_error::HttpError;
87f7dfa1 6use proxmox_schema::api;
7cb339df 7
50fa98e2 8use crate::{Config, Origin};
7cb339df 9
714ef277 10pub mod common;
055db2d1
LW
11#[cfg(feature = "gotify")]
12pub mod gotify;
b421a7ca 13pub mod matcher;
21c5c9a0
LW
14#[cfg(feature = "sendmail")]
15pub mod sendmail;
20b29089
LW
16#[cfg(feature = "smtp")]
17pub 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]
24macro_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]
34macro_rules! http_bail {
35 ($status:ident, $($fmt:tt)+) => {{
36 return Err($crate::api::http_err!($status, $($fmt)+));
37 }};
ad3f78a3
LW
38}
39
1a75668d
LW
40pub use http_bail;
41pub 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.
47pub 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
63pub 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.
80pub 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 119fn 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 132fn 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
156fn 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 167fn 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 178fn 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
190fn 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
218fn 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)]
247fn 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)]
266fn 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)]
272mod 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
282mod 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}