dd_sds/match_validation/
config.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::collections::BTreeMap;
4use std::str::FromStr;
5use std::{hash::Hash, time::Duration};
6
7#[cfg(feature = "third-party-active-checkers")]
8use crate::match_validation::http_validator_v2::HttpValidatorV2;
9
10#[cfg(feature = "third-party-active-checkers")]
11use super::aws_validator::AwsValidator;
12use super::config_v2::CustomHttpConfigV2;
13#[cfg(feature = "third-party-active-checkers")]
14use super::http_validator::HttpValidator;
15use super::match_validator::MatchValidator;
16
17pub const DEFAULT_HTTPS_TIMEOUT_SEC: u64 = 3;
18pub const DEFAULT_AWS_STS_ENDPOINT: &str = "https://sts.amazonaws.com";
19
20#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
21pub struct AwsConfig {
22    // Override default AWS STS endpoint for testing
23    #[serde(default = "default_aws_sts_endpoint")]
24    pub aws_sts_endpoint: String,
25    // Override default datetime for testing
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub forced_datetime_utc: Option<DateTime<Utc>>,
28    #[serde(default = "default_timeout")]
29    pub timeout: Duration,
30}
31
32fn default_aws_sts_endpoint() -> String {
33    DEFAULT_AWS_STS_ENDPOINT.to_string()
34}
35
36fn default_timeout() -> Duration {
37    Duration::from_secs(DEFAULT_HTTPS_TIMEOUT_SEC)
38}
39
40impl Default for AwsConfig {
41    fn default() -> Self {
42        AwsConfig {
43            aws_sts_endpoint: default_aws_sts_endpoint(),
44            forced_datetime_utc: None,
45            timeout: default_timeout(),
46        }
47    }
48}
49
50#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
51#[serde(tag = "kind")]
52pub enum AwsType {
53    AwsId,
54    AwsSecret(AwsConfig),
55    AwsSession,
56}
57
58#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
59#[serde(rename_all = "UPPERCASE")]
60pub enum HttpMethod {
61    Get,
62    Post,
63    Put,
64    Delete,
65    Patch,
66}
67
68impl FromStr for HttpMethod {
69    type Err = String;
70
71    fn from_str(s: &str) -> Result<Self, Self::Err> {
72        match s.to_uppercase().as_str() {
73            "GET" => Ok(HttpMethod::Get),
74            "POST" => Ok(HttpMethod::Post),
75            "PUT" => Ok(HttpMethod::Put),
76            "DELETE" => Ok(HttpMethod::Delete),
77            "PATCH" => Ok(HttpMethod::Patch),
78            _ => Err(format!("Invalid HTTP method: {s}")),
79        }
80    }
81}
82
83#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
84pub struct RequestHeader {
85    pub key: String,
86    // $MATCH is a special keyword that will be replaced by the matched string
87    pub value: String,
88}
89
90impl RequestHeader {
91    pub fn get_value_with_match(&self, matche: &str) -> String {
92        // Replace $MATCH in value
93        let mut value = self.value.clone();
94        value = value.replace("$MATCH", matche);
95        value
96    }
97}
98
99#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
100pub struct HttpValidatorOption {
101    pub timeout: Duration,
102    // TODO(trosenblatt) add more options
103    // pub max_retries: u64,
104    // pub retry_delay: u64,
105}
106
107#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
108pub struct CustomHttpConfig {
109    pub endpoint: String,
110    #[serde(default)]
111    pub hosts: Vec<String>,
112    #[serde(default = "default_http_method")]
113    pub http_method: HttpMethod,
114    pub request_headers: BTreeMap<String, String>,
115    #[serde(default = "default_valid_http_status_code")]
116    pub valid_http_status_code: Vec<HttpStatusCodeRange>,
117    #[serde(default = "default_invalid_http_status_code")]
118    pub invalid_http_status_code: Vec<HttpStatusCodeRange>,
119    #[serde(default = "default_timeout_seconds")]
120    pub timeout_seconds: u32,
121}
122
123impl Default for CustomHttpConfig {
124    fn default() -> Self {
125        CustomHttpConfig {
126            endpoint: "".to_string(),
127            hosts: vec![],
128            http_method: HttpMethod::Get,
129            request_headers: BTreeMap::new(),
130            valid_http_status_code: vec![],
131            invalid_http_status_code: vec![],
132            timeout_seconds: DEFAULT_HTTPS_TIMEOUT_SEC as u32,
133        }
134    }
135}
136
137impl CustomHttpConfig {
138    pub fn get_endpoints(&self) -> Result<Vec<String>, String> {
139        // Handle errors cases
140        // - endpoint contains $HOST but no hosts are provided
141        // - endpoint does not contain $HOST but hosts are provided
142        if self.endpoint.contains("$HOST") && self.hosts.is_empty() {
143            return Err("Endpoint contains $HOST but no hosts are provided".to_string());
144        }
145        if !self.endpoint.contains("$HOST") && !self.hosts.is_empty() {
146            return Err("Endpoint does not contain $HOST but hosts are provided".to_string());
147        }
148
149        // Replace $HOST in endpoint and build the endpoints vector
150        let mut endpoints = vec![];
151        for host in self.hosts.clone() {
152            endpoints.push(self.endpoint.replace("$HOST", &host));
153        }
154        if endpoints.is_empty() {
155            // If no hosts are provided, use the endpoint as is
156            endpoints.push(self.endpoint.to_string());
157        }
158        Ok(endpoints)
159    }
160
161    // Builders
162
163    pub fn with_endpoint(mut self, endpoint: String) -> Self {
164        self.endpoint = endpoint;
165        self
166    }
167
168    pub fn with_hosts(mut self, hosts: Vec<String>) -> Self {
169        self.hosts = hosts;
170        self
171    }
172
173    pub fn with_request_headers(mut self, request_headers: BTreeMap<String, String>) -> Self {
174        self.request_headers = request_headers;
175        self
176    }
177
178    pub fn with_valid_http_status_code(
179        mut self,
180        valid_http_status_code: Vec<HttpStatusCodeRange>,
181    ) -> Self {
182        self.valid_http_status_code = valid_http_status_code;
183        self
184    }
185
186    pub fn with_invalid_http_status_code(
187        mut self,
188        invalid_http_status_code: Vec<HttpStatusCodeRange>,
189    ) -> Self {
190        self.invalid_http_status_code = invalid_http_status_code;
191        self
192    }
193
194    // Setters
195
196    pub fn set_endpoint(&mut self, endpoint: String) {
197        self.endpoint = endpoint;
198    }
199
200    pub fn set_hosts(&mut self, hosts: Vec<String>) {
201        self.hosts = hosts;
202    }
203
204    pub fn set_http_method(&mut self, http_method: HttpMethod) {
205        self.http_method = http_method;
206    }
207
208    pub fn set_request_headers(&mut self, request_headers: BTreeMap<String, String>) {
209        self.request_headers = request_headers;
210    }
211
212    pub fn set_valid_http_status_code(&mut self, valid_http_status_code: Vec<HttpStatusCodeRange>) {
213        self.valid_http_status_code = valid_http_status_code;
214    }
215
216    pub fn set_invalid_http_status_code(
217        &mut self,
218        invalid_http_status_code: Vec<HttpStatusCodeRange>,
219    ) {
220        self.invalid_http_status_code = invalid_http_status_code;
221    }
222
223    pub fn set_timeout_seconds(&mut self, timeout_seconds: u32) {
224        self.timeout_seconds = timeout_seconds;
225    }
226}
227
228fn default_timeout_seconds() -> u32 {
229    DEFAULT_HTTPS_TIMEOUT_SEC as u32
230}
231
232fn default_http_method() -> HttpMethod {
233    HttpMethod::Get
234}
235
236fn default_valid_http_status_code() -> Vec<HttpStatusCodeRange> {
237    vec![HttpStatusCodeRange {
238        start: 200,
239        end: 300,
240    }]
241}
242
243fn default_invalid_http_status_code() -> Vec<HttpStatusCodeRange> {
244    vec![HttpStatusCodeRange {
245        start: 400,
246        end: 500,
247    }]
248}
249
250#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
251pub struct HttpStatusCodeRange {
252    pub start: u16,
253    pub end: u16,
254}
255
256#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
257#[serde(tag = "type", content = "config")]
258pub enum MatchValidationType {
259    Aws(AwsType),
260    CustomHttp(CustomHttpConfig),
261    CustomHttpV2(CustomHttpConfigV2),
262}
263
264impl MatchValidationType {
265    // Method used to check if the validator can be created based on this type
266    pub fn can_create_match_validator(&self) -> bool {
267        match self {
268            MatchValidationType::Aws(aws_type) => matches!(aws_type, AwsType::AwsSecret(_)),
269            MatchValidationType::CustomHttp(_) => true,
270            MatchValidationType::CustomHttpV2(http_config_v2) => !http_config_v2.calls.is_empty(),
271        }
272    }
273    pub fn get_internal_match_validation_type(&self) -> InternalMatchValidationType {
274        match self {
275            MatchValidationType::Aws(_) => InternalMatchValidationType::Aws,
276            MatchValidationType::CustomHttp(http_config) => {
277                InternalMatchValidationType::CustomHttp(http_config.get_endpoints().unwrap())
278            }
279            MatchValidationType::CustomHttpV2(_) => InternalMatchValidationType::CustomHttpV2,
280        }
281    }
282    #[cfg(feature = "third-party-active-checkers")]
283    pub fn into_match_validator(&self) -> Result<Box<dyn MatchValidator>, String> {
284        match self {
285            MatchValidationType::Aws(aws_type) => match aws_type {
286                AwsType::AwsSecret(aws_config) => {
287                    Ok(Box::new(AwsValidator::new(aws_config.clone())))
288                }
289                _ => Err("This aws type shall not be used to create a validator".to_string()),
290            },
291            MatchValidationType::CustomHttp(http_config) => Ok(Box::new(
292                HttpValidator::new_from_config(http_config.clone()),
293            )),
294            MatchValidationType::CustomHttpV2(_) => Ok(Box::new(HttpValidatorV2)),
295        }
296    }
297
298    /// When the `third-party-active-checkers` feature is disabled, no network-backed
299    /// validators can be created. Callers should gate validator creation on the same
300    /// feature; this stub exists only so the type keeps a stable public API.
301    #[cfg(not(feature = "third-party-active-checkers"))]
302    pub fn into_match_validator(&self) -> Result<Box<dyn MatchValidator>, String> {
303        Err("Third-party active checkers are not enabled in this build".to_string())
304    }
305}
306
307// This is the match validation type stored in the compiled rule
308// It is used to retrieve the MatchValidator. We don't need the full configuration for that purpose
309// as it would be heavy to compute hash and compare the full configuration.
310#[derive(PartialEq, Eq, Hash)]
311pub enum InternalMatchValidationType {
312    Aws,
313    CustomHttp(Vec<String>),
314    CustomHttpV2,
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320
321    #[test]
322    fn test_serialization_of_aws_config() {
323        let aws_config = AwsConfig {
324            aws_sts_endpoint: "https://sts.amazonaws.com".to_string(),
325            forced_datetime_utc: None,
326            timeout: Duration::from_secs(3),
327        };
328        let serialized = serde_json::to_string(&aws_config).unwrap();
329        // The forced_datetime_utc is not serialized because it is None
330        assert_eq!(
331            serialized,
332            "{\"aws_sts_endpoint\":\"https://sts.amazonaws.com\",\"timeout\":{\"secs\":3,\"nanos\":0}}"
333        );
334    }
335}