arminius_lib_nats/
credentials.rs1mod tests;
7
8use std::{env, fmt};
9
10use arminius_lib_config::ArminiusConfigurationError;
11use nkeys::KeyPair;
12
13#[derive(Clone)]
15#[expect(
16 clippy::large_enum_variant,
17 reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
18)]
19pub enum NatsCredentials {
20 None,
22 Jwt(NatsJwtCredentials),
24 UserAndPassword {
26 user: String,
28 password: String,
30 },
31}
32
33impl NatsCredentials {
34 const AUTH_TYPE: &'static str = "NATS_AUTH_TYPE";
36 const AUTH_TOKEN: &'static str = "NATS_AUTH_TOKEN";
38 const AUTH_KEY: &'static str = "NATS_AUTH_KEY";
40
41 pub fn from_env() -> Result<Self, ArminiusConfigurationError> {
54 Self::parse_from_env(&env::vars())
55 }
56
57 fn parse_from_env(env: &impl Env) -> Result<Self, ArminiusConfigurationError> {
61 match Self::get_env_var(env, Self::AUTH_TYPE) {
62 Ok(value) if value.eq_ignore_ascii_case("JWT") => Self::parse_jwt_credentials(env),
63 Ok(other_value) => {
64 Err(ArminiusConfigurationError::malformed(Self::AUTH_TYPE, Some(other_value)))
65 }
66 Err(ArminiusConfigurationError::MissingConfigValue { .. }) => Ok(NatsCredentials::None),
67 Err(other) => Err(other),
68 }
69 }
70
71 fn parse_jwt_credentials(env: &impl Env) -> Result<Self, ArminiusConfigurationError> {
73 let token = Self::get_env_var(env, Self::AUTH_TOKEN)?;
74 let key = Self::get_env_var(env, Self::AUTH_KEY)?;
75 let signing_key = KeyPair::from_seed(&key).map_err(|err| {
76 ArminiusConfigurationError::malformed(
77 Self::AUTH_KEY,
78 format!("error decoding key: {err:?}"),
79 )
80 })?;
81 Ok(NatsCredentials::Jwt(NatsJwtCredentials { key: signing_key, token }))
82 }
83
84 fn get_env_var(
87 env: &impl Env,
88 var: &'static str,
89 ) -> Result<EnvVarValue, ArminiusConfigurationError> {
90 env.get(var).map_err(|err| match err {
91 env::VarError::NotPresent => ArminiusConfigurationError::missing(var),
92 env::VarError::NotUnicode(invalid_value) => ArminiusConfigurationError::malformed(
93 var,
94 Some(invalid_value.to_string_lossy().to_string()),
95 ),
96 })
97 }
98}
99
100impl From<NatsCredentials> for async_nats::ConnectOptions {
101 fn from(val: NatsCredentials) -> Self {
102 use async_nats::ConnectOptions;
103 match val {
104 NatsCredentials::None => ConnectOptions::default(),
105 NatsCredentials::Jwt(NatsJwtCredentials { key, token }) => {
106 ConnectOptions::with_jwt(token, move |nonce| {
107 tracing::trace!("signing server nonce: {nonce:?}");
108 let signing_key = key.clone();
109 async move {
110 signing_key.sign(&nonce).map_err(|err| {
111 async_nats::AuthError::new(format!(
112 "signing server nonce failed: {err:?}"
113 ))
114 })
115 }
116 })
117 }
118 NatsCredentials::UserAndPassword { user, password } => {
119 ConnectOptions::default().user_and_password(user, password)
120 }
121 }
122 }
123}
124
125impl fmt::Debug for NatsCredentials {
126 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127 let kind = match self {
128 NatsCredentials::None => "None",
129 NatsCredentials::Jwt { .. } => "Jwt",
130 NatsCredentials::UserAndPassword { .. } => "UserAndPassword",
131 };
132 write!(f, "NatsCredentials::{kind}")
133 }
134}
135
136#[derive(Debug, Clone)]
142pub struct NatsJwtCredentials {
143 pub key: KeyPair,
145 pub token: String,
147}
148
149impl From<NatsJwtCredentials> for NatsCredentials {
150 fn from(value: NatsJwtCredentials) -> Self {
151 NatsCredentials::Jwt(value)
152 }
153}
154
155trait Env {
161 fn get(&self, name: &'static str) -> Result<EnvVarValue, env::VarError>;
164}
165
166impl Env for env::Vars {
167 fn get(&self, name: &'static str) -> Result<String, env::VarError> {
168 env::var(name)
169 }
170}
171
172type EnvVarValue = String;