aster_forge_external_auth/
registry.rs1use std::collections::HashMap;
10use std::sync::{Arc, OnceLock};
11
12use super::driver::{
13 ExternalAuthProviderConfig, ExternalAuthProviderDescriptor, ExternalAuthProviderDriver,
14};
15#[cfg(feature = "github")]
16use super::providers::github::GitHubProviderDriver;
17#[cfg(feature = "google")]
18use super::providers::google::GoogleProviderDriver;
19#[cfg(feature = "microsoft")]
20use super::providers::microsoft::MicrosoftProviderDriver;
21#[cfg(feature = "oauth2")]
22use super::providers::oauth2::OAuth2ProviderDriver;
23#[cfg(feature = "oidc")]
24use super::providers::oidc::OidcProviderDriver;
25#[cfg(feature = "qq")]
26use super::providers::qq::QqProviderDriver;
27use crate::types::ExternalAuthProviderKind;
28use crate::{ExternalAuthError, Result};
29
30pub struct ExternalAuthProviderRegistry {
38 drivers: HashMap<ExternalAuthProviderKind, Arc<dyn ExternalAuthProviderDriver>>,
39}
40
41impl ExternalAuthProviderRegistry {
42 #[must_use]
48 pub fn empty() -> Self {
49 Self {
50 drivers: HashMap::new(),
51 }
52 }
53
54 #[must_use]
56 pub fn new() -> Self {
57 #[cfg(not(any(
58 feature = "oidc",
59 feature = "oauth2",
60 feature = "github",
61 feature = "google",
62 feature = "microsoft",
63 feature = "qq"
64 )))]
65 let registry = Self::empty();
66 #[cfg(any(
67 feature = "oidc",
68 feature = "oauth2",
69 feature = "github",
70 feature = "google",
71 feature = "microsoft",
72 feature = "qq"
73 ))]
74 let mut registry = Self::empty();
75 #[cfg(feature = "oidc")]
76 registry.register_builtin(OidcProviderDriver::new());
77 #[cfg(feature = "oauth2")]
78 registry.register_builtin(OAuth2ProviderDriver::new());
79 #[cfg(feature = "github")]
80 registry.register_builtin(GitHubProviderDriver::new());
81 #[cfg(feature = "google")]
82 registry.register_builtin(GoogleProviderDriver::new());
83 #[cfg(feature = "microsoft")]
84 registry.register_builtin(MicrosoftProviderDriver::new());
85 #[cfg(feature = "qq")]
86 registry.register_builtin(QqProviderDriver::new());
87 registry
88 }
89
90 pub fn with_external_registrations<F>(configure: F) -> Result<Self>
102 where
103 F: FnOnce(&mut Self) -> Result<()>,
104 {
105 let mut registry = Self::new();
106 configure(&mut registry)?;
107 Ok(registry)
108 }
109
110 pub fn add<D>(&mut self, driver: D) -> Result<()>
120 where
121 D: ExternalAuthProviderDriver + 'static,
122 {
123 self.add_arc(Arc::new(driver))
124 }
125
126 pub fn add_arc(&mut self, driver: Arc<dyn ExternalAuthProviderDriver>) -> Result<()> {
132 let kind = driver.kind();
133 Self::validate_driver_descriptor(kind, &driver.descriptor())?;
134 if self.drivers.contains_key(&kind) {
135 return Err(ExternalAuthError::config_error(format!(
136 "external auth provider driver '{}' is already registered",
137 kind.as_str()
138 )));
139 }
140 self.drivers.insert(kind, driver);
141 Ok(())
142 }
143
144 pub fn register<D>(&mut self, driver: D) -> Result<()>
153 where
154 D: ExternalAuthProviderDriver + 'static,
155 {
156 self.register_arc(Arc::new(driver))
157 }
158
159 pub fn register_arc(&mut self, driver: Arc<dyn ExternalAuthProviderDriver>) -> Result<()> {
165 let kind = driver.kind();
166 Self::validate_driver_descriptor(kind, &driver.descriptor())?;
167 self.drivers.insert(kind, driver);
168 Ok(())
169 }
170
171 pub fn supported_kinds(&self) -> impl Iterator<Item = ExternalAuthProviderKind> + '_ {
173 self.drivers.keys().copied()
174 }
175
176 #[must_use]
178 pub fn contains(&self, kind: ExternalAuthProviderKind) -> bool {
179 self.drivers.contains_key(&kind)
180 }
181
182 #[must_use]
184 pub fn descriptors(&self) -> Vec<ExternalAuthProviderDescriptor> {
185 let mut descriptors = self
186 .drivers
187 .values()
188 .map(|driver| driver.descriptor())
189 .collect::<Vec<_>>();
190 descriptors.sort_by_key(|descriptor| descriptor.kind.as_str());
191 descriptors
192 }
193
194 pub fn descriptor_for(
200 &self,
201 kind: ExternalAuthProviderKind,
202 ) -> Result<ExternalAuthProviderDescriptor> {
203 Ok(self.get_driver(kind)?.descriptor())
204 }
205
206 pub fn ensure_provider_supported(&self, kind: ExternalAuthProviderKind) -> Result<()> {
215 if self.contains(kind) {
216 return Ok(());
217 }
218 Err(ExternalAuthError::config_error(format!(
219 "external auth provider driver '{}' is not registered",
220 kind.as_str()
221 )))
222 }
223
224 pub fn validate_provider_config(
235 &self,
236 provider: &ExternalAuthProviderConfig,
237 ) -> Result<ExternalAuthProviderDescriptor> {
238 let descriptor = self.descriptor_for(provider.provider_kind)?;
239 if provider.protocol != descriptor.protocol {
240 return Err(ExternalAuthError::validation_error(format!(
241 "external auth provider '{}' is configured with protocol '{}' but driver expects '{}'",
242 provider.provider_kind.as_str(),
243 provider.protocol.as_str(),
244 descriptor.protocol.as_str()
245 )));
246 }
247 Ok(descriptor)
248 }
249
250 pub fn driver_for_provider(
260 &self,
261 provider: &ExternalAuthProviderConfig,
262 ) -> Result<Arc<dyn ExternalAuthProviderDriver>> {
263 self.validate_provider_config(provider)?;
264 self.get_driver(provider.provider_kind)
265 }
266
267 pub fn get_driver(
273 &self,
274 kind: ExternalAuthProviderKind,
275 ) -> Result<Arc<dyn ExternalAuthProviderDriver>> {
276 self.drivers.get(&kind).cloned().ok_or_else(|| {
277 ExternalAuthError::config_error(format!(
278 "external auth provider driver '{}' is not registered",
279 kind.as_str()
280 ))
281 })
282 }
283
284 pub fn oidc(&self) -> Result<Arc<dyn ExternalAuthProviderDriver>> {
290 self.get_driver(ExternalAuthProviderKind::Oidc)
291 }
292
293 fn validate_driver_descriptor(
294 kind: ExternalAuthProviderKind,
295 descriptor: &ExternalAuthProviderDescriptor,
296 ) -> Result<()> {
297 if descriptor.kind == kind {
298 return Ok(());
299 }
300 Err(ExternalAuthError::config_error(format!(
301 "external auth provider driver '{}' returned descriptor for '{}'",
302 kind.as_str(),
303 descriptor.kind.as_str()
304 )))
305 }
306
307 #[cfg(any(
308 feature = "github",
309 feature = "google",
310 feature = "microsoft",
311 feature = "oauth2",
312 feature = "oidc",
313 feature = "qq"
314 ))]
315 fn register_builtin<D>(&mut self, driver: D)
316 where
317 D: ExternalAuthProviderDriver + 'static,
318 {
319 let kind = driver.kind();
320 self.drivers.insert(kind, Arc::new(driver));
321 }
322}
323
324impl Default for ExternalAuthProviderRegistry {
325 fn default() -> Self {
326 Self::new()
327 }
328}
329
330pub fn default_registry() -> &'static ExternalAuthProviderRegistry {
332 static REGISTRY: OnceLock<ExternalAuthProviderRegistry> = OnceLock::new();
333 REGISTRY.get_or_init(ExternalAuthProviderRegistry::new)
334}
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339 use crate::{
340 ExternalAuthAuthorizationStart, ExternalAuthCallback, ExternalAuthProfile,
341 ExternalAuthProviderConfig, ExternalAuthProviderTestResult,
342 };
343 use async_trait::async_trait;
344
345 #[derive(Default)]
346 struct TestOidcDriver;
347
348 #[async_trait]
349 impl ExternalAuthProviderDriver for TestOidcDriver {
350 fn kind(&self) -> ExternalAuthProviderKind {
351 ExternalAuthProviderKind::Oidc
352 }
353
354 fn descriptor(&self) -> ExternalAuthProviderDescriptor {
355 ExternalAuthProviderDescriptor {
356 kind: ExternalAuthProviderKind::Oidc,
357 protocol: crate::types::ExternalAuthProtocol::Oidc,
358 display_name: "Test OIDC",
359 description: "Test OIDC driver",
360 default_scopes: "openid email profile",
361 issuer_url_required: true,
362 manual_endpoint_configuration_supported: false,
363 authorization_url_required: false,
364 token_url_required: false,
365 userinfo_url_required: false,
366 supports_discovery: true,
367 supports_pkce: true,
368 supports_email_verified_claim: true,
369 }
370 }
371
372 async fn start_authorization(
373 &self,
374 _provider: &ExternalAuthProviderConfig,
375 _redirect_uri: &str,
376 ) -> Result<ExternalAuthAuthorizationStart> {
377 unreachable!("registry tests only inspect driver registration")
378 }
379
380 async fn exchange_callback(
381 &self,
382 _provider: &ExternalAuthProviderConfig,
383 _callback: ExternalAuthCallback,
384 ) -> Result<ExternalAuthProfile> {
385 unreachable!("registry tests only inspect driver registration")
386 }
387
388 async fn test_provider(
389 &self,
390 _provider: &ExternalAuthProviderConfig,
391 ) -> Result<ExternalAuthProviderTestResult> {
392 unreachable!("registry tests only inspect driver registration")
393 }
394 }
395
396 #[derive(Default)]
397 struct MismatchedDescriptorDriver;
398
399 #[async_trait]
400 impl ExternalAuthProviderDriver for MismatchedDescriptorDriver {
401 fn kind(&self) -> ExternalAuthProviderKind {
402 ExternalAuthProviderKind::Oidc
403 }
404
405 fn descriptor(&self) -> ExternalAuthProviderDescriptor {
406 ExternalAuthProviderDescriptor {
407 kind: ExternalAuthProviderKind::GenericOAuth2,
408 protocol: crate::types::ExternalAuthProtocol::OAuth2,
409 display_name: "Mismatched driver",
410 description: "Driver with inconsistent registry metadata",
411 default_scopes: "email",
412 issuer_url_required: false,
413 manual_endpoint_configuration_supported: true,
414 authorization_url_required: true,
415 token_url_required: true,
416 userinfo_url_required: true,
417 supports_discovery: false,
418 supports_pkce: true,
419 supports_email_verified_claim: false,
420 }
421 }
422
423 async fn start_authorization(
424 &self,
425 _provider: &ExternalAuthProviderConfig,
426 _redirect_uri: &str,
427 ) -> Result<ExternalAuthAuthorizationStart> {
428 unreachable!("registry tests only inspect driver registration")
429 }
430
431 async fn exchange_callback(
432 &self,
433 _provider: &ExternalAuthProviderConfig,
434 _callback: ExternalAuthCallback,
435 ) -> Result<ExternalAuthProfile> {
436 unreachable!("registry tests only inspect driver registration")
437 }
438
439 async fn test_provider(
440 &self,
441 _provider: &ExternalAuthProviderConfig,
442 ) -> Result<ExternalAuthProviderTestResult> {
443 unreachable!("registry tests only inspect driver registration")
444 }
445 }
446
447 fn oidc_provider_config() -> ExternalAuthProviderConfig {
448 ExternalAuthProviderConfig {
449 id: 1,
450 key: "test-oidc".to_string(),
451 provider_kind: ExternalAuthProviderKind::Oidc,
452 protocol: crate::types::ExternalAuthProtocol::Oidc,
453 options: crate::types::ExternalAuthProviderOptions::default(),
454 issuer_url: Some("https://issuer.example.com".to_string()),
455 authorization_url: None,
456 token_url: None,
457 userinfo_url: None,
458 client_id: "client-id".to_string(),
459 client_secret: Some("client-secret".to_string()),
460 scopes: "openid email profile".to_string(),
461 subject_claim: None,
462 username_claim: None,
463 display_name_claim: None,
464 email_claim: None,
465 email_verified_claim: None,
466 groups_claim: None,
467 avatar_url_claim: None,
468 outbound_http_user_agent: None,
469 }
470 }
471
472 #[cfg(feature = "oidc")]
473 #[test]
474 fn registry_returns_oidc_driver_by_kind() {
475 let registry = ExternalAuthProviderRegistry::new();
476 let driver = registry
477 .get_driver(ExternalAuthProviderKind::Oidc)
478 .expect("OIDC driver should be registered");
479
480 assert_eq!(driver.kind(), ExternalAuthProviderKind::Oidc);
481 }
482
483 #[test]
484 fn registry_allows_driver_replacement_by_kind() {
485 let mut registry = ExternalAuthProviderRegistry::new();
486 registry
487 .register(TestOidcDriver)
488 .expect("replacement driver should register");
489
490 assert!(registry.contains(ExternalAuthProviderKind::Oidc));
491 #[cfg(feature = "oauth2")]
492 assert!(registry.contains(ExternalAuthProviderKind::GenericOAuth2));
493 #[cfg(feature = "github")]
494 assert!(registry.contains(ExternalAuthProviderKind::GitHub));
495 #[cfg(feature = "google")]
496 assert!(registry.contains(ExternalAuthProviderKind::Google));
497 #[cfg(feature = "microsoft")]
498 assert!(registry.contains(ExternalAuthProviderKind::Microsoft));
499 #[cfg(feature = "qq")]
500 assert!(registry.contains(ExternalAuthProviderKind::Qq));
501 }
502
503 #[test]
504 fn registry_register_rejects_driver_descriptor_kind_mismatch() {
505 let mut registry = ExternalAuthProviderRegistry {
506 drivers: HashMap::new(),
507 };
508
509 let error = registry
510 .register(MismatchedDescriptorDriver)
511 .expect_err("mismatched descriptor should fail");
512
513 assert!(error.to_string().contains(
514 "external auth provider driver 'oidc' returned descriptor for 'generic_oauth2'"
515 ));
516 assert!(!registry.contains(ExternalAuthProviderKind::Oidc));
517 }
518
519 #[test]
520 fn registry_add_rejects_duplicate_kind_without_replacing_existing_driver() {
521 let mut registry = ExternalAuthProviderRegistry {
522 drivers: HashMap::new(),
523 };
524 registry
525 .add(TestOidcDriver)
526 .expect("initial add should work");
527
528 let error = registry
529 .add(TestOidcDriver)
530 .expect_err("duplicate add should fail");
531
532 assert!(
533 error
534 .to_string()
535 .contains("external auth provider driver 'oidc' is already registered")
536 );
537 }
538
539 #[test]
540 fn registry_add_rejects_driver_descriptor_kind_mismatch() {
541 let mut registry = ExternalAuthProviderRegistry {
542 drivers: HashMap::new(),
543 };
544
545 let error = registry
546 .add(MismatchedDescriptorDriver)
547 .expect_err("mismatched descriptor should fail");
548
549 assert!(error.to_string().contains(
550 "external auth provider driver 'oidc' returned descriptor for 'generic_oauth2'"
551 ));
552 assert!(!registry.contains(ExternalAuthProviderKind::Oidc));
553 }
554
555 #[test]
556 fn registry_descriptor_for_returns_registered_descriptor() {
557 let mut registry = ExternalAuthProviderRegistry {
558 drivers: HashMap::new(),
559 };
560 registry
561 .add(TestOidcDriver)
562 .expect("test driver should register");
563
564 let descriptor = registry
565 .descriptor_for(ExternalAuthProviderKind::Oidc)
566 .expect("descriptor should exist");
567
568 assert_eq!(descriptor.kind, ExternalAuthProviderKind::Oidc);
569 assert_eq!(
570 descriptor.protocol,
571 crate::types::ExternalAuthProtocol::Oidc
572 );
573 }
574
575 #[test]
576 fn registry_validate_provider_config_rejects_protocol_mismatch() {
577 let mut registry = ExternalAuthProviderRegistry {
578 drivers: HashMap::new(),
579 };
580 registry
581 .add(TestOidcDriver)
582 .expect("test driver should register");
583 let mut provider = oidc_provider_config();
584 provider.protocol = crate::types::ExternalAuthProtocol::OAuth2;
585
586 let error = registry
587 .validate_provider_config(&provider)
588 .expect_err("protocol mismatch should fail");
589
590 assert!(error.to_string().contains(
591 "external auth provider 'oidc' is configured with protocol 'oauth2' but driver expects 'oidc'"
592 ));
593 }
594
595 #[test]
596 fn registry_driver_for_provider_returns_validated_driver() {
597 let mut registry = ExternalAuthProviderRegistry {
598 drivers: HashMap::new(),
599 };
600 registry
601 .add(TestOidcDriver)
602 .expect("test driver should register");
603 let provider = oidc_provider_config();
604
605 let driver = registry
606 .driver_for_provider(&provider)
607 .expect("valid provider should resolve driver");
608
609 assert_eq!(driver.kind(), ExternalAuthProviderKind::Oidc);
610 }
611
612 #[test]
613 fn registry_with_external_registrations_exposes_configure_hook() {
614 let registry = ExternalAuthProviderRegistry::with_external_registrations(|registry| {
615 if registry.contains(ExternalAuthProviderKind::Oidc) {
616 Ok(())
617 } else {
618 registry.add(TestOidcDriver)
619 }
620 })
621 .expect("external registration hook should add driver");
622
623 assert!(registry.contains(ExternalAuthProviderKind::Oidc));
624 }
625
626 #[test]
627 fn default_registry_is_singleton() {
628 let first = std::ptr::from_ref::<ExternalAuthProviderRegistry>(default_registry());
629 let second = std::ptr::from_ref::<ExternalAuthProviderRegistry>(default_registry());
630
631 assert_eq!(first, second);
632 }
633}