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}