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 HT_AUTH_KEY: &[u8] = b"secrets/ht_auth_key";
303pub const GATEWAY_URL_KEY: &[u8] = b"gateway/url";
304const GATEWAY_URL_ENV: &str = "HT_GATEWAY_URL";
305const DEFAULT_AUTH_USERNAME_ENV: &str = "HT_DEFAULT_USER";
306const DEFAULT_AUTH_PASSWORD_ENV: &str = "HT_DEFAULT_PASSWORD";
307const DEFAULT_AUTH_USERNAME: &str = "admin";
308const DEFAULT_AUTH_PASSWORD: &str = "admin";
309
310#[derive(Clone)]
311pub struct CommonContext {
312 pub kv: NamespacedKv,
313 pub auth: Arc<GatewayAuthService>,
314}
315
316impl CommonContext {
317 pub fn new(kv: KvHandle) -> Self {
318 let kv = NamespacedKv::from_handle(kv);
319 let auth = kv.auth_service();
320 Self { kv, auth }
321 }
322
323 pub fn namespaced(&self, prefix: &[u8]) -> Self {
324 Self {
325 kv: self.kv.clone_with_additional_prefix(prefix),
326 auth: Arc::clone(&self.auth),
327 }
328 }
329}
330
331impl fmt::Debug for CommonContext {
332 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
333 f.debug_struct("CommonContext")
334 .field("kv", &self.kv)
335 .finish()
336 }
337}
338
339#[derive(Clone, Debug)]
340pub struct NamespacedKv {
341 inner: Arc<KvHandle>,
342 prefix: Option<Vec<u8>>,
343}
344
345impl NamespacedKv {
346 pub fn from_handle(kv: KvHandle) -> Self {
347 Self {
348 inner: Arc::new(kv),
349 prefix: None,
350 }
351 }
352
353 fn with_parts(inner: Arc<KvHandle>, prefix: Option<Vec<u8>>) -> Self {
354 Self { inner, prefix }
355 }
356
357 pub fn clone_with_additional_prefix(&self, additional: &[u8]) -> Self {
358 let prefix = match &self.prefix {
359 Some(existing) => {
360 let mut composed = existing.clone();
361 if !existing.is_empty() {
362 composed.push(b'/');
363 }
364 composed.extend_from_slice(additional);
365 composed
366 }
367 None => additional.to_vec(),
368 };
369
370 Self::with_parts(Arc::clone(&self.inner), Some(prefix))
371 }
372
373 pub fn put_bytes(&self, key: &[u8], value: &[u8]) -> Result<(), hightower_kv::Error> {
374 let key = self.prefixed_key(key);
375 self.inner.put_bytes(key.as_ref(), value)
376 }
377
378 pub fn put_secret(&self, key: &[u8], value: &[u8]) {
379 let key = self.prefixed_key(key);
380 self.inner.put_secret(key.as_ref(), value);
381 }
382
383 pub fn get_bytes(&self, key: &[u8]) -> Result<Option<Vec<u8>>, hightower_kv::Error> {
384 let key = self.prefixed_key(key);
385 self.inner.get_bytes(key.as_ref())
386 }
387
388 pub fn auth_service(&self) -> Arc<GatewayAuthService> {
389 self.inner.auth()
390 }
391
392 pub fn list_by_prefix(
393 &self,
394 prefix: &[u8],
395 ) -> Result<Vec<(Vec<u8>, Vec<u8>)>, hightower_kv::Error> {
396 let full_prefix = match self.prefixed_key(prefix) {
397 Cow::Borrowed(bytes) => bytes.to_vec(),
398 Cow::Owned(bytes) => bytes,
399 };
400
401 let entries = self.inner.get_prefix(&full_prefix)?;
402
403 let mut results = Vec::new();
404 for (key, value) in entries {
405 let mut remainder = &key[full_prefix.len()..];
406 if remainder.first() == Some(&b'/') {
407 remainder = &remainder[1..];
408 }
409 results.push((remainder.to_vec(), value));
410 }
411
412 Ok(results)
413 }
414
415 fn prefixed_key<'a>(&self, key: &'a [u8]) -> Cow<'a, [u8]> {
416 match &self.prefix {
417 Some(prefix) if !prefix.is_empty() => {
418 let mut composed = Vec::with_capacity(prefix.len() + 1 + key.len());
419 composed.extend_from_slice(prefix);
420 composed.push(b'/');
421 composed.extend_from_slice(key);
422 Cow::Owned(composed)
423 }
424 _ => Cow::Borrowed(key),
425 }
426 }
427}
428
429#[derive(Debug)]
430pub enum ContextError {
431 Token(TokenError),
432 Kv(KvInitError),
433 Auth(hightower_kv::Error),
434}
435
436impl fmt::Display for ContextError {
437 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
438 match self {
439 ContextError::Token(err) => write!(f, "failed to read HT_AUTH_KEY: {}", err),
440 ContextError::Kv(err) => write!(f, "failed to initialize key-value store: {}", err),
441 ContextError::Auth(err) => write!(f, "failed to bootstrap auth service: {}", err),
442 }
443 }
444}
445
446impl Error for ContextError {
447 fn source(&self) -> Option<&(dyn Error + 'static)> {
448 match self {
449 ContextError::Token(err) => Some(err),
450 ContextError::Kv(err) => Some(err),
451 ContextError::Auth(err) => Some(err),
452 }
453 }
454}
455
456pub fn initialize_with_token_source<F>(
457 kv_path: Option<&Path>,
458 mut lookup: F,
459) -> Result<CommonContext, ContextError>
460where
461 F: FnMut(&str) -> Result<String, VarError>,
462{
463 let token = token::fetch(|key| lookup(key)).map_err(ContextError::Token)?;
464 initialize_with_token(kv_path, token)
465}
466
467pub fn initialize_with_token(
468 kv_path: Option<&Path>,
469 token: String,
470) -> Result<CommonContext, ContextError> {
471 let kv = kv::initialize(kv_path).map_err(ContextError::Kv)?;
472 let context = CommonContext::new(kv);
473 context.kv.put_secret(HT_AUTH_KEY, token.as_bytes());
474
475 if let Ok(gateway_url) = std::env::var(GATEWAY_URL_ENV) {
476 context.kv.put_secret(GATEWAY_URL_KEY, gateway_url.as_bytes());
477 }
478
479 bootstrap_default_user(&context).map_err(ContextError::Auth)?;
480 Ok(context)
481}
482
483fn bootstrap_default_user(context: &CommonContext) -> Result<(), hightower_kv::Error> {
484 let username = std::env::var(DEFAULT_AUTH_USERNAME_ENV)
485 .unwrap_or_else(|_| DEFAULT_AUTH_USERNAME.to_string())
486 .trim()
487 .to_owned();
488
489 if username.is_empty() {
490 tracing::warn!("Skipping default auth bootstrap; username is empty");
491 return Ok(());
492 }
493
494 let password = std::env::var(DEFAULT_AUTH_PASSWORD_ENV)
495 .unwrap_or_else(|_| DEFAULT_AUTH_PASSWORD.to_string());
496
497 if password.trim().is_empty() {
498 tracing::warn!("Skipping default auth bootstrap; password is empty");
499 return Ok(());
500 }
501
502 match context.auth.create_user(&username, &password) {
503 Ok(_) => {
504 tracing::info!(username = %username, "Bootstrapped default auth user");
505 Ok(())
506 }
507 Err(hightower_kv::Error::Conflict(_)) => {
508 tracing::debug!(username = %username, "Default auth user already exists");
509 Ok(())
510 }
511 Err(err) => Err(err),
512 }
513}
514
515#[cfg(test)]
516pub mod fixtures {
517 use crate::context::{CommonContext, initialize_kv};
518 use tempfile::TempDir;
519
520 pub fn context() -> CommonContext {
521 let temp = TempDir::new().expect("tempdir");
522 let kv = initialize_kv(Some(temp.path())).expect("kv init");
523 CommonContext::new(kv)
524 }
525}
526
527pub mod env {
528 use std::sync::{Mutex, MutexGuard, OnceLock};
529
530 pub const DISABLE_GATEWAY_REGISTRATION_ENV: &str = "HT_DISABLE_ROOT_REGISTRATION";
531
532 fn registration_lock() -> &'static Mutex<()> {
533 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
534 LOCK.get_or_init(|| Mutex::new(()))
535 }
536
537 pub struct RegistrationGuard {
538 lock: Option<MutexGuard<'static, ()>>,
539 }
540
541 impl RegistrationGuard {
542 fn new(lock: MutexGuard<'static, ()>) -> Self {
543 Self { lock: Some(lock) }
544 }
545 }
546
547 impl Drop for RegistrationGuard {
548 fn drop(&mut self) {
549 unsafe {
550 std::env::remove_var(DISABLE_GATEWAY_REGISTRATION_ENV);
551 }
552 drop(self.lock.take());
553 }
554 }
555
556 pub fn disable_gateway_registration() -> RegistrationGuard {
557 let guard = registration_lock().lock().expect("registration lock");
558 unsafe {
559 std::env::set_var(DISABLE_GATEWAY_REGISTRATION_ENV, "1");
560 }
561 RegistrationGuard::new(guard)
562 }
563
564 pub struct RegistrationEnableGuard {
565 lock: Option<MutexGuard<'static, ()>>,
566 previous: Option<String>,
567 }
568
569 impl RegistrationEnableGuard {
570 fn new(lock: MutexGuard<'static, ()>, previous: Option<String>) -> Self {
571 Self {
572 lock: Some(lock),
573 previous,
574 }
575 }
576 }
577
578 impl Drop for RegistrationEnableGuard {
579 fn drop(&mut self) {
580 unsafe {
581 if let Some(value) = self.previous.take() {
582 std::env::set_var(DISABLE_GATEWAY_REGISTRATION_ENV, value);
583 } else {
584 std::env::remove_var(DISABLE_GATEWAY_REGISTRATION_ENV);
585 }
586 }
587 drop(self.lock.take());
588 }
589 }
590
591 pub fn enable_gateway_registration() -> RegistrationEnableGuard {
592 let guard = registration_lock().lock().expect("registration lock");
593 let previous = std::env::var(DISABLE_GATEWAY_REGISTRATION_ENV).ok();
594 unsafe {
595 std::env::remove_var(DISABLE_GATEWAY_REGISTRATION_ENV);
596 }
597 RegistrationEnableGuard::new(guard, previous)
598 }
599}
600
601#[cfg(test)]
602mod tests {
603 use super::*;
604 use std::env::VarError;
605 use tempfile::TempDir;
606
607 #[test]
608 fn initialize_with_token_source_persists_token() {
609 let temp = TempDir::new().expect("tempdir");
610 let context =
611 initialize_with_token_source(Some(temp.path()), |_| Ok("test-auth".into()))
612 .expect("initialize");
613
614 let stored = context
615 .kv
616 .get_bytes(HT_AUTH_KEY)
617 .expect("kv read")
618 .expect("value present");
619 assert_eq!(stored, b"test-auth");
620 }
621
622 #[test]
623 fn initialize_with_token_source_reports_missing_token() {
624 let error = initialize_with_token_source(None, |_| Err(VarError::NotPresent))
625 .expect_err("missing token");
626 assert!(matches!(error, ContextError::Token(TokenError::Missing)));
627 }
628
629 #[test]
630 fn initialize_with_token_bootstraps_default_user() {
631 use std::sync::{Mutex, OnceLock};
632
633 fn env_lock() -> &'static Mutex<()> {
634 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
635 LOCK.get_or_init(|| Mutex::new(()))
636 }
637
638 let _guard = env_lock().lock().expect("env lock");
639
640 let previous_user = std::env::var(DEFAULT_AUTH_USERNAME_ENV).ok();
641 let previous_password = std::env::var(DEFAULT_AUTH_PASSWORD_ENV).ok();
642 unsafe {
643 std::env::remove_var(DEFAULT_AUTH_USERNAME_ENV);
644 std::env::remove_var(DEFAULT_AUTH_PASSWORD_ENV);
645 }
646
647 let temp = TempDir::new().expect("tempdir");
648 let context = initialize_with_token(Some(temp.path()), "token".into())
649 .expect("context initialized");
650
651 assert!(
652 context
653 .auth
654 .verify_password(DEFAULT_AUTH_USERNAME, DEFAULT_AUTH_PASSWORD)
655 .expect("default password verification")
656 );
657
658 match previous_user {
659 Some(value) => unsafe { std::env::set_var(DEFAULT_AUTH_USERNAME_ENV, value) },
660 None => unsafe { std::env::remove_var(DEFAULT_AUTH_USERNAME_ENV) },
661 }
662
663 match previous_password {
664 Some(value) => unsafe { std::env::set_var(DEFAULT_AUTH_PASSWORD_ENV, value) },
665 None => unsafe { std::env::remove_var(DEFAULT_AUTH_PASSWORD_ENV) },
666 }
667 }
668
669 #[test]
670 fn context_creates_isolated_store() {
671 let ctx_a = fixtures::context();
672 let ctx_b = fixtures::context();
673
674 ctx_a
675 .kv
676 .put_bytes(b"test/key", b"value-a")
677 .expect("store a");
678
679 let stored = ctx_b.kv.get_bytes(b"test/key").expect("read b");
680 assert!(stored.is_none(), "unexpected shared state");
681 }
682
683 #[test]
684 fn disable_gateway_registration_sets_env() {
685 {
686 let _guard = env::disable_gateway_registration();
687 let value = std::env::var(env::DISABLE_GATEWAY_REGISTRATION_ENV).unwrap();
688 assert_eq!(value, "1");
689 }
690
691 assert!(std::env::var(env::DISABLE_GATEWAY_REGISTRATION_ENV).is_err());
692 }
693
694 #[test]
695 fn enable_gateway_registration_restores_previous_state() {
696 use std::sync::{Mutex, OnceLock};
697
698 fn test_env_lock() -> &'static Mutex<()> {
699 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
700 LOCK.get_or_init(|| Mutex::new(()))
701 }
702
703 let _test_lock = test_env_lock().lock().expect("test env lock");
704
705 let previous = std::env::var(env::DISABLE_GATEWAY_REGISTRATION_ENV).ok();
706
707 unsafe {
708 std::env::set_var(env::DISABLE_GATEWAY_REGISTRATION_ENV, "1");
709 }
710
711 {
712 let _guard = env::enable_gateway_registration();
713 assert!(
714 std::env::var(env::DISABLE_GATEWAY_REGISTRATION_ENV).is_err(),
715 "env var should be cleared while guard is active"
716 );
717 }
718
719 let restored =
720 std::env::var(env::DISABLE_GATEWAY_REGISTRATION_ENV).expect("env var restored");
721 assert_eq!(restored, "1");
722
723 match previous {
724 Some(value) => unsafe { std::env::set_var(env::DISABLE_GATEWAY_REGISTRATION_ENV, value) },
725 None => unsafe { std::env::remove_var(env::DISABLE_GATEWAY_REGISTRATION_ENV) },
726 }
727 }
728}