From 559cbc10011b155a7477ca0f8b720d58fe42c001 Mon Sep 17 00:00:00 2001 From: Skyler Grey Date: Fri, 13 Mar 2026 09:23:42 +0000 Subject: [PATCH] feat: allow setting up SSO with environment This is particularly useful in nix to avoid having to put secrets in your configuration (or use some workaround) - but is probably quite useful in other environments too and is a nice part of allowing either environment or toml configuration. To do this, I've had to split up the providers so as to provide individual environment keys - I'm sure there's some clever macro that could do this in fewer lines and more complexity --- crates/tranquil-config/src/lib.rs | 241 +++++++++++++++++++++++------- example.toml | 22 +++ 2 files changed, 213 insertions(+), 50 deletions(-) diff --git a/crates/tranquil-config/src/lib.rs b/crates/tranquil-config/src/lib.rs index a5422b5..9d98d63 100644 --- a/crates/tranquil-config/src/lib.rs +++ b/crates/tranquil-config/src/lib.rs @@ -258,7 +258,7 @@ impl TranquilConfig { // -- SSO providers ---------------------------------------------------- self.validate_sso_provider("sso.github", &self.sso.github, &mut errors); self.validate_sso_provider("sso.google", &self.sso.google, &mut errors); - self.validate_sso_discord(&mut errors); + self.validate_sso_provider("sso.discord", &self.sso.discord, &mut errors); self.validate_sso_with_issuer("sso.gitlab", &self.sso.gitlab, &mut errors); self.validate_sso_with_issuer("sso.oidc", &self.sso.oidc, &mut errors); self.validate_sso_apple(&mut errors); @@ -300,14 +300,19 @@ impl TranquilConfig { } } - fn validate_sso_provider(&self, prefix: &str, p: &SsoProviderConfig, errors: &mut Vec) { - if p.enabled { - if p.client_id.is_none() { + fn validate_sso_provider( + &self, + prefix: &str, + p: &impl SsoProviderConfig, + errors: &mut Vec, + ) { + if p.get_enabled() { + if p.get_client_id().is_none() { errors.push(format!( "{prefix}.client_id is required when {prefix}.enabled = true" )); } - if p.client_secret.is_none() { + if p.get_client_secret().is_none() { errors.push(format!( "{prefix}.client_secret is required when {prefix}.enabled = true" )); @@ -315,41 +320,15 @@ impl TranquilConfig { } } - fn validate_sso_discord(&self, errors: &mut Vec) { - let p = &self.sso.discord; - if p.enabled { - if p.client_id.is_none() { - errors.push( - "sso.discord.client_id is required when sso.discord.enabled = true".to_string(), - ); - } - if p.client_secret.is_none() { - errors.push( - "sso.discord.client_secret is required when sso.discord.enabled = true" - .to_string(), - ); - } - } - } - fn validate_sso_with_issuer( &self, prefix: &str, - p: &SsoProviderWithIssuerConfig, + p: &(impl SsoProviderConfig + SsoProviderIssuerConfig), errors: &mut Vec, ) { - if p.enabled { - if p.client_id.is_none() { - errors.push(format!( - "{prefix}.client_id is required when {prefix}.enabled = true" - )); - } - if p.client_secret.is_none() { - errors.push(format!( - "{prefix}.client_secret is required when {prefix}.enabled = true" - )); - } - if p.issuer.is_none() { + self.validate_sso_provider(prefix, p, errors); + if p.get_enabled() { + if p.get_issuer().is_none() { errors.push(format!( "{prefix}.issuer is required when {prefix}.enabled = true" )); @@ -772,59 +751,221 @@ pub struct NotificationConfig { pub batch_size: i64, } +pub trait SsoProviderConfig { + fn get_enabled(&self) -> bool; + fn get_client_id(&self) -> &Option; + fn get_client_secret(&self) -> &Option; + fn get_display_name(&self) -> &Option; +} + +pub trait SsoProviderIssuerConfig { + fn get_issuer(&self) -> &Option; +} + #[derive(Debug, Config)] pub struct SsoConfig { #[config(nested)] - pub github: SsoProviderConfig, + pub github: SsoGitHubConfig, #[config(nested)] - pub discord: SsoDiscordProviderConfig, + pub discord: SsoDiscordConfig, #[config(nested)] - pub google: SsoProviderConfig, + pub google: SsoGoogleConfig, #[config(nested)] - pub gitlab: SsoProviderWithIssuerConfig, + pub gitlab: SsoGitLabConfig, #[config(nested)] - pub oidc: SsoProviderWithIssuerConfig, + pub oidc: SsoOidcConfig, #[config(nested)] pub apple: SsoAppleConfig, } -// Generic SSO provider (GitHub, Google) #[derive(Debug, Config)] -pub struct SsoProviderConfig { - #[config(default = false)] +pub struct SsoGitHubConfig { + #[config(env = "SSO_GITHUB_ENABLED", default = false)] pub enabled: bool, + + #[config(env = "SSO_GITHUB_CLIENT_ID")] pub client_id: Option, + + #[config(env = "SSO_GITHUB_CLIENT_SECRET")] pub client_secret: Option, + + #[config(env = "SSO_GITHUB_DISPLAY_NAME")] pub display_name: Option, } -// SSO provider with custom env prefixes for Discord -// (since the nested TOML key is `sso.discord` but env vars are `SSO_DISCORD_*`) +impl SsoProviderConfig for SsoGitHubConfig { + fn get_enabled(&self) -> bool { + self.enabled + } + + fn get_client_id(&self) -> &Option { + &self.client_id + } + + fn get_client_secret(&self) -> &Option { + &self.client_secret + } + + fn get_display_name(&self) -> &Option { + &self.display_name + } +} + #[derive(Debug, Config)] -pub struct SsoDiscordProviderConfig { - #[config(default = false)] +pub struct SsoDiscordConfig { + #[config(env = "SSO_DISCORD_ENABLED", default = false)] pub enabled: bool, + + #[config(env = "SSO_DISCORD_CLIENT_ID")] pub client_id: Option, + + #[config(env = "SSO_DISCORD_CLIENT_SECRET")] pub client_secret: Option, + + #[config(env = "SSO_DISCORD_DISPLAY_NAME")] pub display_name: Option, } -// SSO providers that require an issuer URL (GitLab, OIDC) +impl SsoProviderConfig for SsoDiscordConfig { + fn get_enabled(&self) -> bool { + self.enabled + } + + fn get_client_id(&self) -> &Option { + &self.client_id + } + + fn get_client_secret(&self) -> &Option { + &self.client_secret + } + + fn get_display_name(&self) -> &Option { + &self.display_name + } +} + #[derive(Debug, Config)] -pub struct SsoProviderWithIssuerConfig { - #[config(default = false)] +pub struct SsoGoogleConfig { + #[config(env = "SSO_GOOGLE_ENABLED", default = false)] pub enabled: bool, + + #[config(env = "SSO_GOOGLE_CLIENT_ID")] pub client_id: Option, + + #[config(env = "SSO_GOOGLE_CLIENT_SECRET")] pub client_secret: Option, + + #[config(env = "SSO_GOOGLE_DISPLAY_NAME")] + pub display_name: Option, +} + +impl SsoProviderConfig for SsoGoogleConfig { + fn get_enabled(&self) -> bool { + self.enabled + } + + fn get_client_id(&self) -> &Option { + &self.client_id + } + + fn get_client_secret(&self) -> &Option { + &self.client_secret + } + + fn get_display_name(&self) -> &Option { + &self.display_name + } +} + +#[derive(Debug, Config)] +pub struct SsoGitLabConfig { + #[config(env = "SSO_GITLAB_ENABLED", default = false)] + pub enabled: bool, + + #[config(env = "SSO_GITLAB_CLIENT_ID")] + pub client_id: Option, + + #[config(env = "SSO_GITLAB_CLIENT_SECRET")] + pub client_secret: Option, + + #[config(env = "SSO_GITLAB_ISSUER")] pub issuer: Option, + + #[config(env = "SSO_GITLAB_DISPLAY_NAME")] pub display_name: Option, } +impl SsoProviderConfig for SsoGitLabConfig { + fn get_enabled(&self) -> bool { + self.enabled + } + + fn get_client_id(&self) -> &Option { + &self.client_id + } + + fn get_client_secret(&self) -> &Option { + &self.client_secret + } + + fn get_display_name(&self) -> &Option { + &self.display_name + } +} + +impl SsoProviderIssuerConfig for SsoGitLabConfig { + fn get_issuer(&self) -> &Option { + &self.issuer + } +} + +#[derive(Debug, Config)] +pub struct SsoOidcConfig { + #[config(env = "SSO_OIDC_ENABLED", default = false)] + pub enabled: bool, + + #[config(env = "SSO_OIDC_CLIENT_ID")] + pub client_id: Option, + + #[config(env = "SSO_OIDC_CLIENT_SECRET")] + pub client_secret: Option, + + #[config(env = "SSO_OIDC_ISSUER")] + pub issuer: Option, + + #[config(env = "SSO_OIDC_DISPLAY_NAME")] + pub display_name: Option, +} + +impl SsoProviderConfig for SsoOidcConfig { + fn get_enabled(&self) -> bool { + self.enabled + } + + fn get_client_id(&self) -> &Option { + &self.client_id + } + + fn get_client_secret(&self) -> &Option { + &self.client_secret + } + + fn get_display_name(&self) -> &Option { + &self.display_name + } +} + +impl SsoProviderIssuerConfig for SsoOidcConfig { + fn get_issuer(&self) -> &Option { + &self.issuer + } +} + #[derive(Debug, Config)] pub struct SsoAppleConfig { #[config(env = "SSO_APPLE_ENABLED", default = false)] diff --git a/example.toml b/example.toml index d2b07d6..34a4078 100644 --- a/example.toml +++ b/example.toml @@ -390,57 +390,79 @@ [sso] [sso.github] +# Can also be specified via environment variable `SSO_GITHUB_ENABLED`. # Default value: false #enabled = false +# Can also be specified via environment variable `SSO_GITHUB_CLIENT_ID`. #client_id = +# Can also be specified via environment variable `SSO_GITHUB_CLIENT_SECRET`. #client_secret = +# Can also be specified via environment variable `SSO_GITHUB_DISPLAY_NAME`. #display_name = [sso.discord] +# Can also be specified via environment variable `SSO_DISCORD_ENABLED`. # Default value: false #enabled = false +# Can also be specified via environment variable `SSO_DISCORD_CLIENT_ID`. #client_id = +# Can also be specified via environment variable `SSO_DISCORD_CLIENT_SECRET`. #client_secret = +# Can also be specified via environment variable `SSO_DISCORD_DISPLAY_NAME`. #display_name = [sso.google] +# Can also be specified via environment variable `SSO_GOOGLE_ENABLED`. # Default value: false #enabled = false +# Can also be specified via environment variable `SSO_GOOGLE_CLIENT_ID`. #client_id = +# Can also be specified via environment variable `SSO_GOOGLE_CLIENT_SECRET`. #client_secret = +# Can also be specified via environment variable `SSO_GOOGLE_DISPLAY_NAME`. #display_name = [sso.gitlab] +# Can also be specified via environment variable `SSO_GITLAB_ENABLED`. # Default value: false #enabled = false +# Can also be specified via environment variable `SSO_GITLAB_CLIENT_ID`. #client_id = +# Can also be specified via environment variable `SSO_GITLAB_CLIENT_SECRET`. #client_secret = +# Can also be specified via environment variable `SSO_GITLAB_ISSUER`. #issuer = +# Can also be specified via environment variable `SSO_GITLAB_DISPLAY_NAME`. #display_name = [sso.oidc] +# Can also be specified via environment variable `SSO_OIDC_ENABLED`. # Default value: false #enabled = false +# Can also be specified via environment variable `SSO_OIDC_CLIENT_ID`. #client_id = +# Can also be specified via environment variable `SSO_OIDC_CLIENT_SECRET`. #client_secret = +# Can also be specified via environment variable `SSO_OIDC_ISSUER`. #issuer = +# Can also be specified via environment variable `SSO_OIDC_DISPLAY_NAME`. #display_name = [sso.apple]