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
This commit is contained in:
Skyler Grey
2026-03-13 10:10:46 +00:00
committed by Tangled
parent 18cdb612f6
commit 559cbc1001
2 changed files with 213 additions and 50 deletions
+191 -50
View File
@@ -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<String>) {
if p.enabled {
if p.client_id.is_none() {
fn validate_sso_provider(
&self,
prefix: &str,
p: &impl SsoProviderConfig,
errors: &mut Vec<String>,
) {
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<String>) {
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<String>,
) {
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<String>;
fn get_client_secret(&self) -> &Option<String>;
fn get_display_name(&self) -> &Option<String>;
}
pub trait SsoProviderIssuerConfig {
fn get_issuer(&self) -> &Option<String>;
}
#[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<String>,
#[config(env = "SSO_GITHUB_CLIENT_SECRET")]
pub client_secret: Option<String>,
#[config(env = "SSO_GITHUB_DISPLAY_NAME")]
pub display_name: Option<String>,
}
// 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<String> {
&self.client_id
}
fn get_client_secret(&self) -> &Option<String> {
&self.client_secret
}
fn get_display_name(&self) -> &Option<String> {
&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<String>,
#[config(env = "SSO_DISCORD_CLIENT_SECRET")]
pub client_secret: Option<String>,
#[config(env = "SSO_DISCORD_DISPLAY_NAME")]
pub display_name: Option<String>,
}
// 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<String> {
&self.client_id
}
fn get_client_secret(&self) -> &Option<String> {
&self.client_secret
}
fn get_display_name(&self) -> &Option<String> {
&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<String>,
#[config(env = "SSO_GOOGLE_CLIENT_SECRET")]
pub client_secret: Option<String>,
#[config(env = "SSO_GOOGLE_DISPLAY_NAME")]
pub display_name: Option<String>,
}
impl SsoProviderConfig for SsoGoogleConfig {
fn get_enabled(&self) -> bool {
self.enabled
}
fn get_client_id(&self) -> &Option<String> {
&self.client_id
}
fn get_client_secret(&self) -> &Option<String> {
&self.client_secret
}
fn get_display_name(&self) -> &Option<String> {
&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<String>,
#[config(env = "SSO_GITLAB_CLIENT_SECRET")]
pub client_secret: Option<String>,
#[config(env = "SSO_GITLAB_ISSUER")]
pub issuer: Option<String>,
#[config(env = "SSO_GITLAB_DISPLAY_NAME")]
pub display_name: Option<String>,
}
impl SsoProviderConfig for SsoGitLabConfig {
fn get_enabled(&self) -> bool {
self.enabled
}
fn get_client_id(&self) -> &Option<String> {
&self.client_id
}
fn get_client_secret(&self) -> &Option<String> {
&self.client_secret
}
fn get_display_name(&self) -> &Option<String> {
&self.display_name
}
}
impl SsoProviderIssuerConfig for SsoGitLabConfig {
fn get_issuer(&self) -> &Option<String> {
&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<String>,
#[config(env = "SSO_OIDC_CLIENT_SECRET")]
pub client_secret: Option<String>,
#[config(env = "SSO_OIDC_ISSUER")]
pub issuer: Option<String>,
#[config(env = "SSO_OIDC_DISPLAY_NAME")]
pub display_name: Option<String>,
}
impl SsoProviderConfig for SsoOidcConfig {
fn get_enabled(&self) -> bool {
self.enabled
}
fn get_client_id(&self) -> &Option<String> {
&self.client_id
}
fn get_client_secret(&self) -> &Option<String> {
&self.client_secret
}
fn get_display_name(&self) -> &Option<String> {
&self.display_name
}
}
impl SsoProviderIssuerConfig for SsoOidcConfig {
fn get_issuer(&self) -> &Option<String> {
&self.issuer
}
}
#[derive(Debug, Config)]
pub struct SsoAppleConfig {
#[config(env = "SSO_APPLE_ENABLED", default = false)]
+22
View File
@@ -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]