]> git.proxmox.com Git - proxmox.git/blob - proxmox-notify/src/api/mod.rs
762d448ad90ec09bd6df153e36217b12a9764bf0
[proxmox.git] / proxmox-notify / src / api / mod.rs
1 use serde::Serialize;
2 use std::collections::HashSet;
3
4 use proxmox_http_error::HttpError;
5
6 use crate::Config;
7
8 pub mod common;
9 #[cfg(feature = "gotify")]
10 pub mod gotify;
11 pub mod matcher;
12 #[cfg(feature = "sendmail")]
13 pub mod sendmail;
14 #[cfg(feature = "smtp")]
15 pub mod smtp;
16
17 // We have our own, local versions of http_err and http_bail, because
18 // we don't want to wrap the error in anyhow::Error. If we were to do that,
19 // we would need to downcast in the perlmod bindings, since we need
20 // to return `HttpError` from there.
21 #[macro_export]
22 macro_rules! http_err {
23 ($status:ident, $($fmt:tt)+) => {{
24 proxmox_http_error::HttpError::new(
25 proxmox_http_error::StatusCode::$status,
26 format!($($fmt)+)
27 )
28 }};
29 }
30
31 #[macro_export]
32 macro_rules! http_bail {
33 ($status:ident, $($fmt:tt)+) => {{
34 return Err($crate::api::http_err!($status, $($fmt)+));
35 }};
36 }
37
38 pub use http_bail;
39 pub use http_err;
40
41 fn verify_digest(config: &Config, digest: Option<&[u8]>) -> Result<(), HttpError> {
42 if let Some(digest) = digest {
43 if config.digest != *digest {
44 http_bail!(
45 BAD_REQUEST,
46 "detected modified configuration - file changed by other user? Try again."
47 );
48 }
49 }
50
51 Ok(())
52 }
53
54 fn ensure_endpoint_exists(#[allow(unused)] config: &Config, name: &str) -> Result<(), HttpError> {
55 #[allow(unused_mut)]
56 let mut exists = false;
57
58 #[cfg(feature = "sendmail")]
59 {
60 exists = exists || sendmail::get_endpoint(config, name).is_ok();
61 }
62 #[cfg(feature = "gotify")]
63 {
64 exists = exists || gotify::get_endpoint(config, name).is_ok();
65 }
66 #[cfg(feature = "smtp")]
67 {
68 exists = exists || smtp::get_endpoint(config, name).is_ok();
69 }
70
71 if !exists {
72 http_bail!(NOT_FOUND, "endpoint '{name}' does not exist")
73 } else {
74 Ok(())
75 }
76 }
77
78 fn ensure_endpoints_exist<T: AsRef<str>>(
79 config: &Config,
80 endpoints: &[T],
81 ) -> Result<(), HttpError> {
82 for endpoint in endpoints {
83 ensure_endpoint_exists(config, endpoint.as_ref())?;
84 }
85
86 Ok(())
87 }
88
89 fn ensure_unique(config: &Config, entity: &str) -> Result<(), HttpError> {
90 if config.config.sections.contains_key(entity) {
91 http_bail!(
92 BAD_REQUEST,
93 "Cannot create '{entity}', an entity with the same name already exists"
94 );
95 }
96
97 Ok(())
98 }
99
100 fn get_referrers(config: &Config, entity: &str) -> Result<HashSet<String>, HttpError> {
101 let mut referrers = HashSet::new();
102
103 for matcher in matcher::get_matchers(config)? {
104 if let Some(targets) = matcher.target {
105 if targets.iter().any(|target| target == entity) {
106 referrers.insert(matcher.name.clone());
107 }
108 }
109 }
110
111 Ok(referrers)
112 }
113
114 fn ensure_unused(config: &Config, entity: &str) -> Result<(), HttpError> {
115 let referrers = get_referrers(config, entity)?;
116
117 if !referrers.is_empty() {
118 let used_by = referrers.into_iter().collect::<Vec<_>>().join(", ");
119
120 http_bail!(
121 BAD_REQUEST,
122 "cannot delete '{entity}', referenced by: {used_by}"
123 );
124 }
125
126 Ok(())
127 }
128
129 fn get_referenced_entities(config: &Config, entity: &str) -> HashSet<String> {
130 let mut to_expand = HashSet::new();
131 let mut expanded = HashSet::new();
132 to_expand.insert(entity.to_string());
133
134 let expand = |entities: &HashSet<String>| -> HashSet<String> {
135 let mut new = HashSet::new();
136
137 for entity in entities {
138 if let Ok(group) = matcher::get_matcher(config, entity) {
139 if let Some(targets) = group.target {
140 for target in targets {
141 new.insert(target.clone());
142 }
143 }
144 }
145 }
146
147 new
148 };
149
150 while !to_expand.is_empty() {
151 let new = expand(&to_expand);
152 expanded.extend(to_expand);
153 to_expand = new;
154 }
155
156 expanded
157 }
158
159 #[allow(unused)]
160 fn set_private_config_entry<T: Serialize>(
161 config: &mut Config,
162 private_config: &T,
163 typename: &str,
164 name: &str,
165 ) -> Result<(), HttpError> {
166 config
167 .private_config
168 .set_data(name, typename, private_config)
169 .map_err(|e| {
170 http_err!(
171 INTERNAL_SERVER_ERROR,
172 "could not save private config for endpoint '{}': {e}",
173 name
174 )
175 })
176 }
177
178 #[allow(unused)]
179 fn remove_private_config_entry(config: &mut Config, name: &str) -> Result<(), HttpError> {
180 config.private_config.sections.remove(name);
181 Ok(())
182 }
183
184 #[cfg(test)]
185 mod test_helpers {
186 use crate::Config;
187
188 #[allow(unused)]
189 pub fn empty_config() -> Config {
190 Config::new("", "").unwrap()
191 }
192 }
193
194 #[cfg(all(test, gotify, sendmail))]
195 mod tests {
196 use super::*;
197 use crate::endpoints::gotify::{GotifyConfig, GotifyPrivateConfig};
198 use crate::endpoints::sendmail::SendmailConfig;
199 use crate::filter::FilterConfig;
200 use crate::group::GroupConfig;
201
202 fn prepare_config() -> Result<Config, HttpError> {
203 let mut config = super::test_helpers::empty_config();
204
205 matcher::add_matcher(
206 &mut config,
207 &MatcherConfig {
208 name: "matcher".to_string(),
209 target: Some(vec!["sendmail".to_string(), "gotify".to_string()])
210 ..Default::default(),
211 },
212 )?;
213
214 sendmail::add_endpoint(
215 &mut config,
216 &SendmailConfig {
217 name: "sendmail".to_string(),
218 mailto: Some(vec!["foo@example.com".to_string()]),
219 ..Default::default()
220 },
221 )?;
222
223 gotify::add_endpoint(
224 &mut config,
225 &GotifyConfig {
226 name: "gotify".to_string(),
227 server: "localhost".to_string(),
228 ..Default::default()
229 },
230 &GotifyPrivateConfig {
231 name: "gotify".to_string(),
232 token: "foo".to_string(),
233 },
234 )?;
235
236 Ok(config)
237 }
238
239 #[test]
240 fn test_get_referenced_entities() {
241 let config = prepare_config().unwrap();
242
243 assert_eq!(
244 get_referenced_entities(&config, "matcher"),
245 HashSet::from([
246 "matcher".to_string(),
247 "sendmail".to_string(),
248 "gotify".to_string()
249 ])
250 );
251 }
252
253 #[test]
254 fn test_get_referrers_for_entity() -> Result<(), HttpError> {
255 let config = prepare_config().unwrap();
256
257 assert_eq!(
258 get_referrers(&config, "sendmail")?,
259 HashSet::from(["matcher".to_string()])
260 );
261
262 assert_eq!(
263 get_referrers(&config, "gotify")?,
264 HashSet::from(["matcher".to_string()])
265 );
266
267 Ok(())
268 }
269
270 #[test]
271 fn test_ensure_unused() {
272 let config = prepare_config().unwrap();
273
274 assert!(ensure_unused(&config, "gotify").is_err());
275 assert!(ensure_unused(&config, "sendmail").is_err());
276 assert!(ensure_unused(&config, "matcher").is_ok());
277 }
278
279 #[test]
280 fn test_ensure_unique() {
281 let config = prepare_config().unwrap();
282
283 assert!(ensure_unique(&config, "sendmail").is_err());
284 assert!(ensure_unique(&config, "group").is_err());
285 assert!(ensure_unique(&config, "new").is_ok());
286 }
287
288 #[test]
289 fn test_ensure_endpoints_exist() {
290 let config = prepare_config().unwrap();
291
292 assert!(ensure_endpoints_exist(&config, &vec!["sendmail", "gotify"]).is_ok());
293 }
294 }