Skip to main content

unifly_api/controller/
session_queries.rs

1use crate::core_error::CoreError;
2use crate::model::{
3    Admin, Alarm, EntityId, HealthSummary, IpsecSa, MagicSiteToSiteVpnConfig,
4    RemoteAccessVpnServer, SiteToSiteVpn, SysInfo, SystemInfo, VpnClientConnection,
5    VpnClientProfile, VpnSetting, WireGuardPeer,
6};
7use crate::session::models::{ChannelAvailability, RogueAp};
8
9use super::Controller;
10use super::support::{convert_health_summaries, require_session};
11
12const VPN_SETTING_KEYS: &[&str] = &[
13    "teleport",
14    "magic_site_to_site_vpn",
15    "openvpn",
16    "peer_to_peer",
17];
18
19impl Controller {
20    pub async fn list_site_to_site_vpns(&self) -> Result<Vec<SiteToSiteVpn>, CoreError> {
21        let guard = self.inner.session_client.lock().await;
22        let session = require_session(guard.as_ref())?;
23        let raw = session.list_network_conf().await?;
24        Ok(raw
25            .iter()
26            .filter_map(site_to_site_vpn_from_raw)
27            .collect::<Vec<_>>())
28    }
29
30    pub async fn get_site_to_site_vpn(&self, id: &str) -> Result<SiteToSiteVpn, CoreError> {
31        self.list_site_to_site_vpns()
32            .await?
33            .into_iter()
34            .find(|vpn| matches!(&vpn.id, EntityId::Legacy(value) if value == id))
35            .ok_or_else(|| CoreError::NotFound {
36                entity_type: "site-to-site VPN".into(),
37                identifier: id.into(),
38            })
39    }
40
41    pub async fn list_remote_access_vpn_servers(
42        &self,
43    ) -> Result<Vec<RemoteAccessVpnServer>, CoreError> {
44        let guard = self.inner.session_client.lock().await;
45        let session = require_session(guard.as_ref())?;
46        let raw = session.list_network_conf().await?;
47        Ok(raw
48            .iter()
49            .filter_map(remote_access_vpn_server_from_raw)
50            .collect::<Vec<_>>())
51    }
52
53    pub async fn get_remote_access_vpn_server(
54        &self,
55        id: &str,
56    ) -> Result<RemoteAccessVpnServer, CoreError> {
57        self.list_remote_access_vpn_servers()
58            .await?
59            .into_iter()
60            .find(|server| matches!(&server.id, EntityId::Legacy(value) if value == id))
61            .ok_or_else(|| CoreError::NotFound {
62                entity_type: "remote-access VPN server".into(),
63                identifier: id.into(),
64            })
65    }
66
67    pub async fn list_wireguard_peers(
68        &self,
69        server_id: Option<&str>,
70    ) -> Result<Vec<WireGuardPeer>, CoreError> {
71        let guard = self.inner.session_client.lock().await;
72        let session = require_session(guard.as_ref())?;
73        let raw = match server_id {
74            Some(server_id) => session.list_wireguard_peers(server_id).await?,
75            None => session.list_all_wireguard_peers().await?,
76        };
77        let mut peers = raw
78            .iter()
79            .filter_map(|value| wireguard_peer_from_raw(value, server_id))
80            .collect::<Vec<_>>();
81        peers.sort_by(|left, right| {
82            left.name
83                .cmp(&right.name)
84                .then_with(|| left.id.to_string().cmp(&right.id.to_string()))
85        });
86        Ok(peers)
87    }
88
89    pub async fn get_wireguard_peer(
90        &self,
91        server_id: &str,
92        id: &str,
93    ) -> Result<WireGuardPeer, CoreError> {
94        self.list_wireguard_peers(Some(server_id))
95            .await?
96            .into_iter()
97            .find(|peer| matches!(&peer.id, EntityId::Legacy(value) if value == id))
98            .ok_or_else(|| CoreError::NotFound {
99                entity_type: "WireGuard peer".into(),
100                identifier: id.into(),
101            })
102    }
103
104    pub async fn list_wireguard_peer_existing_subnets(&self) -> Result<Vec<String>, CoreError> {
105        let guard = self.inner.session_client.lock().await;
106        let session = require_session(guard.as_ref())?;
107        let raw = session.get_wireguard_peer_existing_subnets().await?;
108        Ok(raw
109            .get("subnets")
110            .and_then(serde_json::Value::as_array)
111            .map(|values| {
112                values
113                    .iter()
114                    .filter_map(serde_json::Value::as_str)
115                    .map(str::to_owned)
116                    .collect::<Vec<_>>()
117            })
118            .unwrap_or_default())
119    }
120
121    pub async fn list_openvpn_port_suggestions(&self) -> Result<Vec<u16>, CoreError> {
122        let guard = self.inner.session_client.lock().await;
123        let session = require_session(guard.as_ref())?;
124        let raw = session.get_openvpn_port_suggestions().await?;
125        Ok(raw
126            .get("available_ports")
127            .and_then(serde_json::Value::as_array)
128            .map(|values| {
129                values
130                    .iter()
131                    .filter_map(serde_json::Value::as_u64)
132                    .filter_map(|value| u16::try_from(value).ok())
133                    .collect::<Vec<_>>()
134            })
135            .unwrap_or_default())
136    }
137
138    pub async fn download_openvpn_configuration(&self, id: &str) -> Result<Vec<u8>, CoreError> {
139        let guard = self.inner.session_client.lock().await;
140        let session = require_session(guard.as_ref())?;
141        Ok(session.download_openvpn_configuration(id).await?)
142    }
143
144    pub async fn list_vpn_client_profiles(&self) -> Result<Vec<VpnClientProfile>, CoreError> {
145        let guard = self.inner.session_client.lock().await;
146        let session = require_session(guard.as_ref())?;
147        let mut clients = session
148            .list_network_conf()
149            .await?
150            .iter()
151            .filter_map(vpn_client_profile_from_raw)
152            .collect::<Vec<_>>();
153        clients.sort_by(|left, right| {
154            left.name
155                .cmp(&right.name)
156                .then_with(|| left.id.to_string().cmp(&right.id.to_string()))
157        });
158        Ok(clients)
159    }
160
161    pub async fn get_vpn_client_profile(&self, id: &str) -> Result<VpnClientProfile, CoreError> {
162        self.list_vpn_client_profiles()
163            .await?
164            .into_iter()
165            .find(|client| matches!(&client.id, EntityId::Legacy(value) if value == id))
166            .ok_or_else(|| CoreError::NotFound {
167                entity_type: "VPN client profile".into(),
168                identifier: id.into(),
169            })
170    }
171
172    pub async fn list_vpn_client_connections(&self) -> Result<Vec<VpnClientConnection>, CoreError> {
173        let guard = self.inner.session_client.lock().await;
174        let session = require_session(guard.as_ref())?;
175        let mut connections = session
176            .list_vpn_client_connections()
177            .await?
178            .iter()
179            .filter_map(vpn_client_connection_from_raw)
180            .collect::<Vec<_>>();
181        connections.sort_by(|left, right| {
182            left.name
183                .cmp(&right.name)
184                .then_with(|| left.id.to_string().cmp(&right.id.to_string()))
185        });
186        Ok(connections)
187    }
188
189    pub async fn get_vpn_client_connection(
190        &self,
191        id: &str,
192    ) -> Result<VpnClientConnection, CoreError> {
193        self.list_vpn_client_connections()
194            .await?
195            .into_iter()
196            .find(|connection| matches!(&connection.id, EntityId::Legacy(value) if value == id))
197            .ok_or_else(|| CoreError::NotFound {
198                entity_type: "VPN client connection".into(),
199                identifier: id.into(),
200            })
201    }
202
203    pub async fn list_magic_site_to_site_vpn_configs(
204        &self,
205    ) -> Result<Vec<MagicSiteToSiteVpnConfig>, CoreError> {
206        let guard = self.inner.session_client.lock().await;
207        let session = require_session(guard.as_ref())?;
208        let mut configs = session
209            .list_magic_site_to_site_vpn_configs()
210            .await?
211            .iter()
212            .filter_map(magic_site_to_site_vpn_config_from_raw)
213            .collect::<Vec<_>>();
214        configs.sort_by(|left, right| {
215            left.name
216                .cmp(&right.name)
217                .then_with(|| left.id.to_string().cmp(&right.id.to_string()))
218        });
219        Ok(configs)
220    }
221
222    pub async fn get_magic_site_to_site_vpn_config(
223        &self,
224        id: &str,
225    ) -> Result<MagicSiteToSiteVpnConfig, CoreError> {
226        self.list_magic_site_to_site_vpn_configs()
227            .await?
228            .into_iter()
229            .find(|config| matches!(&config.id, EntityId::Legacy(value) if value == id))
230            .ok_or_else(|| CoreError::NotFound {
231                entity_type: "magic site-to-site VPN config".into(),
232                identifier: id.into(),
233            })
234    }
235
236    pub async fn list_backups(&self) -> Result<Vec<serde_json::Value>, CoreError> {
237        let guard = self.inner.session_client.lock().await;
238        let session = require_session(guard.as_ref())?;
239        Ok(session.list_backups().await?)
240    }
241
242    pub async fn download_backup(&self, filename: &str) -> Result<Vec<u8>, CoreError> {
243        let guard = self.inner.session_client.lock().await;
244        let session = require_session(guard.as_ref())?;
245        Ok(session.download_backup(filename).await?)
246    }
247
248    pub async fn get_site_stats(
249        &self,
250        interval: &str,
251        start: Option<i64>,
252        end: Option<i64>,
253        attrs: Option<&[String]>,
254    ) -> Result<Vec<serde_json::Value>, CoreError> {
255        let guard = self.inner.session_client.lock().await;
256        let session = require_session(guard.as_ref())?;
257        Ok(session.get_site_stats(interval, start, end, attrs).await?)
258    }
259
260    pub async fn get_device_stats(
261        &self,
262        interval: &str,
263        macs: Option<&[String]>,
264        attrs: Option<&[String]>,
265    ) -> Result<Vec<serde_json::Value>, CoreError> {
266        let guard = self.inner.session_client.lock().await;
267        let session = require_session(guard.as_ref())?;
268        Ok(session.get_device_stats(interval, macs, attrs).await?)
269    }
270
271    pub async fn get_client_stats(
272        &self,
273        interval: &str,
274        macs: Option<&[String]>,
275        attrs: Option<&[String]>,
276    ) -> Result<Vec<serde_json::Value>, CoreError> {
277        let guard = self.inner.session_client.lock().await;
278        let session = require_session(guard.as_ref())?;
279        Ok(session.get_client_stats(interval, macs, attrs).await?)
280    }
281
282    pub async fn get_gateway_stats(
283        &self,
284        interval: &str,
285        start: Option<i64>,
286        end: Option<i64>,
287        attrs: Option<&[String]>,
288    ) -> Result<Vec<serde_json::Value>, CoreError> {
289        let guard = self.inner.session_client.lock().await;
290        let session = require_session(guard.as_ref())?;
291        Ok(session
292            .get_gateway_stats(interval, start, end, attrs)
293            .await?)
294    }
295
296    pub async fn get_dpi_stats(
297        &self,
298        group_by: &str,
299        macs: Option<&[String]>,
300    ) -> Result<Vec<serde_json::Value>, CoreError> {
301        let guard = self.inner.session_client.lock().await;
302        let session = require_session(guard.as_ref())?;
303        Ok(session.get_dpi_stats(group_by, macs).await?)
304    }
305
306    pub async fn list_admins(&self) -> Result<Vec<Admin>, CoreError> {
307        let guard = self.inner.session_client.lock().await;
308        let session = require_session(guard.as_ref())?;
309        let raw = session.list_admins().await?;
310        Ok(raw
311            .into_iter()
312            .map(|value| Admin {
313                id: value
314                    .get("_id")
315                    .and_then(|value| value.as_str())
316                    .map_or_else(
317                        || EntityId::Legacy("unknown".into()),
318                        |value| EntityId::Legacy(value.into()),
319                    ),
320                name: value
321                    .get("name")
322                    .and_then(|value| value.as_str())
323                    .unwrap_or("")
324                    .to_owned(),
325                email: value
326                    .get("email")
327                    .and_then(|value| value.as_str())
328                    .map(String::from),
329                role: value
330                    .get("role")
331                    .and_then(|value| value.as_str())
332                    .unwrap_or("unknown")
333                    .to_owned(),
334                is_super: value
335                    .get("is_super")
336                    .and_then(serde_json::Value::as_bool)
337                    .unwrap_or(false),
338                last_login: None,
339            })
340            .collect())
341    }
342
343    pub async fn list_users(
344        &self,
345    ) -> Result<Vec<crate::session::models::SessionUserEntry>, CoreError> {
346        let guard = self.inner.session_client.lock().await;
347        let session = require_session(guard.as_ref())?;
348        Ok(session.list_users().await?)
349    }
350
351    pub async fn list_rogue_aps(
352        &self,
353        within_secs: Option<i64>,
354    ) -> Result<Vec<RogueAp>, CoreError> {
355        let guard = self.inner.session_client.lock().await;
356        let session = require_session(guard.as_ref())?;
357        Ok(session.list_rogue_aps(within_secs).await?)
358    }
359
360    pub async fn list_channels(&self) -> Result<Vec<ChannelAvailability>, CoreError> {
361        let guard = self.inner.session_client.lock().await;
362        let session = require_session(guard.as_ref())?;
363        Ok(session.list_channels().await?)
364    }
365
366    pub async fn get_client_roams(
367        &self,
368        mac: &str,
369        limit: Option<u32>,
370    ) -> Result<Vec<serde_json::Value>, CoreError> {
371        let guard = self.inner.session_client.lock().await;
372        let session = require_session(guard.as_ref())?;
373        Ok(session.get_client_roams(mac, limit).await?)
374    }
375
376    pub async fn get_client_wifi_experience(
377        &self,
378        client_ip: &str,
379    ) -> Result<serde_json::Value, CoreError> {
380        let guard = self.inner.session_client.lock().await;
381        let session = require_session(guard.as_ref())?;
382        Ok(session.get_client_wifi_experience(client_ip).await?)
383    }
384
385    pub async fn is_dpi_enabled(&self) -> Result<bool, CoreError> {
386        let guard = self.inner.session_client.lock().await;
387        let session = require_session(guard.as_ref())?;
388        let settings = session.get_site_settings().await?;
389        let enabled = settings
390            .iter()
391            .find(|s| s.get("key").and_then(|v| v.as_str()) == Some("dpi"))
392            .and_then(|s| s.get("enabled"))
393            .and_then(serde_json::Value::as_bool)
394            .unwrap_or(false);
395        Ok(enabled)
396    }
397
398    pub async fn list_alarms(&self) -> Result<Vec<Alarm>, CoreError> {
399        let guard = self.inner.session_client.lock().await;
400        let session = require_session(guard.as_ref())?;
401        let raw = session.list_alarms().await?;
402        Ok(raw.into_iter().map(Alarm::from).collect())
403    }
404
405    pub async fn get_system_info(&self) -> Result<SystemInfo, CoreError> {
406        {
407            let guard = self.inner.integration_client.lock().await;
408            if let Some(ic) = guard.as_ref() {
409                let info = ic.get_info().await?;
410                let fields = &info.fields;
411                return Ok(SystemInfo {
412                    controller_name: fields
413                        .get("applicationName")
414                        .or_else(|| fields.get("name"))
415                        .and_then(|value| value.as_str())
416                        .map(String::from),
417                    version: fields
418                        .get("applicationVersion")
419                        .or_else(|| fields.get("version"))
420                        .and_then(|value| value.as_str())
421                        .unwrap_or("unknown")
422                        .to_owned(),
423                    build: fields
424                        .get("build")
425                        .and_then(|value| value.as_str())
426                        .map(String::from),
427                    hostname: fields
428                        .get("hostname")
429                        .and_then(|value| value.as_str())
430                        .map(String::from),
431                    ip: None,
432                    uptime_secs: fields.get("uptime").and_then(serde_json::Value::as_u64),
433                    update_available: fields
434                        .get("isUpdateAvailable")
435                        .or_else(|| fields.get("update_available"))
436                        .and_then(serde_json::Value::as_bool),
437                });
438            }
439        }
440
441        let guard = self.inner.session_client.lock().await;
442        let session = require_session(guard.as_ref())?;
443        let raw = session.get_sysinfo().await?;
444        Ok(SystemInfo {
445            controller_name: raw
446                .get("controller_name")
447                .or_else(|| raw.get("name"))
448                .and_then(|value| value.as_str())
449                .map(String::from),
450            version: raw
451                .get("version")
452                .and_then(|value| value.as_str())
453                .unwrap_or("unknown")
454                .to_owned(),
455            build: raw
456                .get("build")
457                .and_then(|value| value.as_str())
458                .map(String::from),
459            hostname: raw
460                .get("hostname")
461                .and_then(|value| value.as_str())
462                .map(String::from),
463            ip: raw
464                .get("ip_addrs")
465                .and_then(|value| value.as_array())
466                .and_then(|values| values.first())
467                .and_then(|value| value.as_str())
468                .and_then(|value| value.parse().ok()),
469            uptime_secs: raw.get("uptime").and_then(serde_json::Value::as_u64),
470            update_available: raw
471                .get("update_available")
472                .and_then(serde_json::Value::as_bool),
473        })
474    }
475
476    pub async fn get_site_health(&self) -> Result<Vec<HealthSummary>, CoreError> {
477        let guard = self.inner.session_client.lock().await;
478        let session = require_session(guard.as_ref())?;
479        let raw = session.get_health().await?;
480        Ok(convert_health_summaries(raw))
481    }
482
483    pub async fn list_ipsec_sa(&self) -> Result<Vec<IpsecSa>, CoreError> {
484        let guard = self.inner.session_client.lock().await;
485        let session = require_session(guard.as_ref())?;
486        Ok(session.list_ipsec_sa().await?)
487    }
488
489    pub fn get_vpn_health(&self) -> Option<HealthSummary> {
490        self.inner
491            .store
492            .site_health_snapshot()
493            .iter()
494            .find(|summary| summary.subsystem.eq_ignore_ascii_case("vpn"))
495            .cloned()
496    }
497
498    pub async fn get_sysinfo(&self) -> Result<SysInfo, CoreError> {
499        let guard = self.inner.session_client.lock().await;
500        let session = require_session(guard.as_ref())?;
501        let raw = session.get_sysinfo().await?;
502        Ok(SysInfo {
503            timezone: raw
504                .get("timezone")
505                .and_then(|value| value.as_str())
506                .map(String::from),
507            autobackup: raw.get("autobackup").and_then(serde_json::Value::as_bool),
508            hostname: raw
509                .get("hostname")
510                .and_then(|value| value.as_str())
511                .map(String::from),
512            ip_addrs: raw
513                .get("ip_addrs")
514                .and_then(|value| value.as_array())
515                .map(|values| {
516                    values
517                        .iter()
518                        .filter_map(|value| value.as_str().map(String::from))
519                        .collect()
520                })
521                .unwrap_or_default(),
522            live_chat: raw
523                .get("live_chat")
524                .and_then(|value| value.as_str())
525                .map(String::from),
526            #[allow(clippy::as_conversions, clippy::cast_possible_truncation)]
527            data_retention_days: raw
528                .get("data_retention_days")
529                .and_then(serde_json::Value::as_u64)
530                .map(|value| value as u32),
531            extra: raw,
532        })
533    }
534
535    pub async fn list_vpn_settings(&self) -> Result<Vec<VpnSetting>, CoreError> {
536        let guard = self.inner.session_client.lock().await;
537        let session = require_session(guard.as_ref())?;
538        let raw = session.get_site_settings().await?;
539        let mut settings = raw
540            .iter()
541            .filter_map(vpn_setting_from_raw)
542            .collect::<Vec<_>>();
543        settings.sort_by(|left, right| left.key.cmp(&right.key));
544        Ok(settings)
545    }
546
547    pub async fn get_vpn_setting(&self, key: &str) -> Result<VpnSetting, CoreError> {
548        self.list_vpn_settings()
549            .await?
550            .into_iter()
551            .find(|setting| setting.key == key)
552            .ok_or_else(|| CoreError::NotFound {
553                entity_type: "vpn setting".into(),
554                identifier: key.into(),
555            })
556    }
557
558    pub async fn update_vpn_setting(
559        &self,
560        key: &str,
561        body: &serde_json::Value,
562    ) -> Result<VpnSetting, CoreError> {
563        if !VPN_SETTING_KEYS.contains(&key) {
564            return Err(CoreError::NotFound {
565                entity_type: "vpn setting".into(),
566                identifier: key.into(),
567            });
568        }
569
570        let guard = self.inner.session_client.lock().await;
571        let session = require_session(guard.as_ref())?;
572        session.set_site_setting(key, body).await?;
573        drop(guard);
574
575        self.get_vpn_setting(key).await
576    }
577
578    pub async fn get_all_site_settings(&self) -> Result<Vec<serde_json::Value>, CoreError> {
579        let guard = self.inner.session_client.lock().await;
580        let session = require_session(guard.as_ref())?;
581        Ok(session.get_site_settings().await?)
582    }
583
584    pub async fn get_site_setting(&self, key: &str) -> Result<serde_json::Value, CoreError> {
585        self.get_all_site_settings()
586            .await?
587            .into_iter()
588            .find(|s| s.get("key").and_then(|v| v.as_str()) == Some(key))
589            .ok_or_else(|| CoreError::NotFound {
590                entity_type: "setting".into(),
591                identifier: key.into(),
592            })
593    }
594
595    pub async fn update_site_setting(
596        &self,
597        key: &str,
598        body: &serde_json::Value,
599    ) -> Result<(), CoreError> {
600        let guard = self.inner.session_client.lock().await;
601        let session = require_session(guard.as_ref())?;
602        session.set_site_setting(key, body).await?;
603        Ok(())
604    }
605
606    /// Send a raw GET request to an arbitrary path on the controller.
607    ///
608    /// The `path` is appended to the controller base URL + platform prefix
609    /// (e.g. `/proxy/network/`). The response is returned as raw JSON
610    /// without session envelope unwrapping.
611    pub async fn raw_get(&self, path: &str) -> Result<serde_json::Value, CoreError> {
612        let guard = self.inner.session_client.lock().await;
613        let session = require_session(guard.as_ref())?;
614        Ok(session.raw_get(path).await?)
615    }
616
617    /// Send a raw POST request to an arbitrary path on the controller.
618    pub async fn raw_post(
619        &self,
620        path: &str,
621        body: &serde_json::Value,
622    ) -> Result<serde_json::Value, CoreError> {
623        let guard = self.inner.session_client.lock().await;
624        let session = require_session(guard.as_ref())?;
625        Ok(session.raw_post(path, body).await?)
626    }
627
628    /// Send a raw PUT request to an arbitrary path on the controller.
629    pub async fn raw_put(
630        &self,
631        path: &str,
632        body: &serde_json::Value,
633    ) -> Result<serde_json::Value, CoreError> {
634        let guard = self.inner.session_client.lock().await;
635        let session = require_session(guard.as_ref())?;
636        Ok(session.raw_put(path, body).await?)
637    }
638
639    /// Send a raw PATCH request to an arbitrary path on the controller.
640    pub async fn raw_patch(
641        &self,
642        path: &str,
643        body: &serde_json::Value,
644    ) -> Result<serde_json::Value, CoreError> {
645        let guard = self.inner.session_client.lock().await;
646        let session = require_session(guard.as_ref())?;
647        Ok(session.raw_patch(path, body).await?)
648    }
649
650    /// Send a raw DELETE request to an arbitrary path on the controller.
651    pub async fn raw_delete(&self, path: &str) -> Result<(), CoreError> {
652        let guard = self.inner.session_client.lock().await;
653        let session = require_session(guard.as_ref())?;
654        session.raw_delete(path).await?;
655        Ok(())
656    }
657}
658
659fn site_to_site_vpn_from_raw(raw: &serde_json::Value) -> Option<SiteToSiteVpn> {
660    let fields = raw.as_object()?;
661    if fields.get("purpose")?.as_str()? != "site-vpn" {
662        return None;
663    }
664
665    let id = fields.get("_id")?.as_str()?.to_owned();
666    let name = fields
667        .get("name")
668        .and_then(serde_json::Value::as_str)
669        .unwrap_or_default()
670        .to_owned();
671
672    let vpn_type = fields
673        .get("vpn_type")
674        .and_then(serde_json::Value::as_str)
675        .unwrap_or("unknown")
676        .to_owned();
677
678    let remote_host = fields
679        .get("ipsec_peer_ip")
680        .or_else(|| fields.get("openvpn_remote_host"))
681        .and_then(serde_json::Value::as_str)
682        .map(str::to_owned);
683
684    let local_ip = fields
685        .get("ipsec_local_ip")
686        .or_else(|| fields.get("openvpn_local_address"))
687        .and_then(serde_json::Value::as_str)
688        .map(str::to_owned);
689
690    let interface = fields
691        .get("ipsec_interface")
692        .and_then(serde_json::Value::as_str)
693        .map(str::to_owned);
694
695    let remote_site_id = fields
696        .get("remote_site_id")
697        .and_then(serde_json::Value::as_str)
698        .filter(|value| !value.is_empty())
699        .map(str::to_owned);
700
701    let remote_vpn_subnets = fields
702        .get("remote_vpn_subnets")
703        .and_then(serde_json::Value::as_array)
704        .map(|values| {
705            values
706                .iter()
707                .filter_map(serde_json::Value::as_str)
708                .map(str::to_owned)
709                .collect::<Vec<_>>()
710        })
711        .unwrap_or_default();
712
713    Some(SiteToSiteVpn {
714        id: EntityId::Legacy(id),
715        name,
716        enabled: fields
717            .get("enabled")
718            .and_then(serde_json::Value::as_bool)
719            .unwrap_or(true),
720        vpn_type,
721        remote_site_id,
722        local_ip,
723        interface,
724        remote_host,
725        remote_vpn_subnets,
726        fields: redact_sensitive_value(&serde_json::Value::Object(fields.clone()))
727            .as_object()
728            .cloned()
729            .unwrap_or_default(),
730    })
731}
732
733fn remote_access_vpn_server_from_raw(raw: &serde_json::Value) -> Option<RemoteAccessVpnServer> {
734    let fields = raw.as_object()?;
735    if fields.get("purpose")?.as_str()? != "remote-user-vpn" {
736        return None;
737    }
738
739    let id = fields.get("_id")?.as_str()?.to_owned();
740    let vpn_type = fields
741        .get("vpn_type")
742        .and_then(serde_json::Value::as_str)
743        .unwrap_or("unknown")
744        .to_owned();
745
746    Some(RemoteAccessVpnServer {
747        id: EntityId::Legacy(id),
748        name: fields
749            .get("name")
750            .and_then(serde_json::Value::as_str)
751            .unwrap_or_default()
752            .to_owned(),
753        enabled: fields
754            .get("enabled")
755            .and_then(serde_json::Value::as_bool)
756            .unwrap_or(true),
757        vpn_type,
758        local_port: fields
759            .get("local_port")
760            .and_then(serde_json::Value::as_u64)
761            .and_then(|value| u16::try_from(value).ok()),
762        local_wan_ip: first_non_empty_string(
763            fields,
764            &[
765                "wireguard_local_wan_ip",
766                "openvpn_local_wan_ip",
767                "l2tp_local_wan_ip",
768            ],
769        ),
770        interface: first_non_empty_string(
771            fields,
772            &["wireguard_interface", "openvpn_interface", "l2tp_interface"],
773        ),
774        gateway_subnet: first_non_empty_string(fields, &["ip_subnet", "ipv6_subnet"]),
775        radius_profile_id: first_non_empty_string(fields, &["radiusprofile_id"]),
776        exposed_to_site_vpn: fields
777            .get("exposed_to_site_vpn")
778            .and_then(serde_json::Value::as_bool),
779        fields: redact_sensitive_value(&serde_json::Value::Object(fields.clone()))
780            .as_object()
781            .cloned()
782            .unwrap_or_default(),
783    })
784}
785
786fn wireguard_peer_from_raw(
787    raw: &serde_json::Value,
788    default_server_id: Option<&str>,
789) -> Option<WireGuardPeer> {
790    let fields = raw.as_object()?;
791    let id = fields.get("_id")?.as_str()?.to_owned();
792    let server_id = first_non_empty_string(fields, &["networkId", "network_id"])
793        .or_else(|| default_server_id.map(str::to_owned))
794        .map(EntityId::Legacy);
795    let allowed_ips = fields
796        .get("allowed_ips")
797        .and_then(serde_json::Value::as_array)
798        .map(|values| {
799            values
800                .iter()
801                .filter_map(serde_json::Value::as_str)
802                .map(str::to_owned)
803                .collect::<Vec<_>>()
804        })
805        .unwrap_or_default();
806
807    Some(WireGuardPeer {
808        id: EntityId::Legacy(id),
809        server_id,
810        name: fields
811            .get("name")
812            .and_then(serde_json::Value::as_str)
813            .unwrap_or_default()
814            .to_owned(),
815        interface_ip: first_non_empty_string(fields, &["interface_ip"]),
816        interface_ipv6: first_non_empty_string(fields, &["interface_ipv6"]),
817        public_key: first_non_empty_string(fields, &["public_key"]),
818        allowed_ips,
819        has_private_key: fields
820            .get("private_key")
821            .and_then(serde_json::Value::as_str)
822            .is_some_and(|value| !value.is_empty()),
823        has_preshared_key: fields
824            .get("preshared_key")
825            .and_then(serde_json::Value::as_str)
826            .is_some_and(|value| !value.is_empty()),
827        fields: redact_sensitive_value(&serde_json::Value::Object(fields.clone()))
828            .as_object()
829            .cloned()
830            .unwrap_or_default(),
831    })
832}
833
834fn vpn_client_connection_from_raw(raw: &serde_json::Value) -> Option<VpnClientConnection> {
835    let fields = raw.as_object()?;
836    let id = first_non_empty_string(fields, &["network_id", "networkId", "id", "_id"])?;
837
838    Some(VpnClientConnection {
839        id: EntityId::Legacy(id),
840        name: first_non_empty_string(
841            fields,
842            &[
843                "name",
844                "display_name",
845                "network_name",
846                "server_name",
847                "remote_name",
848            ],
849        ),
850        connection_type: first_non_empty_string(fields, &["type", "vpn_type", "connection_type"]),
851        status: first_non_empty_string(fields, &["status", "state"]),
852        local_address: first_non_empty_string(
853            fields,
854            &["local_ip", "local_address", "localAddress"],
855        ),
856        remote_address: first_non_empty_string(
857            fields,
858            &[
859                "remote_ip",
860                "remote_address",
861                "remoteAddress",
862                "server_ip",
863                "serverAddress",
864                "remote_host",
865            ],
866        ),
867        username: first_non_empty_string(fields, &["username", "user"]),
868        fields: redact_sensitive_value(&serde_json::Value::Object(fields.clone()))
869            .as_object()
870            .cloned()
871            .unwrap_or_default(),
872    })
873}
874
875fn vpn_client_profile_from_raw(raw: &serde_json::Value) -> Option<VpnClientProfile> {
876    let fields = raw.as_object()?;
877    let vpn_type = first_non_empty_string(fields, &["vpn_type", "type"]).unwrap_or_default();
878
879    match fields.get("purpose").and_then(serde_json::Value::as_str) {
880        Some("vpn-client") => {}
881        Some(_) => return None,
882        None if !vpn_type.ends_with("-client") => return None,
883        None => {}
884    }
885
886    let id = first_non_empty_string(fields, &["_id", "id"])?;
887
888    Some(VpnClientProfile {
889        id: EntityId::Legacy(id),
890        name: fields
891            .get("name")
892            .and_then(serde_json::Value::as_str)
893            .unwrap_or_default()
894            .to_owned(),
895        enabled: fields
896            .get("enabled")
897            .and_then(serde_json::Value::as_bool)
898            .unwrap_or(true),
899        vpn_type,
900        server_address: first_non_empty_string(
901            fields,
902            &[
903                "remote_host",
904                "remote_address",
905                "server_address",
906                "server_ip",
907                "server",
908                "host",
909                "peer_ip",
910            ],
911        ),
912        server_port: ["remote_port", "server_port", "port"]
913            .iter()
914            .find_map(|key| {
915                fields
916                    .get(*key)
917                    .and_then(serde_json::Value::as_u64)
918                    .and_then(|value| u16::try_from(value).ok())
919            }),
920        local_address: first_non_empty_string(
921            fields,
922            &[
923                "local_address",
924                "local_ip",
925                "interface_ip",
926                "vpn_ip",
927                "openvpn_local_address",
928                "wireguard_local_address",
929            ],
930        ),
931        username: first_non_empty_string(fields, &["username", "user"]),
932        interface: first_non_empty_string(
933            fields,
934            &[
935                "interface",
936                "wan_interface",
937                "wan_network",
938                "bind_interface",
939                "openvpn_interface",
940                "wireguard_interface",
941            ],
942        ),
943        route_distance: fields
944            .get("route_distance")
945            .and_then(serde_json::Value::as_u64)
946            .and_then(|value| u32::try_from(value).ok()),
947        fields: redact_sensitive_value(&serde_json::Value::Object(fields.clone()))
948            .as_object()
949            .cloned()
950            .unwrap_or_default(),
951    })
952}
953
954fn magic_site_to_site_vpn_config_from_raw(
955    raw: &serde_json::Value,
956) -> Option<MagicSiteToSiteVpnConfig> {
957    let fields = raw.as_object()?;
958    let id = first_non_empty_string(fields, &["id", "_id"])?;
959
960    Some(MagicSiteToSiteVpnConfig {
961        id: EntityId::Legacy(id),
962        name: first_non_empty_string(fields, &["name", "display_name"]),
963        status: first_non_empty_string(fields, &["status", "state"]),
964        enabled: fields.get("enabled").and_then(serde_json::Value::as_bool),
965        local_site_name: first_non_empty_string(
966            fields,
967            &["localSiteName", "local_site_name", "local_site"],
968        ),
969        remote_site_name: first_non_empty_string(
970            fields,
971            &["remoteSiteName", "remote_site_name", "remote_site"],
972        ),
973        fields: redact_sensitive_value(&serde_json::Value::Object(fields.clone()))
974            .as_object()
975            .cloned()
976            .unwrap_or_default(),
977    })
978}
979
980fn first_non_empty_string(
981    fields: &serde_json::Map<String, serde_json::Value>,
982    keys: &[&str],
983) -> Option<String> {
984    keys.iter().find_map(|key| {
985        fields
986            .get(*key)
987            .and_then(serde_json::Value::as_str)
988            .filter(|value| !value.is_empty())
989            .map(str::to_owned)
990    })
991}
992
993fn vpn_setting_from_raw(raw: &serde_json::Value) -> Option<VpnSetting> {
994    let object = raw.as_object()?;
995    let key = object.get("key")?.as_str()?;
996    if !VPN_SETTING_KEYS.contains(&key) {
997        return None;
998    }
999
1000    let mut fields = object.clone();
1001    fields.remove("_id");
1002    fields.remove("key");
1003    fields.remove("site_id");
1004    let fields = redact_sensitive_value(&serde_json::Value::Object(fields))
1005        .as_object()
1006        .cloned()
1007        .unwrap_or_default();
1008
1009    Some(VpnSetting {
1010        key: key.to_owned(),
1011        enabled: fields.get("enabled").and_then(serde_json::Value::as_bool),
1012        fields,
1013    })
1014}
1015
1016fn redact_sensitive_value(value: &serde_json::Value) -> serde_json::Value {
1017    match value {
1018        serde_json::Value::Object(map) => serde_json::Value::Object(
1019            map.iter()
1020                .map(|(key, value)| {
1021                    (
1022                        key.clone(),
1023                        if should_redact_field(key) {
1024                            serde_json::Value::String("***REDACTED***".into())
1025                        } else {
1026                            redact_sensitive_value(value)
1027                        },
1028                    )
1029                })
1030                .collect(),
1031        ),
1032        serde_json::Value::Array(values) => {
1033            serde_json::Value::Array(values.iter().map(redact_sensitive_value).collect())
1034        }
1035        _ => value.clone(),
1036    }
1037}
1038
1039fn should_redact_field(key: &str) -> bool {
1040    let key = key.to_ascii_lowercase();
1041    [
1042        "private",
1043        "password",
1044        "secret",
1045        "token",
1046        "psk",
1047        "shared_key",
1048        "certificate",
1049        "dh_key",
1050    ]
1051    .into_iter()
1052    .any(|needle| key.contains(needle))
1053}
1054
1055#[cfg(test)]
1056mod tests {
1057    use std::sync::Arc;
1058
1059    use super::{
1060        Controller, magic_site_to_site_vpn_config_from_raw, redact_sensitive_value,
1061        remote_access_vpn_server_from_raw, site_to_site_vpn_from_raw,
1062        vpn_client_connection_from_raw, vpn_client_profile_from_raw, vpn_setting_from_raw,
1063        wireguard_peer_from_raw,
1064    };
1065    use crate::config::ControllerConfig;
1066    use crate::model::{EntityId, HealthSummary};
1067
1068    #[test]
1069    fn get_vpn_health_reads_cached_store_snapshot() {
1070        let controller = Controller::new(ControllerConfig::default());
1071        let summaries = Arc::new(vec![
1072            HealthSummary {
1073                subsystem: "wan".into(),
1074                status: "ok".into(),
1075                num_adopted: Some(1),
1076                num_sta: Some(5),
1077                tx_bytes_r: Some(100),
1078                rx_bytes_r: Some(200),
1079                latency: Some(1.0),
1080                wan_ip: Some("198.51.100.1".into()),
1081                gateways: Some(vec!["gw".into()]),
1082                extra: serde_json::Value::Null,
1083            },
1084            HealthSummary {
1085                subsystem: "vpn".into(),
1086                status: "warn".into(),
1087                num_adopted: None,
1088                num_sta: Some(2),
1089                tx_bytes_r: Some(300),
1090                rx_bytes_r: Some(400),
1091                latency: Some(2.5),
1092                wan_ip: None,
1093                gateways: None,
1094                extra: serde_json::json!({ "source": "cache" }),
1095            },
1096        ]);
1097        let _previous = controller.inner.store.site_health.send_replace(summaries);
1098
1099        let vpn_health = controller.get_vpn_health().expect("vpn summary");
1100
1101        assert_eq!(vpn_health.subsystem, "vpn");
1102        assert_eq!(vpn_health.status, "warn");
1103        assert_eq!(vpn_health.num_sta, Some(2));
1104        assert_eq!(vpn_health.extra["source"], "cache");
1105    }
1106
1107    #[test]
1108    fn site_to_site_vpn_from_raw_maps_legacy_networkconf_record() {
1109        let raw = serde_json::json!({
1110            "_id": "vpn123",
1111            "name": "Branch Tunnel",
1112            "purpose": "site-vpn",
1113            "enabled": true,
1114            "vpn_type": "ipsec-vpn",
1115            "ipsec_local_ip": "10.0.0.1",
1116            "ipsec_interface": "WAN1",
1117            "ipsec_peer_ip": "198.51.100.10",
1118            "remote_vpn_subnets": ["10.10.0.0/24"]
1119        });
1120
1121        let vpn = site_to_site_vpn_from_raw(&raw).expect("site-vpn should map");
1122
1123        assert_eq!(vpn.id, EntityId::Legacy("vpn123".into()));
1124        assert_eq!(vpn.name, "Branch Tunnel");
1125        assert_eq!(vpn.vpn_type, "ipsec-vpn");
1126        assert_eq!(vpn.local_ip.as_deref(), Some("10.0.0.1"));
1127        assert_eq!(vpn.interface.as_deref(), Some("WAN1"));
1128        assert_eq!(vpn.remote_host.as_deref(), Some("198.51.100.10"));
1129        assert_eq!(vpn.remote_vpn_subnets, vec!["10.10.0.0/24"]);
1130    }
1131
1132    #[test]
1133    fn vpn_setting_from_raw_filters_to_known_keys() {
1134        let raw = serde_json::json!({
1135            "key": "teleport",
1136            "enabled": true,
1137            "_id": "abc123",
1138            "site_id": "default",
1139        });
1140        let setting = vpn_setting_from_raw(&raw).expect("teleport should be recognized");
1141
1142        assert_eq!(setting.key, "teleport");
1143        assert_eq!(setting.enabled, Some(true));
1144        assert!(!setting.fields.contains_key("_id"));
1145        assert!(!setting.fields.contains_key("site_id"));
1146    }
1147
1148    #[test]
1149    fn remote_access_vpn_server_from_raw_maps_legacy_networkconf_record() {
1150        let raw = serde_json::json!({
1151            "_id": "vpn456",
1152            "name": "WireGuard Remote Access",
1153            "purpose": "remote-user-vpn",
1154            "enabled": true,
1155            "vpn_type": "wireguard",
1156            "local_port": 51820,
1157            "wireguard_local_wan_ip": "203.0.113.10",
1158            "wireguard_interface": "WAN1",
1159            "ip_subnet": "192.168.42.1/24",
1160            "radiusprofile_id": "radius123",
1161            "exposed_to_site_vpn": true,
1162            "x_wireguard_private_key": "secret"
1163        });
1164
1165        let server = remote_access_vpn_server_from_raw(&raw).expect("remote-user-vpn should map");
1166
1167        assert_eq!(server.id, EntityId::Legacy("vpn456".into()));
1168        assert_eq!(server.name, "WireGuard Remote Access");
1169        assert_eq!(server.vpn_type, "wireguard");
1170        assert_eq!(server.local_port, Some(51820));
1171        assert_eq!(server.local_wan_ip.as_deref(), Some("203.0.113.10"));
1172        assert_eq!(server.interface.as_deref(), Some("WAN1"));
1173        assert_eq!(server.gateway_subnet.as_deref(), Some("192.168.42.1/24"));
1174        assert_eq!(server.radius_profile_id.as_deref(), Some("radius123"));
1175        assert_eq!(server.exposed_to_site_vpn, Some(true));
1176        assert_eq!(
1177            server.fields["x_wireguard_private_key"].as_str(),
1178            Some("***REDACTED***")
1179        );
1180    }
1181
1182    #[test]
1183    fn wireguard_peer_from_raw_maps_legacy_v2_record() {
1184        let raw = serde_json::json!({
1185            "_id": "peer789",
1186            "networkId": "server123",
1187            "name": "Laptop",
1188            "interface_ip": "192.168.42.2",
1189            "interface_ipv6": "fd00::2",
1190            "public_key": "pubkey",
1191            "private_key": "secret",
1192            "preshared_key": "psk",
1193            "allowed_ips": ["10.0.0.0/24"]
1194        });
1195
1196        let peer = wireguard_peer_from_raw(&raw, None).expect("WireGuard peer should map");
1197
1198        assert_eq!(peer.id, EntityId::Legacy("peer789".into()));
1199        assert_eq!(peer.server_id, Some(EntityId::Legacy("server123".into())));
1200        assert_eq!(peer.name, "Laptop");
1201        assert_eq!(peer.interface_ip.as_deref(), Some("192.168.42.2"));
1202        assert_eq!(peer.interface_ipv6.as_deref(), Some("fd00::2"));
1203        assert_eq!(peer.public_key.as_deref(), Some("pubkey"));
1204        assert_eq!(peer.allowed_ips, vec!["10.0.0.0/24"]);
1205        assert!(peer.has_private_key);
1206        assert!(peer.has_preshared_key);
1207        assert_eq!(peer.fields["private_key"].as_str(), Some("***REDACTED***"));
1208        assert_eq!(
1209            peer.fields["preshared_key"].as_str(),
1210            Some("***REDACTED***")
1211        );
1212    }
1213
1214    #[test]
1215    fn vpn_client_connection_from_raw_maps_v2_record() {
1216        let raw = serde_json::json!({
1217            "network_id": "vpn-client-1",
1218            "name": "Branch Client",
1219            "type": "openvpn-client",
1220            "status": "CONNECTED",
1221            "local_ip": "10.0.0.2",
1222            "remote_ip": "198.51.100.10",
1223            "username": "branch-user",
1224            "password": "secret"
1225        });
1226
1227        let connection = vpn_client_connection_from_raw(&raw).expect("VPN connection should map");
1228
1229        assert_eq!(connection.id, EntityId::Legacy("vpn-client-1".into()));
1230        assert_eq!(connection.name.as_deref(), Some("Branch Client"));
1231        assert_eq!(
1232            connection.connection_type.as_deref(),
1233            Some("openvpn-client")
1234        );
1235        assert_eq!(connection.status.as_deref(), Some("CONNECTED"));
1236        assert_eq!(connection.local_address.as_deref(), Some("10.0.0.2"));
1237        assert_eq!(connection.remote_address.as_deref(), Some("198.51.100.10"));
1238        assert_eq!(connection.username.as_deref(), Some("branch-user"));
1239        assert_eq!(
1240            connection.fields["password"].as_str(),
1241            Some("***REDACTED***")
1242        );
1243    }
1244
1245    #[test]
1246    fn vpn_client_profile_from_raw_maps_legacy_networkconf_record() {
1247        let raw = serde_json::json!({
1248            "_id": "vpn-client-1",
1249            "name": "Branch Client",
1250            "purpose": "vpn-client",
1251            "enabled": false,
1252            "vpn_type": "openvpn-client",
1253            "remote_host": "198.51.100.10",
1254            "remote_port": 1194,
1255            "local_address": "10.0.0.2",
1256            "username": "branch-user",
1257            "interface": "WAN1",
1258            "route_distance": 15,
1259            "password": "secret"
1260        });
1261
1262        let client = vpn_client_profile_from_raw(&raw).expect("vpn-client should map");
1263
1264        assert_eq!(client.id, EntityId::Legacy("vpn-client-1".into()));
1265        assert_eq!(client.name, "Branch Client");
1266        assert!(!client.enabled);
1267        assert_eq!(client.vpn_type, "openvpn-client");
1268        assert_eq!(client.server_address.as_deref(), Some("198.51.100.10"));
1269        assert_eq!(client.server_port, Some(1194));
1270        assert_eq!(client.local_address.as_deref(), Some("10.0.0.2"));
1271        assert_eq!(client.username.as_deref(), Some("branch-user"));
1272        assert_eq!(client.interface.as_deref(), Some("WAN1"));
1273        assert_eq!(client.route_distance, Some(15));
1274        assert_eq!(client.fields["password"].as_str(), Some("***REDACTED***"));
1275    }
1276
1277    #[test]
1278    fn magic_site_to_site_vpn_config_from_raw_maps_v2_record() {
1279        let raw = serde_json::json!({
1280            "id": "magic-1",
1281            "name": "HQ <-> Branch",
1282            "status": "CONNECTED",
1283            "enabled": true,
1284            "localSiteName": "HQ",
1285            "remoteSiteName": "Branch",
1286            "token": "secret"
1287        });
1288
1289        let config =
1290            magic_site_to_site_vpn_config_from_raw(&raw).expect("magic site-to-site should map");
1291
1292        assert_eq!(config.id, EntityId::Legacy("magic-1".into()));
1293        assert_eq!(config.name.as_deref(), Some("HQ <-> Branch"));
1294        assert_eq!(config.status.as_deref(), Some("CONNECTED"));
1295        assert_eq!(config.enabled, Some(true));
1296        assert_eq!(config.local_site_name.as_deref(), Some("HQ"));
1297        assert_eq!(config.remote_site_name.as_deref(), Some("Branch"));
1298        assert_eq!(config.fields["token"].as_str(), Some("***REDACTED***"));
1299    }
1300
1301    #[test]
1302    fn redact_sensitive_value_masks_nested_vpn_secrets() {
1303        let redacted = redact_sensitive_value(&serde_json::json!({
1304            "enabled": true,
1305            "public_key": "keep-me",
1306            "x_private_key": "secret",
1307            "nested": {
1308                "psk": "hide-me",
1309                "certificatePem": "hide-me-too"
1310            }
1311        }));
1312
1313        assert_eq!(
1314            redacted.get("enabled").and_then(serde_json::Value::as_bool),
1315            Some(true)
1316        );
1317        assert_eq!(
1318            redacted
1319                .get("public_key")
1320                .and_then(serde_json::Value::as_str),
1321            Some("keep-me")
1322        );
1323        assert_eq!(
1324            redacted
1325                .get("x_private_key")
1326                .and_then(serde_json::Value::as_str),
1327            Some("***REDACTED***")
1328        );
1329        assert_eq!(redacted["nested"]["psk"].as_str(), Some("***REDACTED***"));
1330        assert_eq!(
1331            redacted["nested"]["certificatePem"].as_str(),
1332            Some("***REDACTED***")
1333        );
1334    }
1335}