Skip to main content

slim_controller/
config.rs

1// Copyright AGNTCY Contributors (https://github.com/agntcy)
2// SPDX-License-Identifier: Apache-2.0
3
4use std::sync::Arc;
5
6use serde::Deserialize;
7
8use slim_auth::auth_provider::{AuthProvider, AuthVerifier};
9use slim_config::auth::identity::{IdentityProviderConfig, IdentityVerifierConfig};
10use slim_config::component::configuration::Configuration;
11use slim_config::component::id::ID;
12use slim_config::grpc::client::ClientConfig;
13use slim_config::grpc::server::ServerConfig;
14use slim_datapath::message_processing::MessageProcessor;
15
16use crate::errors::ControllerError;
17use crate::service::{ControlPlane, ControlPlaneSettings, from_server_config};
18
19/// Configuration for the Control-Plane / Data-Plane component
20#[derive(Debug, Clone, Deserialize, Default, PartialEq)]
21#[serde(deny_unknown_fields)]
22pub struct Config {
23    /// Controller GRPC server settings
24    #[serde(default)]
25    pub servers: Vec<ServerConfig>,
26
27    /// Controller client config to connect to control plane
28    #[serde(default)]
29    pub clients: Vec<ClientConfig>,
30
31    /// Token provider authentication configuration
32    #[serde(default)]
33    pub token_provider: IdentityProviderConfig,
34
35    /// Token verifier authentication configuration
36    #[serde(default)]
37    pub token_verifier: IdentityVerifierConfig,
38
39    /// How long to keep routing state after a server-side connection drops,
40    /// waiting for the peer to reconnect before notifying the control plane.
41    /// Accepts duration strings like "30s", "1s", "500ms".  Defaults to 30 s.
42    #[serde(default)]
43    pub recovery_ttl: Option<duration_string::DurationString>,
44}
45
46impl Config {
47    /// Create a new Config instance with default values
48    pub fn new() -> Self {
49        Self::default()
50    }
51
52    pub fn is_default(&self) -> bool {
53        self == &Self::default()
54    }
55
56    /// Create a new Config instance with the given servers
57    pub fn with_servers(self, servers: Vec<ServerConfig>) -> Self {
58        Self { servers, ..self }
59    }
60
61    /// Create a new Config instance with the given clients
62    pub fn with_clients(self, clients: Vec<ClientConfig>) -> Self {
63        Self { clients, ..self }
64    }
65
66    /// Set the token provider authentication configuration
67    pub fn with_token_provider_auth(self, auth: IdentityProviderConfig) -> Self {
68        Self {
69            token_provider: auth,
70            ..self
71        }
72    }
73
74    /// Set the token verifier authentication configuration
75    pub fn with_token_verifier_auth(self, auth: IdentityVerifierConfig) -> Self {
76        Self {
77            token_verifier: auth,
78            ..self
79        }
80    }
81
82    /// Get the list of server configurations
83    pub fn servers(&self) -> &[ServerConfig] {
84        &self.servers
85    }
86
87    /// Get the list of client configurations
88    pub fn clients(&self) -> &[ClientConfig] {
89        &self.clients
90    }
91
92    fn get_token_provider_auth(&self) -> Option<AuthProvider> {
93        match &self.token_provider {
94            IdentityProviderConfig::SharedSecret { id, data } => {
95                AuthProvider::shared_secret_from_str(id, data).ok()
96            }
97            IdentityProviderConfig::StaticJwt(static_jwt_config) => {
98                let provider = static_jwt_config
99                    .build_static_token_provider()
100                    .expect("Failed to build StaticTokenProvider");
101                Some(AuthProvider::static_token(provider))
102            }
103            IdentityProviderConfig::Jwt(jwt_config) => {
104                let provider = jwt_config
105                    .get_provider()
106                    .expect("Failed to build JwtTokenProvider");
107                Some(AuthProvider::jwt_signer(provider))
108            }
109            #[cfg(not(target_family = "windows"))]
110            IdentityProviderConfig::Spire(spire_config) => {
111                let manager = spire_config
112                    .create_provider()
113                    .expect("Failed to build SpireIdentityManager");
114                Some(AuthProvider::spire(manager))
115            }
116            IdentityProviderConfig::None => None,
117        }
118    }
119
120    fn get_token_verifier_auth(&self) -> Option<AuthVerifier> {
121        match &self.token_verifier {
122            IdentityVerifierConfig::SharedSecret { id, data } => {
123                AuthVerifier::shared_secret_from_str(id, data).ok()
124            }
125            IdentityVerifierConfig::Jwt(jwt_config) => {
126                let verifier = jwt_config
127                    .get_verifier()
128                    .expect("Failed to build JwtTokenVerifier");
129                Some(AuthVerifier::jwt_verifier(verifier))
130            }
131            #[cfg(not(target_family = "windows"))]
132            IdentityVerifierConfig::Spire(spire_config) => {
133                let manager = spire_config
134                    .create_provider()
135                    .expect("Failed to build SpireIdentityManager");
136                Some(AuthVerifier::spire(manager))
137            }
138            IdentityVerifierConfig::None => None,
139        }
140    }
141
142    /// Create a ControlPlane service instance from this configuration
143    pub fn into_service(
144        &self,
145        id: ID,
146        group_name: Option<String>,
147        message_processor: Arc<MessageProcessor>,
148        // List of server configurations for the dataplane services.
149        // Used to extract connection type information required to connect to the node
150        // (e.g., TLS settings). This information is used by the control plane.
151        dataplane_servers: &[ServerConfig],
152    ) -> ControlPlane {
153        let auth_provider = self.get_token_provider_auth();
154        let auth_verifier = self.get_token_verifier_auth();
155
156        let connection_details = dataplane_servers.iter().map(from_server_config).collect();
157
158        ControlPlane::new(ControlPlaneSettings {
159            id,
160            group_name,
161            servers: self.servers.clone(),
162            clients: self.clients.clone(),
163            message_processor,
164            auth_provider,
165            auth_verifier,
166            connection_details,
167        })
168    }
169}
170
171impl Configuration for Config {
172    type Error = ControllerError;
173
174    fn validate(&self) -> Result<(), Self::Error> {
175        // Validate client and server configurations
176        for server in self.servers.iter() {
177            server.validate()?;
178        }
179
180        for client in &self.clients {
181            client.validate()?;
182        }
183
184        Ok(())
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use slim_config::auth::jwt::Config as JwtConfig;
192    use slim_config::auth::static_jwt::Config as StaticJwtConfig;
193    use slim_config::component::id::{ID, Kind};
194    use slim_config::grpc::client::ClientConfig;
195    use slim_config::grpc::server::ServerConfig;
196    use slim_datapath::message_processing::MessageProcessor;
197    use slim_testing::utils::TEST_VALID_SECRET;
198    use std::sync::Arc;
199
200    fn create_test_server_config() -> ServerConfig {
201        ServerConfig::with_endpoint("127.0.0.1:50051")
202            .with_tls_settings(slim_config::tls::server::TlsServerConfig::insecure())
203    }
204
205    fn create_test_client_config() -> ClientConfig {
206        ClientConfig::with_endpoint("http://127.0.0.1:50051")
207            .with_tls_setting(slim_config::tls::client::TlsClientConfig::insecure())
208    }
209
210    #[test]
211    fn test_config_new() {
212        let config = Config::new();
213        assert!(config.servers.is_empty());
214        assert!(config.clients.is_empty());
215        assert_eq!(config.token_provider, IdentityProviderConfig::None);
216        assert_eq!(config.token_verifier, IdentityVerifierConfig::None);
217    }
218
219    #[test]
220    fn test_config_default() {
221        let config = Config::default();
222        assert!(config.servers.is_empty());
223        assert!(config.clients.is_empty());
224        assert_eq!(config.token_provider, IdentityProviderConfig::None);
225        assert_eq!(config.token_verifier, IdentityVerifierConfig::None);
226    }
227
228    #[test]
229    fn test_config_with_servers() {
230        let server_config = create_test_server_config();
231        let config = Config::new().with_servers(vec![server_config.clone()]);
232
233        assert_eq!(config.servers.len(), 1);
234        assert_eq!(config.servers[0], server_config);
235        assert!(config.clients.is_empty());
236    }
237
238    #[test]
239    fn test_config_with_clients() {
240        let client_config = create_test_client_config();
241        let config = Config::new().with_clients(vec![client_config.clone()]);
242
243        assert_eq!(config.clients.len(), 1);
244        assert_eq!(config.clients[0], client_config);
245        assert!(config.servers.is_empty());
246    }
247
248    #[test]
249    fn test_config_with_token_provider_auth_shared_secret() {
250        let auth = IdentityProviderConfig::SharedSecret {
251            id: "test-provider".to_string(),
252            data: "test-secret".to_string(),
253        };
254        let config = Config::new().with_token_provider_auth(auth.clone());
255
256        assert_eq!(config.token_provider, auth);
257        assert_eq!(config.token_verifier, IdentityVerifierConfig::None);
258    }
259
260    #[test]
261    fn test_config_with_token_provider_auth_static_jwt() {
262        let static_jwt_config = StaticJwtConfig::with_file("test-key".to_string());
263        let auth = IdentityProviderConfig::StaticJwt(static_jwt_config);
264        let config = Config::new().with_token_provider_auth(auth.clone());
265
266        assert_eq!(config.token_provider, auth);
267    }
268
269    #[test]
270    fn test_config_with_token_provider_auth_jwt() {
271        use slim_auth::jwt::{Algorithm, Key, KeyData, KeyFormat};
272        use slim_config::auth::jwt::{Claims, JwtKey};
273        use std::time::Duration;
274
275        let claims = Claims::default();
276        let duration = Duration::from_secs(3600);
277        let key = JwtKey::Encoding(Key {
278            algorithm: Algorithm::HS256,
279            format: KeyFormat::Pem,
280            key: KeyData::Data("test-secret".to_string()),
281        });
282        let jwt_config = JwtConfig::new(claims, duration, key);
283        let auth = IdentityProviderConfig::Jwt(jwt_config);
284        let config = Config::new().with_token_provider_auth(auth.clone());
285
286        assert_eq!(config.token_provider, auth);
287    }
288
289    #[test]
290    fn test_config_with_token_verifier_auth_shared_secret() {
291        let auth = IdentityVerifierConfig::SharedSecret {
292            id: "test-verifier".to_string(),
293            data: "test-secret".to_string(),
294        };
295        let config = Config::new().with_token_verifier_auth(auth.clone());
296
297        assert_eq!(config.token_verifier, auth);
298        assert_eq!(config.token_provider, IdentityProviderConfig::None);
299    }
300
301    #[test]
302    fn test_config_with_token_verifier_auth_jwt() {
303        use slim_auth::jwt::{Algorithm, Key, KeyData, KeyFormat};
304        use slim_config::auth::jwt::{Claims, JwtKey};
305        use std::time::Duration;
306
307        let claims = Claims::default();
308        let duration = Duration::from_secs(3600);
309        let key = JwtKey::Decoding(Key {
310            algorithm: Algorithm::HS256,
311            format: KeyFormat::Pem,
312            key: KeyData::Data("test-secret".to_string()),
313        });
314        let jwt_config = JwtConfig::new(claims, duration, key);
315        let auth = IdentityVerifierConfig::Jwt(jwt_config);
316        let config = Config::new().with_token_verifier_auth(auth.clone());
317
318        assert_eq!(config.token_verifier, auth);
319    }
320
321    #[test]
322    fn test_config_servers_getter() {
323        let server_config = create_test_server_config();
324        let config = Config::new().with_servers(vec![server_config.clone()]);
325
326        let servers = config.servers();
327        assert_eq!(servers.len(), 1);
328        assert_eq!(servers[0], server_config);
329    }
330
331    #[test]
332    fn test_config_clients_getter() {
333        let client_config = create_test_client_config();
334        let config = Config::new().with_clients(vec![client_config.clone()]);
335
336        let clients = config.clients();
337        assert_eq!(clients.len(), 1);
338        assert_eq!(clients[0], client_config);
339    }
340
341    #[test]
342    fn test_config_chaining() {
343        let server_config = create_test_server_config();
344        let client_config = create_test_client_config();
345        let provider_auth = IdentityProviderConfig::SharedSecret {
346            id: "test-provider".to_string(),
347            data: "provider-secret".to_string(),
348        };
349        let verifier_auth = IdentityVerifierConfig::SharedSecret {
350            id: "test-verifier".to_string(),
351            data: "verifier-secret".to_string(),
352        };
353
354        let config = Config::new()
355            .with_servers(vec![server_config.clone()])
356            .with_clients(vec![client_config.clone()])
357            .with_token_provider_auth(provider_auth.clone())
358            .with_token_verifier_auth(verifier_auth.clone());
359
360        assert_eq!(config.servers.len(), 1);
361        assert_eq!(config.clients.len(), 1);
362        assert_eq!(config.token_provider, provider_auth);
363        assert_eq!(config.token_verifier, verifier_auth);
364    }
365
366    #[test]
367    fn test_config_validate_empty() {
368        let config = Config::new();
369        assert!(config.validate().is_ok());
370    }
371
372    #[test]
373    fn test_config_validate_with_valid_servers_and_clients() {
374        let server_config = create_test_server_config();
375        let client_config = create_test_client_config();
376        let config = Config::new()
377            .with_servers(vec![server_config])
378            .with_clients(vec![client_config]);
379
380        assert!(config.validate().is_ok());
381    }
382
383    #[test]
384    fn test_token_provider_auth_config_equality() {
385        let secret1 = IdentityProviderConfig::SharedSecret {
386            id: "test-id".to_string(),
387            data: "secret0".to_string(),
388        };
389        let secret2 = IdentityProviderConfig::SharedSecret {
390            id: "test-id".to_string(),
391            data: "secret0".to_string(),
392        };
393        let secret3 = IdentityProviderConfig::SharedSecret {
394            id: "test-id".to_string(),
395            data: "secret2".to_string(),
396        };
397
398        assert_eq!(secret1, secret2);
399        assert_ne!(secret1, secret3);
400    }
401
402    #[test]
403    fn test_token_verifier_auth_config_equality() {
404        let secret1 = IdentityVerifierConfig::SharedSecret {
405            id: "test-id".to_string(),
406            data: "secret0".to_string(),
407        };
408        let secret2 = IdentityVerifierConfig::SharedSecret {
409            id: "test-id".to_string(),
410            data: "secret0".to_string(),
411        };
412        let secret3 = IdentityVerifierConfig::SharedSecret {
413            id: "test-id".to_string(),
414            data: "secret2".to_string(),
415        };
416
417        assert_eq!(secret1, secret2);
418        assert_ne!(secret1, secret3);
419    }
420
421    #[test]
422    fn test_config_clone() {
423        let server_config = create_test_server_config();
424        let client_config = create_test_client_config();
425        let auth = IdentityProviderConfig::SharedSecret {
426            id: "test-provider".to_string(),
427            data: "secret0".to_string(),
428        };
429
430        let config1 = Config::new()
431            .with_servers(vec![server_config])
432            .with_clients(vec![client_config])
433            .with_token_provider_auth(auth);
434
435        let config2 = config1.clone();
436
437        assert_eq!(config1.servers, config2.servers);
438        assert_eq!(config1.clients, config2.clients);
439        assert_eq!(config1.token_provider, config2.token_provider);
440        assert_eq!(config1.token_verifier, config2.token_verifier);
441    }
442
443    #[tokio::test]
444    async fn test_config_into_service() {
445        let server_config = create_test_server_config();
446        let client_config = create_test_client_config();
447        let auth = IdentityProviderConfig::SharedSecret {
448            id: "test-provider".to_string(),
449            data: TEST_VALID_SECRET.to_string(),
450        };
451
452        let config = Config::new()
453            .with_servers(vec![server_config.clone()])
454            .with_clients(vec![client_config])
455            .with_token_provider_auth(auth);
456
457        let id = ID::new_with_name(Kind::new("slim").unwrap(), "test-instance").unwrap();
458        let group_name = Some("test-group".to_string());
459        let message_processor = Arc::new(MessageProcessor::new());
460
461        let _control_plane =
462            config.into_service(id, group_name, message_processor, &[server_config]);
463    }
464
465    #[test]
466    fn test_config_debug_trait() {
467        let config = Config::new();
468        let debug_str = format!("{:?}", config);
469        assert!(debug_str.contains("Config"));
470        assert!(debug_str.contains("servers"));
471        assert!(debug_str.contains("clients"));
472    }
473
474    mod serde_tests {
475        use super::*;
476        use serde_json;
477
478        #[test]
479        fn test_token_provider_auth_config_serialize_shared_secret() {
480            let auth = IdentityProviderConfig::SharedSecret {
481                id: "test-provider".to_string(),
482                data: "test-secret".to_string(),
483            };
484            let json = serde_json::to_string(&auth).unwrap();
485            assert!(json.contains("shared_secret"));
486            assert!(json.contains("test-secret"));
487        }
488
489        #[test]
490        fn test_token_provider_auth_config_deserialize_shared_secret() {
491            let json = r#"{"type": "shared_secret", "id": "test-provider", "data": "test-secret"}"#;
492            let auth: IdentityProviderConfig = serde_json::from_str(json).unwrap();
493
494            match auth {
495                IdentityProviderConfig::SharedSecret { id, data } => {
496                    assert_eq!(id, "test-provider");
497                    assert_eq!(data, "test-secret");
498                }
499                _ => panic!("Expected SharedSecret variant"),
500            }
501        }
502
503        #[test]
504        fn test_config_validate_with_multiple_servers() {
505            let server1 = create_test_server_config();
506            let server2 = ServerConfig::with_endpoint("127.0.0.1:50052")
507                .with_tls_settings(slim_config::tls::server::TlsServerConfig::insecure());
508
509            let config = Config::new().with_servers(vec![server1, server2]);
510            assert!(config.validate().is_ok());
511        }
512
513        #[test]
514        fn test_config_validate_with_multiple_clients() {
515            let client1 = create_test_client_config();
516            let client2 = ClientConfig::with_endpoint("http://127.0.0.1:50052")
517                .with_tls_setting(slim_config::tls::client::TlsClientConfig::insecure());
518
519            let config = Config::new().with_clients(vec![client1, client2]);
520            assert!(config.validate().is_ok());
521        }
522
523        #[test]
524        fn test_config_with_all_auth_combinations() {
525            let provider_auth = IdentityProviderConfig::SharedSecret {
526                id: "test-provider".to_string(),
527                data: "provider-secret".to_string(),
528            };
529            let verifier_auth = IdentityVerifierConfig::SharedSecret {
530                id: "test-verifier".to_string(),
531                data: "verifier-secret".to_string(),
532            };
533
534            let config = Config::new()
535                .with_token_provider_auth(provider_auth.clone())
536                .with_token_verifier_auth(verifier_auth.clone());
537
538            assert_eq!(config.token_provider, provider_auth);
539            assert_eq!(config.token_verifier, verifier_auth);
540        }
541
542        #[test]
543        fn test_empty_servers_slice() {
544            let config = Config::new();
545            let servers = config.servers();
546            assert!(servers.is_empty());
547            assert_eq!(servers.len(), 0);
548        }
549
550        #[test]
551        fn test_empty_clients_slice() {
552            let config = Config::new();
553            let clients = config.clients();
554            assert!(clients.is_empty());
555            assert_eq!(clients.len(), 0);
556        }
557
558        #[test]
559        fn test_config_partial_eq() {
560            let config1 = Config::new();
561            let config2 = Config::new();
562
563            // Default configs should be equal
564            assert_eq!(config1, config2);
565
566            // Add server to one config
567            let server_config = create_test_server_config();
568            let config3 = config1.clone().with_servers(vec![server_config]);
569
570            // Should not be equal anymore
571            assert_ne!(config1, config3);
572        }
573
574        #[test]
575        fn test_mixed_auth_types() {
576            use slim_auth::jwt::{Algorithm, Key, KeyData, KeyFormat};
577
578            let static_jwt =
579                IdentityProviderConfig::StaticJwt(StaticJwtConfig::with_file("test-token.jwt"));
580
581            let jwt = IdentityVerifierConfig::Jwt(JwtConfig::new(
582                slim_config::auth::jwt::Claims::default(),
583                std::time::Duration::from_secs(3600),
584                slim_config::auth::jwt::JwtKey::Decoding(Key {
585                    algorithm: Algorithm::HS256,
586                    format: KeyFormat::Pem,
587                    key: KeyData::Data("test-key".to_string()),
588                }),
589            ));
590
591            let config = Config::new()
592                .with_token_provider_auth(static_jwt.clone())
593                .with_token_verifier_auth(jwt.clone());
594
595            assert_eq!(config.token_provider, static_jwt);
596            assert_eq!(config.token_verifier, jwt);
597        }
598
599        mod edge_case_tests {
600            use super::*;
601
602            #[test]
603            fn test_config_builder_pattern_reuse() {
604                let base_config = Config::new();
605
606                let config1 = base_config
607                    .clone()
608                    .with_servers(vec![create_test_server_config()]);
609                let config2 = base_config
610                    .clone()
611                    .with_clients(vec![create_test_client_config()]);
612
613                // Base config should still be empty
614                assert!(base_config.servers.is_empty());
615                assert!(base_config.clients.is_empty());
616
617                // Derived configs should have their respective additions
618                assert_eq!(config1.servers.len(), 1);
619                assert!(config1.clients.is_empty());
620
621                assert!(config2.servers.is_empty());
622                assert_eq!(config2.clients.len(), 1);
623            }
624
625            #[test]
626            fn test_config_overwrite_behavior() {
627                let server1 = create_test_server_config();
628                let server2 = ServerConfig::with_endpoint("127.0.0.1:50052")
629                    .with_tls_settings(slim_config::tls::server::TlsServerConfig::insecure());
630
631                let config = Config::new()
632                    .with_servers(vec![server1])
633                    .with_servers(vec![server2.clone()]); // This should overwrite, not append
634
635                assert_eq!(config.servers.len(), 1);
636                assert_eq!(config.servers[0], server2);
637            }
638
639            #[test]
640            fn test_auth_config_none_variants() {
641                let config = Config::new();
642
643                assert_eq!(config.token_provider, IdentityProviderConfig::None);
644                assert_eq!(config.token_verifier, IdentityVerifierConfig::None);
645
646                // Adding one shouldn't affect the other
647                let config_with_provider =
648                    config
649                        .clone()
650                        .with_token_provider_auth(IdentityProviderConfig::SharedSecret {
651                            id: "test-provider".to_string(),
652                            data: "secret".to_string(),
653                        });
654
655                assert_ne!(
656                    config_with_provider.token_provider,
657                    IdentityProviderConfig::None
658                );
659                assert_eq!(
660                    config_with_provider.token_verifier,
661                    IdentityVerifierConfig::None
662                );
663            }
664        }
665
666        #[test]
667        fn test_token_verifier_auth_config_serialize_shared_secret() {
668            let auth = IdentityVerifierConfig::SharedSecret {
669                id: "test-verifier".to_string(),
670                data: "test-secret".to_string(),
671            };
672            let json = serde_json::to_string(&auth).unwrap();
673            assert!(json.contains("shared_secret"));
674            assert!(json.contains("test-secret"));
675        }
676
677        #[test]
678        fn test_token_verifier_auth_config_deserialize_shared_secret() {
679            let json = r#"{"type": "shared_secret", "id": "test-verifier", "data": "test-secret"}"#;
680            let auth: IdentityVerifierConfig = serde_json::from_str(json).unwrap();
681
682            match auth {
683                IdentityVerifierConfig::SharedSecret { id, data } => {
684                    assert_eq!(id, "test-verifier");
685                    assert_eq!(data, "test-secret");
686                }
687                _ => panic!("Expected SharedSecret variant"),
688            }
689        }
690    }
691}