diff --git a/auth/server/src/api/login/local.rs b/auth/server/src/api/login/local.rs index 289a630..24cddf5 100644 --- a/auth/server/src/api/login/local.rs +++ b/auth/server/src/api/login/local.rs @@ -24,7 +24,7 @@ pub async fn sign_up_local_user( let no_users_exist = auth.no_users_exist().await?; - if auth.registration_disabled() && !no_users_exist { + if auth.local_registration_disabled() && !no_users_exist { return Err( anyhow!("User registration is disabled") .status_code(StatusCode::UNAUTHORIZED), diff --git a/auth/server/src/api/login/mod.rs b/auth/server/src/api/login/mod.rs index 31541cf..c069b23 100644 --- a/auth/server/src/api/login/mod.rs +++ b/auth/server/src/api/login/mod.rs @@ -115,7 +115,7 @@ pub fn get_login_options( .google_config() .map(|config| config.enabled()) .unwrap_or_default(), - registration_disabled: auth.registration_disabled(), + registration_disabled: auth.local_registration_disabled(), } } @@ -128,6 +128,170 @@ impl Resolve for GetLoginOptions { } } +#[cfg(test)] +mod tests { + use super::*; + use crate::AuthImpl; + use mogh_auth_client::config::OidcConfig; + + /// Minimal AuthImpl for testing + struct TestAuth { + local: bool, + oidc: Option, + registration_disabled: bool, + local_registration_disabled: Option, + oidc_registration_disabled: Option, + } + + impl TestAuth { + fn default_test() -> Self { + Self { + local: true, + oidc: None, + registration_disabled: false, + local_registration_disabled: None, + oidc_registration_disabled: None, + } + } + } + + impl AuthImpl for TestAuth { + fn new() -> Self { + Self::default_test() + } + + fn local_auth_enabled(&self) -> bool { + self.local + } + + fn oidc_config(&self) -> Option<&OidcConfig> { + self.oidc.as_ref() + } + + fn registration_disabled(&self) -> bool { + self.registration_disabled + } + + fn local_registration_disabled(&self) -> bool { + self + .local_registration_disabled + .unwrap_or_else(|| self.registration_disabled()) + } + + fn oidc_registration_disabled(&self) -> bool { + self + .oidc_registration_disabled + .unwrap_or_else(|| self.registration_disabled()) + } + + fn get_user( + &self, + _user_id: String, + ) -> crate::DynFuture> + { + Box::pin(async { + Err(anyhow::anyhow!("not implemented").into()) + }) + } + + fn handle_request_authentication( + &self, + _auth: crate::RequestAuthentication, + _require_user_enabled: bool, + _req: axum::extract::Request, + ) -> crate::DynFuture> + { + Box::pin(async { + Err(anyhow::anyhow!("not implemented").into()) + }) + } + + fn jwt_provider( + &self, + ) -> &crate::provider::jwt::JwtProvider { + panic!("not needed for these tests") + } + } + + #[test] + fn test_default_granular_methods_delegate_to_registration_disabled() + { + // When granular overrides are None, they should + // fall back to the global registration_disabled flag. + let auth = TestAuth { + registration_disabled: true, + ..TestAuth::default_test() + }; + assert!(auth.local_registration_disabled()); + assert!(auth.oidc_registration_disabled()); + assert!(auth.github_registration_disabled()); + assert!(auth.google_registration_disabled()); + } + + #[test] + fn test_global_disabled_local_override_enabled() { + // Global registration disabled, but local override allows it + let auth = TestAuth { + registration_disabled: true, + local_registration_disabled: Some(false), + ..TestAuth::default_test() + }; + assert!(!auth.local_registration_disabled()); + assert!(auth.oidc_registration_disabled()); + } + + #[test] + fn test_global_enabled_local_override_disabled() { + // Global registration enabled, but local override blocks it + let auth = TestAuth { + registration_disabled: false, + local_registration_disabled: Some(true), + ..TestAuth::default_test() + }; + assert!(auth.local_registration_disabled()); + assert!(!auth.oidc_registration_disabled()); + } + + #[test] + fn test_disable_local_allow_oidc() { + // The #1087 use case: disable local signup, allow OIDC + let auth = TestAuth { + registration_disabled: false, + local_registration_disabled: Some(true), + oidc_registration_disabled: Some(false), + ..TestAuth::default_test() + }; + assert!(auth.local_registration_disabled()); + assert!(!auth.oidc_registration_disabled()); + } + + #[test] + fn test_registration_disabled_reflects_local_in_login_options() { + // registration_disabled in the response controls the Sign Up button, + // which is local-only. It should reflect local_registration_disabled. + let auth = TestAuth { + registration_disabled: false, + local_registration_disabled: Some(true), + oidc_registration_disabled: Some(false), + ..TestAuth::default_test() + }; + let opts = get_login_options(&auth); + assert!(opts.registration_disabled); + } + + #[test] + fn test_registration_disabled_false_when_local_allowed() { + let auth = TestAuth { + registration_disabled: true, + local_registration_disabled: Some(false), + oidc_registration_disabled: Some(true), + ..TestAuth::default_test() + }; + let opts = get_login_options(&auth); + assert!(!opts.registration_disabled); + } +} + impl Resolve for ExchangeForJwt { #[instrument("ExchangeForJwt", skip_all, fields(ip = ip.to_string()))] async fn resolve( diff --git a/auth/server/src/api/named/github.rs b/auth/server/src/api/named/github.rs index 6f0e4d9..42dcf3d 100644 --- a/auth/server/src/api/named/github.rs +++ b/auth/server/src/api/named/github.rs @@ -174,7 +174,7 @@ pub async fn github_callback( None => { let no_users_exist = auth.no_users_exist().await?; - if auth.registration_disabled() && !no_users_exist { + if auth.github_registration_disabled() && !no_users_exist { return Err( anyhow!("User registration is disabled") .status_code(StatusCode::UNAUTHORIZED), diff --git a/auth/server/src/api/named/google.rs b/auth/server/src/api/named/google.rs index d0e9157..f78bf63 100644 --- a/auth/server/src/api/named/google.rs +++ b/auth/server/src/api/named/google.rs @@ -172,7 +172,7 @@ pub async fn google_callback( None => { let no_users_exist = auth.no_users_exist().await?; - if auth.registration_disabled() && !no_users_exist { + if auth.google_registration_disabled() && !no_users_exist { return Err( anyhow!("User registration is disabled") .status_code(StatusCode::UNAUTHORIZED), diff --git a/auth/server/src/api/oidc.rs b/auth/server/src/api/oidc.rs index 74509c4..b5b2207 100644 --- a/auth/server/src/api/oidc.rs +++ b/auth/server/src/api/oidc.rs @@ -243,7 +243,7 @@ pub async fn oidc_callback( None => { let no_users_exist = auth.no_users_exist().await?; - if auth.registration_disabled() && !no_users_exist { + if auth.oidc_registration_disabled() && !no_users_exist { return Err( anyhow!("User registration is disabled") .status_code(StatusCode::UNAUTHORIZED), diff --git a/auth/server/src/lib.rs b/auth/server/src/lib.rs index 417c4ec..7f57729 100644 --- a/auth/server/src/lib.rs +++ b/auth/server/src/lib.rs @@ -79,11 +79,35 @@ pub trait AuthImpl: Send + Sync + 'static { "/auth" } - /// Disable new user registration. + /// Disable new user registration (all providers). fn registration_disabled(&self) -> bool { false } + /// Disable new user registration for local (username/password) signups only. + /// Defaults to [Self::registration_disabled]. + fn local_registration_disabled(&self) -> bool { + self.registration_disabled() + } + + /// Disable new user registration via OIDC only. + /// Defaults to [Self::registration_disabled]. + fn oidc_registration_disabled(&self) -> bool { + self.registration_disabled() + } + + /// Disable new user registration via GitHub only. + /// Defaults to [Self::registration_disabled]. + fn github_registration_disabled(&self) -> bool { + self.registration_disabled() + } + + /// Disable new user registration via Google only. + /// Defaults to [Self::registration_disabled]. + fn google_registration_disabled(&self) -> bool { + self.registration_disabled() + } + /// Provide usernames to lock credential updates for, /// such as demo users. fn locked_usernames(&self) -> &'static [String] {