]> git.proxmox.com Git - proxmox.git/blame - proxmox-notify/src/api/mod.rs
router: re-export `HttpError` from `proxmox-http-error`
[proxmox.git] / proxmox-notify / src / api / mod.rs
CommitLineData
3c958429 1use std::collections::HashSet;
ad3f78a3
LW
2use std::error::Error as StdError;
3use std::fmt::Display;
4
5use crate::Config;
6use serde::Serialize;
7
714ef277 8pub mod common;
109a936b 9pub mod filter;
055db2d1
LW
10#[cfg(feature = "gotify")]
11pub mod gotify;
ee44fdca 12pub mod group;
21c5c9a0
LW
13#[cfg(feature = "sendmail")]
14pub mod sendmail;
714ef277 15
ad3f78a3
LW
16#[derive(Debug, Serialize)]
17pub 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
27impl 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
62impl 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
68impl 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
77fn 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
90fn 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
113fn 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
121fn 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
132fn 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
168fn 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
183fn 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)]
226mod 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
236mod 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}