feldera_types/
secret_ref.rs

1use regex::Regex;
2use std::fmt;
3use std::fmt::{Display, Formatter};
4use thiserror::Error as ThisError;
5
6/// RFC 1123 specification for a DNS label, which is also used by Kubernetes.
7pub const PATTERN_RFC_1123_DNS_LABEL: &str = r"^[a-z0-9]+(-[a-z0-9]+)*$";
8
9#[derive(Debug, Clone, PartialEq, Eq, ThisError)]
10pub enum KubernetesSecretNameParseError {
11    #[error("cannot be empty")]
12    Empty,
13    #[error("length ({name_len}) exceeds 63 characters")]
14    TooLong { name_len: usize },
15    #[error("must only contain lowercase alphanumeric characters or hyphens (-), and start and end with a lowercase alphanumeric character")]
16    InvalidFormat,
17}
18
19/// Validates it is a valid Kubernetes `Secret` name (follows RFC 1123 DNS label).
20pub fn validate_kubernetes_secret_name(name: &str) -> Result<(), KubernetesSecretNameParseError> {
21    if name.is_empty() {
22        Err(KubernetesSecretNameParseError::Empty)
23    } else if name.len() > 63 {
24        Err(KubernetesSecretNameParseError::TooLong {
25            name_len: name.len(),
26        })
27    } else {
28        let re = Regex::new(PATTERN_RFC_1123_DNS_LABEL).expect("valid regular expression");
29        if re.is_match(name) {
30            Ok(())
31        } else {
32            Err(KubernetesSecretNameParseError::InvalidFormat)
33        }
34    }
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, ThisError)]
38pub enum KubernetesSecretDataKeyParseError {
39    #[error("cannot be empty")]
40    Empty,
41    #[error("length ({data_key_len}) exceeds 255 characters")]
42    TooLong { data_key_len: usize },
43    #[error("must only contain lowercase alphanumeric characters and hyphens (-), and start and end with a lowercase alphanumeric character")]
44    InvalidFormat,
45}
46
47/// Validates it is a valid Kubernetes Secret data key.
48///
49/// YAML itself imposes little restrictions on a quoted string key except
50/// that it should not exceed 1024 Unicode characters.
51///
52/// However, it might be mounted as a file, as such characters such as `/`
53/// are not desirable. For now, we mirror the convention of the secret name,
54/// in which it restricts to lowercase alphanumeric characters and hyphens
55/// (and start/end with lower alphanumeric). In the future, this requirement
56/// might be loosened. The length is limited to 255 characters (which is
57/// stricter than 1024 Unicode characters, but looser than 63 of a DNS label),
58/// as the filename length limit for ext4 is 255.
59pub fn validate_kubernetes_secret_data_key(
60    data_key: &str,
61) -> Result<(), KubernetesSecretDataKeyParseError> {
62    if data_key.is_empty() {
63        Err(KubernetesSecretDataKeyParseError::Empty)
64    } else if data_key.len() > 255 {
65        Err(KubernetesSecretDataKeyParseError::TooLong {
66            data_key_len: data_key.len(),
67        })
68    } else {
69        let re = Regex::new(PATTERN_RFC_1123_DNS_LABEL).expect("valid regular expression");
70        if re.is_match(data_key) {
71            Ok(())
72        } else {
73            Err(KubernetesSecretDataKeyParseError::InvalidFormat)
74        }
75    }
76}
77
78/// Enumeration of possible secret references.
79/// Each variant corresponds to a secret provider.
80#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
81pub enum SecretRef {
82    /// Reference to a data key in a specific Kubernetes `Secret`.
83    Kubernetes {
84        /// Name of the Kubernetes `Secret` object.
85        name: String,
86        /// Key inside the `data:` section of the `Secret` object.
87        data_key: String,
88    },
89}
90
91impl Display for SecretRef {
92    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
93        match self {
94            SecretRef::Kubernetes { name, data_key } => {
95                write!(f, "${{secret:kubernetes:{name}/{data_key}}}")
96            }
97        }
98    }
99}
100
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub enum MaybeSecretRef {
103    String(String),
104    SecretRef(SecretRef),
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, ThisError)]
108pub enum MaybeSecretRefParseError {
109    #[error("secret reference '{secret_ref_str}' does not specify a valid provider (for example: 'kubernetes:')")]
110    InvalidProvider { secret_ref_str: String },
111    #[error("Kubernetes secret reference '{secret_ref_str}' is not valid: does not follow format `<name>/<data key>`")]
112    InvalidKubernetesSecretFormat { secret_ref_str: String },
113    #[error(
114        "Kubernetes secret reference '{secret_ref_str}' has name '{name}' which is not valid: {e}"
115    )]
116    InvalidKubernetesSecretName {
117        secret_ref_str: String,
118        name: String,
119        e: KubernetesSecretNameParseError,
120    },
121    #[error(
122        "Kubernetes secret reference '{secret_ref_str}' has data key '{data_key}' which is not valid: {e}"
123    )]
124    InvalidKubernetesSecretDataKey {
125        secret_ref_str: String,
126        data_key: String,
127        e: KubernetesSecretDataKeyParseError,
128    },
129}
130
131impl MaybeSecretRef {
132    /// Determines whether a string is just a plain string or a reference to a secret.
133    ///
134    /// - Secret reference: any string which starts with `${secret:` and ends with `}`
135    ///   is regarded as an attempt to declare a secret reference
136    /// - Plain string: any other string
137    ///
138    /// A secret reference must follow the following pattern:
139    /// `${secret:<provider>:<identifier>}`
140    ///
141    /// An error is returned if a string is regarded as a secret reference (see above), but:
142    /// - Specifies a `<provider>` which does not exist
143    /// - Specifies a `<identifier>` which does not meet the provider-specific requirements
144    ///
145    /// Supported providers and their identifier expectations:
146    /// - `${secret:kubernetes:<name>/<data key>}`
147    ///
148    /// Note that here is not checked whether the secret reference can actually be resolved.
149    pub fn new(value: String) -> Result<MaybeSecretRef, MaybeSecretRefParseError> {
150        if value.starts_with("${secret:") && value.ends_with('}') {
151            // Because the pattern only has ASCII characters, they are encoded as single bytes.
152            // The secret reference is extracted by slicing away the first 9 bytes and the last byte.
153            let from_idx_incl = 9;
154            let till_idx_excl = value.len() - 1;
155            let content = value[from_idx_incl..till_idx_excl].to_string();
156            if let Some(kubernetes_content) = content.strip_prefix("kubernetes:") {
157                if let Some((name, data_key)) = kubernetes_content.split_once("/") {
158                    if let Err(e) = validate_kubernetes_secret_name(name) {
159                        Err(MaybeSecretRefParseError::InvalidKubernetesSecretName {
160                            secret_ref_str: value,
161                            name: name.to_string(),
162                            e,
163                        })
164                    } else if let Err(e) = validate_kubernetes_secret_data_key(data_key) {
165                        Err(MaybeSecretRefParseError::InvalidKubernetesSecretDataKey {
166                            secret_ref_str: value,
167                            data_key: data_key.to_string(),
168                            e,
169                        })
170                    } else {
171                        Ok(MaybeSecretRef::SecretRef(SecretRef::Kubernetes {
172                            name: name.to_string(),
173                            data_key: data_key.to_string(),
174                        }))
175                    }
176                } else {
177                    Err(MaybeSecretRefParseError::InvalidKubernetesSecretFormat {
178                        secret_ref_str: value,
179                    })
180                }
181            } else {
182                Err(MaybeSecretRefParseError::InvalidProvider {
183                    secret_ref_str: value,
184                })
185            }
186        } else {
187            Ok(MaybeSecretRef::String(value))
188        }
189    }
190}
191
192impl Display for MaybeSecretRef {
193    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
194        match self {
195            MaybeSecretRef::String(plain_str) => {
196                write!(f, "{plain_str}")
197            }
198            MaybeSecretRef::SecretRef(secret_ref) => {
199                write!(f, "{secret_ref}")
200            }
201        }
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::{
208        validate_kubernetes_secret_data_key, validate_kubernetes_secret_name,
209        KubernetesSecretDataKeyParseError, KubernetesSecretNameParseError, MaybeSecretRef,
210    };
211    use super::{MaybeSecretRefParseError, SecretRef};
212
213    #[test]
214    #[rustfmt::skip] // Skip formatting to keep it short
215    fn secret_ref_format() {
216        assert_eq!(
217            format!("{}", SecretRef::Kubernetes {
218                name: "example".to_string(),
219                data_key: "value".to_string(),
220            }),
221            "${secret:kubernetes:example/value}"
222        );
223    }
224
225    #[test]
226    #[rustfmt::skip] // Skip formatting to keep it short
227    fn maybe_secret_ref_format() {
228        assert_eq!(
229            format!("{}", MaybeSecretRef::String("example".to_string())),
230            "example"
231        );
232        assert_eq!(
233            format!("{}", MaybeSecretRef::SecretRef(SecretRef::Kubernetes {
234                name: "example".to_string(),
235                data_key: "value".to_string(),
236            })),
237            "${secret:kubernetes:example/value}"
238        );
239    }
240
241    /// Checks that the parsing of the `MaybeSecretRef` yields the correct result,
242    /// and that the original string is preserved when again formatted.
243    fn test_values_and_expectations(
244        values_and_expectations: Vec<(&str, Result<MaybeSecretRef, MaybeSecretRefParseError>)>,
245    ) {
246        for (value, expectation) in values_and_expectations {
247            let outcome = MaybeSecretRef::new(value.to_string());
248            assert_eq!(outcome, expectation);
249            if let Ok(maybe_secret_ref) = outcome {
250                assert_eq!(maybe_secret_ref.to_string(), value);
251            }
252        }
253    }
254
255    #[test]
256    #[rustfmt::skip] // Skip formatting to keep it short
257    fn maybe_secret_ref_parse_string() {
258        let values_and_expectations = vec![
259            ("", Ok(MaybeSecretRef::String("".to_string()))),
260            ("a", Ok(MaybeSecretRef::String("a".to_string()))),
261            ("1", Ok(MaybeSecretRef::String("1".to_string()))),
262            ("example", Ok(MaybeSecretRef::String("example".to_string()))),
263            ("EXAMPLE", Ok(MaybeSecretRef::String("EXAMPLE".to_string()))),
264            ("123", Ok(MaybeSecretRef::String("123".to_string()))),
265            ("/path/to/file.txt", Ok(MaybeSecretRef::String("/path/to/file.txt".to_string()))),
266            ("$abc", Ok(MaybeSecretRef::String("$abc".to_string()))),
267            ("${secret", Ok(MaybeSecretRef::String("${secret".to_string()))),
268            ("}", Ok(MaybeSecretRef::String("}".to_string()))),
269            ("${secre:}", Ok(MaybeSecretRef::String("${secre:}".to_string()))),
270            // Unicode "Slightly Smiling Face": U+1F642
271            ("\u{1F642}", Ok(MaybeSecretRef::String("\u{1F642}".to_string()))),
272        ];
273        test_values_and_expectations(values_and_expectations);
274    }
275
276    #[test]
277    #[rustfmt::skip] // Skip formatting to keep it short
278    fn maybe_secret_ref_parse_secret_ref_kubernetes() {
279        let secret_ref_name_len_63 = format!("${{secret:kubernetes:{}/b}}", "a".repeat(63));
280        let secret_ref_name_len_64 = format!("${{secret:kubernetes:{}/b}}", "a".repeat(64));
281        let secret_ref_data_key_len_255 = format!("${{secret:kubernetes:a/{}}}", "b".repeat(255));
282        let secret_ref_data_key_len_256 = format!("${{secret:kubernetes:a/{}}}", "b".repeat(256));
283        let values_and_expectations = vec![
284            ("${secret:kubernetes:}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretFormat {
285                secret_ref_str: "${secret:kubernetes:}".to_string()
286            })),
287            ("${secret:kubernetes:ab}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretFormat {
288                secret_ref_str: "${secret:kubernetes:ab}".to_string()
289            })),
290            ("${secret:kubernetes:/}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretName {
291                secret_ref_str: "${secret:kubernetes:/}".to_string(),
292                name: "".to_string(),
293                e: KubernetesSecretNameParseError::Empty
294            })),
295            ("${secret:kubernetes:/b}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretName {
296                secret_ref_str: "${secret:kubernetes:/b}".to_string(),
297                name: "".to_string(),
298                e: KubernetesSecretNameParseError::Empty
299            })),
300            ("${secret:kubernetes:a/}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretDataKey {
301                secret_ref_str: "${secret:kubernetes:a/}".to_string(),
302                data_key: "".to_string(),
303                e: KubernetesSecretDataKeyParseError::Empty
304            })),
305            // Name: character which is not lowercase alphanumeric
306            ("${secret:kubernetes:A/b}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretName {
307                secret_ref_str: "${secret:kubernetes:A/b}".to_string(),
308                name: "A".to_string(),
309                e: KubernetesSecretNameParseError::InvalidFormat
310            })),
311            // Name: cannot start with hyphen
312            ("${secret:kubernetes:-a/b}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretName {
313                secret_ref_str: "${secret:kubernetes:-a/b}".to_string(),
314                name: "-a".to_string(),
315                e: KubernetesSecretNameParseError::InvalidFormat
316            })),
317            // Name: cannot end in hyphen
318            ("${secret:kubernetes:a-/b}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretName {
319                secret_ref_str: "${secret:kubernetes:a-/b}".to_string(),
320                name: "a-".to_string(),
321                e: KubernetesSecretNameParseError::InvalidFormat
322            })),
323            // Data key: character which is not lowercase alphanumeric
324            ("${secret:kubernetes:a/B}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretDataKey {
325                secret_ref_str: "${secret:kubernetes:a/B}".to_string(),
326                data_key: "B".to_string(),
327                e: KubernetesSecretDataKeyParseError::InvalidFormat
328            })),
329            // Data key: cannot start with hyphen
330            ("${secret:kubernetes:a/-b}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretDataKey {
331                secret_ref_str: "${secret:kubernetes:a/-b}".to_string(),
332                data_key: "-b".to_string(),
333                e: KubernetesSecretDataKeyParseError::InvalidFormat
334            })),
335            // Data key: cannot end in hyphen
336            ("${secret:kubernetes:a/b-}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretDataKey {
337                secret_ref_str: "${secret:kubernetes:a/b-}".to_string(),
338                data_key: "b-".to_string(),
339                e: KubernetesSecretDataKeyParseError::InvalidFormat
340            })),
341            // Name: exceeds 63 characters
342            (&secret_ref_name_len_64, Err(MaybeSecretRefParseError::InvalidKubernetesSecretName {
343                secret_ref_str: secret_ref_name_len_64.to_string(),
344                name: "a".repeat(64).to_string(),
345                e: KubernetesSecretNameParseError::TooLong {
346                    name_len: 64
347                }
348            })),
349            // Data key: exceeds 255 characters
350            (&secret_ref_data_key_len_256, Err(MaybeSecretRefParseError::InvalidKubernetesSecretDataKey {
351                secret_ref_str: secret_ref_data_key_len_256.to_string(),
352                data_key: "b".repeat(256).to_string(),
353                e: KubernetesSecretDataKeyParseError::TooLong {
354                    data_key_len: 256
355                }
356            })),
357            // Name: Unicode character
358            // Unicode "Slightly Smiling Face": U+1F642
359            ("${secret:kubernetes:\u{1F642}/b}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretName {
360                secret_ref_str: "${secret:kubernetes:\u{1F642}/b}".to_string(),
361                name: "\u{1F642}".to_string(),
362                e: KubernetesSecretNameParseError::InvalidFormat
363            })),
364            // Data key: Unicode character
365            // Unicode "Slightly Smiling Face": U+1F642
366            ("${secret:kubernetes:a/\u{1F642}}", Err(MaybeSecretRefParseError::InvalidKubernetesSecretDataKey {
367                secret_ref_str: "${secret:kubernetes:a/\u{1F642}}".to_string(),
368                data_key: "\u{1F642}".to_string(),
369                e: KubernetesSecretDataKeyParseError::InvalidFormat
370            })),
371            ("${secret:kubernetes:a/b}", Ok(MaybeSecretRef::SecretRef(SecretRef::Kubernetes {
372                name: "a".to_string(),
373                data_key: "b".to_string()
374            }))),
375            ("${secret:kubernetes:0/1}", Ok(MaybeSecretRef::SecretRef(SecretRef::Kubernetes {
376                name: "0".to_string(),
377                data_key: "1".to_string()
378            }))),
379            ("${secret:kubernetes:a0/b1}", Ok(MaybeSecretRef::SecretRef(SecretRef::Kubernetes {
380                name: "a0".to_string(),
381                data_key: "b1".to_string()
382            }))),
383            ("${secret:kubernetes:0a/1b}", Ok(MaybeSecretRef::SecretRef(SecretRef::Kubernetes {
384                name: "0a".to_string(),
385                data_key: "1b".to_string()
386            }))),
387            ("${secret:kubernetes:a-b/c-d}", Ok(MaybeSecretRef::SecretRef(SecretRef::Kubernetes {
388                name: "a-b".to_string(),
389                data_key: "c-d".to_string()
390            }))),
391            (&secret_ref_name_len_63, Ok(MaybeSecretRef::SecretRef(SecretRef::Kubernetes {
392                name: "a".repeat(63),
393                data_key: "b".to_string()
394            }))),
395            (&secret_ref_data_key_len_255, Ok(MaybeSecretRef::SecretRef(SecretRef::Kubernetes {
396                name: "a".to_string(),
397                data_key: "b".repeat(255),
398            }))),
399        ];
400        test_values_and_expectations(values_and_expectations);
401    }
402
403    #[test]
404    #[rustfmt::skip] // Skip formatting to keep it short
405    fn kubernetes_secret_name_validation() {
406        let name_len_63 = "a".repeat(63);
407        let name_len_64 = "a".repeat(64);
408        for (value, expectation) in vec![
409            ("a", Ok(())),
410            ("0", Ok(())),
411            ("a0", Ok(())),
412            ("0a", Ok(())),
413            ("a-0", Ok(())),
414            (&name_len_63, Ok(())),
415            ("", Err(KubernetesSecretNameParseError::Empty)),
416            ("a-", Err(KubernetesSecretNameParseError::InvalidFormat)),
417            ("-a", Err(KubernetesSecretNameParseError::InvalidFormat)),
418            (&name_len_64, Err(KubernetesSecretNameParseError::TooLong {
419                name_len: 64
420            })),
421        ] {
422            assert_eq!(validate_kubernetes_secret_name(value), expectation);
423        }
424    }
425
426    #[test]
427    #[rustfmt::skip] // Skip formatting to keep it short
428    fn kubernetes_secret_data_key_validation() {
429        let data_key_len_255 = "a".repeat(255);
430        let data_key_len_256 = "a".repeat(256);
431        for (value, expectation) in vec![
432            ("a", Ok(())),
433            ("0", Ok(())),
434            ("a0", Ok(())),
435            ("0a", Ok(())),
436            ("a-0", Ok(())),
437            (&data_key_len_255, Ok(())),
438            ("", Err(KubernetesSecretDataKeyParseError::Empty)),
439            ("a-", Err(KubernetesSecretDataKeyParseError::InvalidFormat)),
440            ("-a", Err(KubernetesSecretDataKeyParseError::InvalidFormat)),
441            (&data_key_len_256, Err(KubernetesSecretDataKeyParseError::TooLong {
442                data_key_len: 256
443            })),
444        ] {
445            assert_eq!(validate_kubernetes_secret_data_key(value), expectation);
446        }
447    }
448}