1#![allow(missing_docs)]
3
4use nkeys::error::Error as NKeysError;
5use snafu::{ResultExt, Snafu};
6use vector_lib::{configurable::configurable_component, sensitive_string::SensitiveString};
7
8use crate::tls::TlsEnableableConfig;
9
10#[derive(Debug, Snafu)]
12pub enum NatsConfigError {
13 #[snafu(display("NATS Auth Config Error: {}", source))]
14 AuthConfigError { source: NKeysError },
15 #[snafu(display("NATS TLS Config Error: missing key"))]
16 TlsMissingKey,
17 #[snafu(display("NATS TLS Config Error: missing cert"))]
18 TlsMissingCert,
19 #[snafu(display("NATS Credentials file error"))]
20 CredentialsFileError { source: std::io::Error },
21}
22
23#[configurable_component]
25#[derive(Clone, Debug)]
26#[serde(rename_all = "snake_case", tag = "strategy")]
27#[configurable(metadata(
28 docs::enum_tag_description = "The strategy used to authenticate with the NATS server.
29
30More information on NATS authentication, and the various authentication strategies, can be found in the
31NATS [documentation][nats_auth_docs]. For TLS client certificate authentication specifically, see the
32`tls` settings.
33
34[nats_auth_docs]: https://docs.nats.io/running-a-nats-service/configuration/securing_nats/auth_intro"
35))]
36pub enum NatsAuthConfig {
37 UserPassword {
39 #[configurable(derived)]
40 user_password: NatsAuthUserPassword,
41 },
42
43 Token {
45 #[configurable(derived)]
46 token: NatsAuthToken,
47 },
48
49 CredentialsFile {
51 #[configurable(derived)]
52 credentials_file: NatsAuthCredentialsFile,
53 },
54
55 Nkey {
57 #[configurable(derived)]
58 nkey: NatsAuthNKey,
59 },
60}
61
62impl std::fmt::Display for NatsAuthConfig {
63 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
64 use NatsAuthConfig::*;
65 let word = match self {
66 UserPassword { .. } => "user_password",
67 Token { .. } => "token",
68 CredentialsFile { .. } => "credentials_file",
69 Nkey { .. } => "nkey",
70 };
71 write!(f, "{word}")
72 }
73}
74
75#[configurable_component]
77#[derive(Clone, Debug)]
78#[serde(deny_unknown_fields)]
79pub struct NatsAuthUserPassword {
80 pub(crate) user: String,
82
83 pub(crate) password: SensitiveString,
85}
86
87#[configurable_component]
89#[derive(Clone, Debug)]
90#[serde(deny_unknown_fields)]
91pub struct NatsAuthToken {
92 pub(crate) value: SensitiveString,
94}
95
96#[configurable_component]
98#[derive(Clone, Debug)]
99#[serde(deny_unknown_fields)]
100pub struct NatsAuthCredentialsFile {
101 #[configurable(metadata(docs::examples = "/etc/nats/nats.creds"))]
103 pub(crate) path: String,
104}
105
106#[configurable_component]
108#[derive(Clone, Debug)]
109#[serde(deny_unknown_fields)]
110pub struct NatsAuthNKey {
111 pub(crate) nkey: String,
115
116 pub(crate) seed: String,
120}
121
122impl NatsAuthConfig {
123 pub(crate) fn to_nats_options(&self) -> Result<async_nats::ConnectOptions, NatsConfigError> {
124 match self {
125 NatsAuthConfig::UserPassword { user_password } => {
126 Ok(async_nats::ConnectOptions::with_user_and_password(
127 user_password.user.clone(),
128 user_password.password.inner().to_string(),
129 ))
130 }
131 NatsAuthConfig::CredentialsFile { credentials_file } => {
132 async_nats::ConnectOptions::with_credentials(
133 &std::fs::read_to_string(credentials_file.path.clone())
134 .context(CredentialsFileSnafu)?,
135 )
136 .context(CredentialsFileSnafu)
137 }
138 NatsAuthConfig::Nkey { nkey } => {
139 Ok(async_nats::ConnectOptions::with_nkey(nkey.seed.clone()))
140 }
141 NatsAuthConfig::Token { token } => Ok(async_nats::ConnectOptions::with_token(
142 token.value.inner().to_string(),
143 )),
144 }
145 }
146}
147
148pub(crate) fn validate_tls_cert_key_pair(
152 tls_config: &TlsEnableableConfig,
153) -> Result<(), NatsConfigError> {
154 if !tls_config.enabled.unwrap_or(false) {
155 return Ok(());
156 }
157 match (&tls_config.options.crt_file, &tls_config.options.key_file) {
158 (Some(_), None) => Err(NatsConfigError::TlsMissingKey),
159 (None, Some(_)) => Err(NatsConfigError::TlsMissingCert),
160 _ => Ok(()),
161 }
162}
163
164pub(crate) fn from_tls_auth_config(
165 connection_name: &str,
166 auth_config: &Option<NatsAuthConfig>,
167 tls_config: &Option<TlsEnableableConfig>,
168) -> Result<async_nats::ConnectOptions, NatsConfigError> {
169 let nats_options = match &auth_config {
170 None => async_nats::ConnectOptions::new(),
171 Some(auth) => auth.to_nats_options()?,
172 };
173
174 let nats_options = nats_options.name(connection_name);
175
176 match tls_config {
177 None => Ok(nats_options),
178 Some(tls_config) => {
179 let tls_enabled = tls_config.enabled.unwrap_or(false);
180 let nats_options = nats_options.require_tls(tls_enabled);
181 if !tls_enabled {
182 return Ok(nats_options);
183 }
184
185 validate_tls_cert_key_pair(tls_config)?;
186
187 let nats_options = match &tls_config.options.ca_file {
188 None => nats_options,
189 Some(ca_file) => nats_options.add_root_certificates(ca_file.clone()),
190 };
191
192 let nats_options = match (&tls_config.options.crt_file, &tls_config.options.key_file) {
193 (None, None) => nats_options,
194 (Some(crt_file), Some(key_file)) => {
195 nats_options.add_client_certificate(crt_file.clone(), key_file.clone())
196 }
197 (Some(_), None) | (None, Some(_)) => {
198 unreachable!("cert/key pairing validated by validate_tls_cert_key_pair")
199 }
200 };
201 Ok(nats_options)
202 }
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use indoc::indoc;
209
210 use super::*;
211
212 fn parse_auth(s: &str) -> Result<async_nats::ConnectOptions, crate::Error> {
213 serde_yaml::from_str(s)
214 .map_err(Into::into)
215 .and_then(|config: NatsAuthConfig| config.to_nats_options().map_err(Into::into))
216 }
217
218 #[test]
219 fn auth_user_password_ok() {
220 parse_auth(indoc! {r#"
221 strategy: user_password
222 user_password:
223 user: username
224 password: password
225 "#})
226 .unwrap();
227 }
228
229 #[test]
230 fn auth_user_password_missing_user() {
231 parse_auth(indoc! {r#"
232 strategy: user_password
233 user_password:
234 password: password
235 "#})
236 .unwrap_err();
237 }
238
239 #[test]
240 fn auth_user_password_missing_password() {
241 parse_auth(indoc! {r#"
242 strategy: user_password
243 user_password:
244 user: username
245 "#})
246 .unwrap_err();
247 }
248
249 #[test]
250 fn auth_user_password_missing_all() {
251 parse_auth(indoc! {r#"
252 strategy: user_password
253 token:
254 value: foobar
255 "#})
256 .unwrap_err();
257 }
258
259 #[test]
260 fn auth_token_ok() {
261 parse_auth(indoc! {r#"
262 strategy: token
263 token:
264 value: token
265 "#})
266 .unwrap();
267 }
268
269 #[test]
270 fn auth_token_missing() {
271 parse_auth(indoc! {r#"
272 strategy: token
273 user_password:
274 user: foobar
275 "#})
276 .unwrap_err();
277 }
278
279 #[test]
280 fn auth_credentials_file_ok() {
281 parse_auth(indoc! {r#"
282 strategy: credentials_file
283 credentials_file:
284 path: tests/integration/nats/data/nats.creds
285 "#})
286 .unwrap();
287 }
288
289 #[test]
290 fn auth_credentials_file_missing() {
291 parse_auth(indoc! {r#"
292 strategy: credentials_file
293 token:
294 value: foobar
295 "#})
296 .unwrap_err();
297 }
298
299 #[test]
300 fn auth_nkey_ok() {
301 parse_auth(indoc! {r#"
302 strategy: nkey
303 nkey:
304 nkey: UC435ZYS52HF72E2VMQF4GO6CUJOCHDUUPEBU7XDXW5AQLIC6JZ46PO5
305 seed: SUAAEZYNLTEA2MDTG7L5X7QODZXYHPOI2LT2KH5I4GD6YVP24SE766EGPA
306 "#})
307 .unwrap();
308 }
309
310 #[test]
311 fn auth_nkey_missing_nkey() {
312 parse_auth(indoc! {r#"
313 strategy: nkey
314 nkey:
315 seed: SUAAEZYNLTEA2MDTG7L5X7QODZXYHPOI2LT2KH5I4GD6YVP24SE766EGPA
316 "#})
317 .unwrap_err();
318 }
319
320 #[test]
321 fn auth_nkey_missing_seed() {
322 parse_auth(indoc! {r#"
323 strategy: nkey
324 nkey:
325 nkey: UC435ZYS52HF72E2VMQF4GO6CUJOCHDUUPEBU7XDXW5AQLIC6JZ46PO5
326 "#})
327 .unwrap_err();
328 }
329
330 #[test]
331 fn auth_nkey_missing_both() {
332 parse_auth(indoc! {r#"
333 strategy: nkey
334 user_password:
335 user: foobar
336 "#})
337 .unwrap_err();
338 }
339}