Skip to main content

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