hightower_node/
context.rs

1use std::borrow::Cow;
2use std::env::VarError;
3use std::error::Error;
4use std::fmt;
5use std::path::Path;
6use std::sync::Arc;
7
8mod kv {
9    use hightower_kv::{
10        AuthService, Error as KvError, KvEngine, SingleNodeEngine, StoreConfig,
11        command::Command,
12        crypto::{AesGcmEncryptor, Argon2SecretHasher},
13    };
14    use rand::RngCore;
15    use std::error::Error;
16    use std::fmt;
17    use std::fs;
18    use std::path::{Path, PathBuf};
19    use std::sync::Arc;
20    use std::time::{SystemTime, UNIX_EPOCH};
21    use tempfile::{Builder as TempDirBuilder, TempDir};
22    use tracing::{debug, info, warn};
23
24    const AUTH_MASTER_KEY: &[u8] = b"secrets/auth_master_key";
25
26    type SharedEngine = Arc<SingleNodeEngine>;
27    type SharedAuthService = AuthService<SharedEngine, Argon2SecretHasher, AesGcmEncryptor>;
28
29    pub type GatewayAuthService = SharedAuthService;
30
31    #[derive(Debug)]
32    pub enum KvInitError {
33        TempDir(std::io::Error),
34        CreateDir(std::io::Error),
35        Store(KvError),
36    }
37
38    impl fmt::Display for KvInitError {
39        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40            match self {
41                KvInitError::TempDir(err) => {
42                    write!(f, "failed to create temporary directory: {}", err)
43                }
44                KvInitError::CreateDir(err) => {
45                    write!(f, "failed to create data directory: {}", err)
46                }
47                KvInitError::Store(err) => {
48                    write!(f, "failed to start key-value engine: {}", err)
49                }
50            }
51        }
52    }
53
54    impl Error for KvInitError {
55        fn source(&self) -> Option<&(dyn Error + 'static)> {
56            match self {
57                KvInitError::TempDir(err) | KvInitError::CreateDir(err) => Some(err),
58                KvInitError::Store(err) => Some(err),
59            }
60        }
61    }
62
63    pub struct KvHandle {
64        engine: SharedEngine,
65        auth: Arc<SharedAuthService>,
66        #[allow(dead_code)]
67        data_dir: PathBuf,
68        #[allow(dead_code)]
69        temp_dir: Option<TempDir>,
70    }
71
72    impl fmt::Debug for KvHandle {
73        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74            f.debug_struct("KvHandle")
75                .field("data_dir", &self.data_dir)
76                .field(
77                    "temp_dir",
78                    &self.temp_dir.as_ref().map(|dir| dir.path().to_path_buf()),
79                )
80                .finish()
81        }
82    }
83
84    impl KvHandle {
85        pub fn data_dir(&self) -> &Path {
86            &self.data_dir
87        }
88
89        #[cfg(test)]
90        pub fn temp_dir_path(&self) -> Option<&Path> {
91            self.temp_dir.as_ref().map(|dir| dir.path())
92        }
93
94        pub fn put_bytes(&self, key: &[u8], value: &[u8]) -> Result<(), KvError> {
95            let (version, timestamp) = monotonic_version_timestamp();
96            self.engine.submit(Command::Set {
97                key: key.to_vec(),
98                value: value.to_vec(),
99                version,
100                timestamp,
101            })?;
102            Ok(())
103        }
104
105        pub fn put_secret(&self, key: &[u8], value: &[u8]) {
106            if let Err(err) = self.put_bytes(key, value) {
107                warn!(?err, "Failed to persist secret to KV");
108            }
109        }
110
111        pub fn get_bytes(&self, key: &[u8]) -> Result<Option<Vec<u8>>, KvError> {
112            self.engine.get(key)
113        }
114
115        pub fn auth(&self) -> Arc<SharedAuthService> {
116            Arc::clone(&self.auth)
117        }
118
119        pub fn get_prefix(&self, prefix: &[u8]) -> Result<Vec<(Vec<u8>, Vec<u8>)>, KvError> {
120            self.engine.get_prefix(prefix)
121        }
122    }
123
124    pub fn initialize(dir: Option<&Path>) -> Result<KvHandle, KvInitError> {
125        let (data_dir, temp_dir) = match dir {
126            Some(path) => {
127                fs::create_dir_all(path).map_err(KvInitError::CreateDir)?;
128                (path.to_path_buf(), None)
129            }
130            None => {
131                let temp = TempDirBuilder::new()
132                    .prefix("hightower-kv")
133                    .tempdir()
134                    .map_err(KvInitError::TempDir)?;
135                (temp.path().to_path_buf(), Some(temp))
136            }
137        };
138
139        let mut config = StoreConfig::default();
140        config.data_dir = data_dir.to_string_lossy().into_owned();
141
142        let engine = SingleNodeEngine::with_config(config).map_err(KvInitError::Store)?;
143        let master_key = load_or_initialize_master_key(&engine).map_err(KvInitError::Store)?;
144        let (engine, auth) = engine.into_argon2_hasher_aes_gcm_auth_service(master_key);
145        let auth = Arc::new(auth);
146        let temporary = temp_dir.is_some();
147        debug!(path = %data_dir.display(), temporary, "Initialized key-value store");
148
149        Ok(KvHandle {
150            engine,
151            auth,
152            data_dir,
153            temp_dir,
154        })
155    }
156
157    fn load_or_initialize_master_key(engine: &SingleNodeEngine) -> Result<[u8; 32], KvError> {
158        match engine.get(AUTH_MASTER_KEY)? {
159            Some(bytes) => match <[u8; 32]>::try_from(bytes.as_slice()) {
160                Ok(key) => Ok(key),
161                Err(_) => {
162                    warn!(
163                        stored_len = bytes.len(),
164                        "Stored auth master key has unexpected length; generating a new key"
165                    );
166                    generate_and_persist_master_key(engine)
167                }
168            },
169            None => generate_and_persist_master_key(engine),
170        }
171    }
172
173    fn generate_and_persist_master_key(engine: &SingleNodeEngine) -> Result<[u8; 32], KvError> {
174        let mut key = [0u8; 32];
175        rand::thread_rng().fill_bytes(&mut key);
176        let (version, timestamp) = monotonic_version_timestamp();
177        engine.submit(Command::Set {
178            key: AUTH_MASTER_KEY.to_vec(),
179            value: key.to_vec(),
180            version,
181            timestamp,
182        })?;
183        info!("Provisioned new auth master key");
184        Ok(key)
185    }
186
187    fn monotonic_version_timestamp() -> (u64, i64) {
188        let now = SystemTime::now()
189            .duration_since(UNIX_EPOCH)
190            .unwrap_or_default();
191        let millis = now.as_millis();
192        let version = millis.min(u64::MAX as u128) as u64;
193        let timestamp = millis.min(i64::MAX as u128) as i64;
194        (version, timestamp)
195    }
196
197    #[cfg(test)]
198    mod tests {
199        use super::*;
200
201        #[test]
202        fn initialize_uses_provided_directory() {
203            let temp_root = TempDirBuilder::new()
204                .prefix("hightower-test")
205                .tempdir()
206                .unwrap();
207            let target = temp_root.path().join("kv-store");
208
209            let handle = initialize(Some(&target)).expect("kv init succeeds");
210
211            assert_eq!(handle.data_dir(), target.as_path());
212            assert!(target.exists());
213            assert!(handle.temp_dir_path().is_none());
214        }
215
216        #[test]
217        fn initialize_falls_back_to_temp_directory() {
218            let handle = initialize(None).expect("kv init succeeds");
219
220            let temp_dir = handle.temp_dir_path().expect("temp dir is retained");
221            assert!(temp_dir.exists());
222            assert!(handle.data_dir().starts_with(temp_dir));
223        }
224
225        #[test]
226        fn put_bytes_persists_values() {
227            let handle = initialize(None).expect("kv init succeeds");
228            let key = b"kv-tests/cert";
229            let value = b"payload";
230
231            handle.put_bytes(key, value).expect("write succeeds");
232
233            let stored = handle.get_bytes(key).expect("read succeeds");
234            assert_eq!(stored, Some(value.to_vec()));
235        }
236
237        #[test]
238        fn put_secret_does_not_panic_on_failure() {
239            let handle = initialize(None).expect("kv init succeeds");
240            let key = b"kv-tests/secret";
241
242            handle.put_secret(key, b"secret");
243            let stored = handle.get_bytes(key).expect("read succeeds");
244            assert!(stored.is_some());
245        }
246    }
247}
248
249mod token {
250    use std::env::VarError;
251    use std::fmt;
252
253    #[derive(Debug, PartialEq, Eq)]
254    pub enum TokenError {
255        Missing,
256    }
257
258    impl fmt::Display for TokenError {
259        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260            match self {
261                TokenError::Missing => {
262                    write!(f, "HT_AUTH_KEY environment variable must be set.")
263                }
264            }
265        }
266    }
267
268    impl std::error::Error for TokenError {}
269
270    pub fn fetch<F>(mut lookup: F) -> Result<String, TokenError>
271    where
272        F: FnMut(&str) -> Result<String, VarError>,
273    {
274        lookup("HT_AUTH_KEY").map_err(|_| TokenError::Missing)
275    }
276
277    #[cfg(test)]
278    mod tests {
279        use super::*;
280
281        #[test]
282        fn fetch_returns_token_when_present() {
283            let token = fetch(|_| Ok(String::from("123"))).expect("token should be present");
284
285            assert_eq!(token, "123");
286        }
287
288        #[test]
289        fn fetch_reports_missing_token() {
290            let error =
291                fetch(|_| Err(VarError::NotPresent)).expect_err("token should be missing");
292
293            assert_eq!(error, TokenError::Missing);
294        }
295    }
296}
297
298pub use kv::{GatewayAuthService, KvHandle, KvInitError, initialize as initialize_kv};
299pub use token::{TokenError, fetch as fetch_token};
300
301pub const NODE_NAME_KEY: &[u8] = b"nodes/name";
302pub const NODE_CERTIFICATE_KEY: &[u8] = b"certificates/node";
303pub const GATEWAY_CERTIFICATE_KEY: &[u8] = b"certificates/gateway";
304pub const GATEWAY_PUBLIC_KEY: &[u8] = b"certificates/gateway_public_key";
305pub const HT_AUTH_KEY: &[u8] = b"secrets/ht_auth_key";
306const DEFAULT_AUTH_USERNAME_ENV: &str = "HT_DEFAULT_USER";
307const DEFAULT_AUTH_PASSWORD_ENV: &str = "HT_DEFAULT_PASSWORD";
308const DEFAULT_AUTH_USERNAME: &str = "admin";
309const DEFAULT_AUTH_PASSWORD: &str = "admin";
310
311#[derive(Clone)]
312pub struct CommonContext {
313    pub kv: NamespacedKv,
314    pub auth: Arc<GatewayAuthService>,
315}
316
317impl CommonContext {
318    pub fn new(kv: KvHandle) -> Self {
319        let kv = NamespacedKv::from_handle(kv);
320        let auth = kv.auth_service();
321        Self { kv, auth }
322    }
323
324    pub fn namespaced(&self, prefix: &[u8]) -> Self {
325        Self {
326            kv: self.kv.clone_with_additional_prefix(prefix),
327            auth: Arc::clone(&self.auth),
328        }
329    }
330}
331
332impl fmt::Debug for CommonContext {
333    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
334        f.debug_struct("CommonContext")
335            .field("kv", &self.kv)
336            .finish()
337    }
338}
339
340#[derive(Clone, Debug)]
341pub struct NamespacedKv {
342    inner: Arc<KvHandle>,
343    prefix: Option<Vec<u8>>,
344}
345
346impl NamespacedKv {
347    pub fn from_handle(kv: KvHandle) -> Self {
348        Self {
349            inner: Arc::new(kv),
350            prefix: None,
351        }
352    }
353
354    fn with_parts(inner: Arc<KvHandle>, prefix: Option<Vec<u8>>) -> Self {
355        Self { inner, prefix }
356    }
357
358    pub fn clone_with_additional_prefix(&self, additional: &[u8]) -> Self {
359        let prefix = match &self.prefix {
360            Some(existing) => {
361                let mut composed = existing.clone();
362                if !existing.is_empty() {
363                    composed.push(b'/');
364                }
365                composed.extend_from_slice(additional);
366                composed
367            }
368            None => additional.to_vec(),
369        };
370
371        Self::with_parts(Arc::clone(&self.inner), Some(prefix))
372    }
373
374    pub fn put_bytes(&self, key: &[u8], value: &[u8]) -> Result<(), hightower_kv::Error> {
375        let key = self.prefixed_key(key);
376        self.inner.put_bytes(key.as_ref(), value)
377    }
378
379    pub fn put_secret(&self, key: &[u8], value: &[u8]) {
380        let key = self.prefixed_key(key);
381        self.inner.put_secret(key.as_ref(), value);
382    }
383
384    pub fn get_bytes(&self, key: &[u8]) -> Result<Option<Vec<u8>>, hightower_kv::Error> {
385        let key = self.prefixed_key(key);
386        self.inner.get_bytes(key.as_ref())
387    }
388
389    pub fn auth_service(&self) -> Arc<GatewayAuthService> {
390        self.inner.auth()
391    }
392
393    pub fn list_by_prefix(
394        &self,
395        prefix: &[u8],
396    ) -> Result<Vec<(Vec<u8>, Vec<u8>)>, hightower_kv::Error> {
397        let full_prefix = match self.prefixed_key(prefix) {
398            Cow::Borrowed(bytes) => bytes.to_vec(),
399            Cow::Owned(bytes) => bytes,
400        };
401
402        let entries = self.inner.get_prefix(&full_prefix)?;
403
404        let mut results = Vec::new();
405        for (key, value) in entries {
406            let mut remainder = &key[full_prefix.len()..];
407            if remainder.first() == Some(&b'/') {
408                remainder = &remainder[1..];
409            }
410            results.push((remainder.to_vec(), value));
411        }
412
413        Ok(results)
414    }
415
416    fn prefixed_key<'a>(&self, key: &'a [u8]) -> Cow<'a, [u8]> {
417        match &self.prefix {
418            Some(prefix) if !prefix.is_empty() => {
419                let mut composed = Vec::with_capacity(prefix.len() + 1 + key.len());
420                composed.extend_from_slice(prefix);
421                composed.push(b'/');
422                composed.extend_from_slice(key);
423                Cow::Owned(composed)
424            }
425            _ => Cow::Borrowed(key),
426        }
427    }
428}
429
430#[derive(Debug)]
431pub enum ContextError {
432    Token(TokenError),
433    Kv(KvInitError),
434    Auth(hightower_kv::Error),
435}
436
437impl fmt::Display for ContextError {
438    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
439        match self {
440            ContextError::Token(err) => write!(f, "failed to read HT_AUTH_KEY: {}", err),
441            ContextError::Kv(err) => write!(f, "failed to initialize key-value store: {}", err),
442            ContextError::Auth(err) => write!(f, "failed to bootstrap auth service: {}", err),
443        }
444    }
445}
446
447impl Error for ContextError {
448    fn source(&self) -> Option<&(dyn Error + 'static)> {
449        match self {
450            ContextError::Token(err) => Some(err),
451            ContextError::Kv(err) => Some(err),
452            ContextError::Auth(err) => Some(err),
453        }
454    }
455}
456
457pub fn initialize_with_token_source<F>(
458    kv_path: Option<&Path>,
459    mut lookup: F,
460) -> Result<CommonContext, ContextError>
461where
462    F: FnMut(&str) -> Result<String, VarError>,
463{
464    let token = token::fetch(|key| lookup(key)).map_err(ContextError::Token)?;
465    initialize_with_token(kv_path, token)
466}
467
468pub fn initialize_with_token(
469    kv_path: Option<&Path>,
470    token: String,
471) -> Result<CommonContext, ContextError> {
472    let kv = kv::initialize(kv_path).map_err(ContextError::Kv)?;
473    let context = CommonContext::new(kv);
474    context.kv.put_secret(HT_AUTH_KEY, token.as_bytes());
475    bootstrap_default_user(&context).map_err(ContextError::Auth)?;
476    Ok(context)
477}
478
479fn bootstrap_default_user(context: &CommonContext) -> Result<(), hightower_kv::Error> {
480    let username = std::env::var(DEFAULT_AUTH_USERNAME_ENV)
481        .unwrap_or_else(|_| DEFAULT_AUTH_USERNAME.to_string())
482        .trim()
483        .to_owned();
484
485    if username.is_empty() {
486        tracing::warn!("Skipping default auth bootstrap; username is empty");
487        return Ok(());
488    }
489
490    let password = std::env::var(DEFAULT_AUTH_PASSWORD_ENV)
491        .unwrap_or_else(|_| DEFAULT_AUTH_PASSWORD.to_string());
492
493    if password.trim().is_empty() {
494        tracing::warn!("Skipping default auth bootstrap; password is empty");
495        return Ok(());
496    }
497
498    match context.auth.create_user(&username, &password) {
499        Ok(_) => {
500            tracing::info!(username = %username, "Bootstrapped default auth user");
501            Ok(())
502        }
503        Err(hightower_kv::Error::Conflict(_)) => {
504            tracing::debug!(username = %username, "Default auth user already exists");
505            Ok(())
506        }
507        Err(err) => Err(err),
508    }
509}
510
511#[cfg(test)]
512pub mod fixtures {
513    use crate::context::{CommonContext, initialize_kv};
514    use tempfile::TempDir;
515
516    pub fn context() -> CommonContext {
517        let temp = TempDir::new().expect("tempdir");
518        let kv = initialize_kv(Some(temp.path())).expect("kv init");
519        CommonContext::new(kv)
520    }
521}
522
523pub mod env {
524    use std::sync::{Mutex, MutexGuard, OnceLock};
525
526    pub const DISABLE_GATEWAY_REGISTRATION_ENV: &str = "HT_DISABLE_ROOT_REGISTRATION";
527
528    fn registration_lock() -> &'static Mutex<()> {
529        static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
530        LOCK.get_or_init(|| Mutex::new(()))
531    }
532
533    pub struct RegistrationGuard {
534        lock: Option<MutexGuard<'static, ()>>,
535    }
536
537    impl RegistrationGuard {
538        fn new(lock: MutexGuard<'static, ()>) -> Self {
539            Self { lock: Some(lock) }
540        }
541    }
542
543    impl Drop for RegistrationGuard {
544        fn drop(&mut self) {
545            unsafe {
546                std::env::remove_var(DISABLE_GATEWAY_REGISTRATION_ENV);
547            }
548            drop(self.lock.take());
549        }
550    }
551
552    pub fn disable_gateway_registration() -> RegistrationGuard {
553        let guard = registration_lock().lock().expect("registration lock");
554        unsafe {
555            std::env::set_var(DISABLE_GATEWAY_REGISTRATION_ENV, "1");
556        }
557        RegistrationGuard::new(guard)
558    }
559
560    pub struct RegistrationEnableGuard {
561        lock: Option<MutexGuard<'static, ()>>,
562        previous: Option<String>,
563    }
564
565    impl RegistrationEnableGuard {
566        fn new(lock: MutexGuard<'static, ()>, previous: Option<String>) -> Self {
567            Self {
568                lock: Some(lock),
569                previous,
570            }
571        }
572    }
573
574    impl Drop for RegistrationEnableGuard {
575        fn drop(&mut self) {
576            unsafe {
577                if let Some(value) = self.previous.take() {
578                    std::env::set_var(DISABLE_GATEWAY_REGISTRATION_ENV, value);
579                } else {
580                    std::env::remove_var(DISABLE_GATEWAY_REGISTRATION_ENV);
581                }
582            }
583            drop(self.lock.take());
584        }
585    }
586
587    pub fn enable_gateway_registration() -> RegistrationEnableGuard {
588        let guard = registration_lock().lock().expect("registration lock");
589        let previous = std::env::var(DISABLE_GATEWAY_REGISTRATION_ENV).ok();
590        unsafe {
591            std::env::remove_var(DISABLE_GATEWAY_REGISTRATION_ENV);
592        }
593        RegistrationEnableGuard::new(guard, previous)
594    }
595}
596
597#[cfg(test)]
598mod tests {
599    use super::*;
600    use std::env::VarError;
601    use tempfile::TempDir;
602
603    #[test]
604    fn initialize_with_token_source_persists_token() {
605        let temp = TempDir::new().expect("tempdir");
606        let context =
607            initialize_with_token_source(Some(temp.path()), |_| Ok("test-auth".into()))
608                .expect("initialize");
609
610        let stored = context
611            .kv
612            .get_bytes(HT_AUTH_KEY)
613            .expect("kv read")
614            .expect("value present");
615        assert_eq!(stored, b"test-auth");
616    }
617
618    #[test]
619    fn initialize_with_token_source_reports_missing_token() {
620        let error = initialize_with_token_source(None, |_| Err(VarError::NotPresent))
621            .expect_err("missing token");
622        assert!(matches!(error, ContextError::Token(TokenError::Missing)));
623    }
624
625    #[test]
626    fn initialize_with_token_bootstraps_default_user() {
627        use std::sync::{Mutex, OnceLock};
628
629        fn env_lock() -> &'static Mutex<()> {
630            static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
631            LOCK.get_or_init(|| Mutex::new(()))
632        }
633
634        let _guard = env_lock().lock().expect("env lock");
635
636        let previous_user = std::env::var(DEFAULT_AUTH_USERNAME_ENV).ok();
637        let previous_password = std::env::var(DEFAULT_AUTH_PASSWORD_ENV).ok();
638        unsafe {
639            std::env::remove_var(DEFAULT_AUTH_USERNAME_ENV);
640            std::env::remove_var(DEFAULT_AUTH_PASSWORD_ENV);
641        }
642
643        let temp = TempDir::new().expect("tempdir");
644        let context = initialize_with_token(Some(temp.path()), "token".into())
645            .expect("context initialized");
646
647        assert!(
648            context
649                .auth
650                .verify_password(DEFAULT_AUTH_USERNAME, DEFAULT_AUTH_PASSWORD)
651                .expect("default password verification")
652        );
653
654        match previous_user {
655            Some(value) => unsafe { std::env::set_var(DEFAULT_AUTH_USERNAME_ENV, value) },
656            None => unsafe { std::env::remove_var(DEFAULT_AUTH_USERNAME_ENV) },
657        }
658
659        match previous_password {
660            Some(value) => unsafe { std::env::set_var(DEFAULT_AUTH_PASSWORD_ENV, value) },
661            None => unsafe { std::env::remove_var(DEFAULT_AUTH_PASSWORD_ENV) },
662        }
663    }
664
665    #[test]
666    fn context_creates_isolated_store() {
667        let ctx_a = fixtures::context();
668        let ctx_b = fixtures::context();
669
670        ctx_a
671            .kv
672            .put_bytes(b"test/key", b"value-a")
673            .expect("store a");
674
675        let stored = ctx_b.kv.get_bytes(b"test/key").expect("read b");
676        assert!(stored.is_none(), "unexpected shared state");
677    }
678
679    #[test]
680    fn disable_gateway_registration_sets_env() {
681        {
682            let _guard = env::disable_gateway_registration();
683            let value = std::env::var(env::DISABLE_GATEWAY_REGISTRATION_ENV).unwrap();
684            assert_eq!(value, "1");
685        }
686
687        assert!(std::env::var(env::DISABLE_GATEWAY_REGISTRATION_ENV).is_err());
688    }
689
690    #[test]
691    fn enable_gateway_registration_restores_previous_state() {
692        use std::sync::{Mutex, OnceLock};
693
694        fn test_env_lock() -> &'static Mutex<()> {
695            static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
696            LOCK.get_or_init(|| Mutex::new(()))
697        }
698
699        let _test_lock = test_env_lock().lock().expect("test env lock");
700
701        let previous = std::env::var(env::DISABLE_GATEWAY_REGISTRATION_ENV).ok();
702
703        unsafe {
704            std::env::set_var(env::DISABLE_GATEWAY_REGISTRATION_ENV, "1");
705        }
706
707        {
708            let _guard = env::enable_gateway_registration();
709            assert!(
710                std::env::var(env::DISABLE_GATEWAY_REGISTRATION_ENV).is_err(),
711                "env var should be cleared while guard is active"
712            );
713        }
714
715        let restored =
716            std::env::var(env::DISABLE_GATEWAY_REGISTRATION_ENV).expect("env var restored");
717        assert_eq!(restored, "1");
718
719        match previous {
720            Some(value) => unsafe { std::env::set_var(env::DISABLE_GATEWAY_REGISTRATION_ENV, value) },
721            None => unsafe { std::env::remove_var(env::DISABLE_GATEWAY_REGISTRATION_ENV) },
722        }
723    }
724}