diff --git a/Cargo.lock b/Cargo.lock index 40c97f349..fa099f357 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3084,7 +3084,6 @@ name = "mas-config" version = "0.8.0" dependencies = [ "anyhow", - "async-trait", "camino", "chrono", "figment", diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml index 6131971e1..183717d4f 100644 --- a/crates/config/Cargo.toml +++ b/crates/config/Cargo.toml @@ -14,7 +14,6 @@ workspace = true [dependencies] tokio = { version = "1.36.0", features = ["fs", "rt"] } tracing.workspace = true -async-trait.workspace = true thiserror.workspace = true anyhow.workspace = true diff --git a/crates/config/src/sections/branding.rs b/crates/config/src/sections/branding.rs index 5bdc79a54..f9cbc2eeb 100644 --- a/crates/config/src/sections/branding.rs +++ b/crates/config/src/sections/branding.rs @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -use async_trait::async_trait; -use rand::Rng; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use url::Url; @@ -24,38 +22,42 @@ use crate::ConfigurationSection; #[derive(Clone, Debug, Deserialize, JsonSchema, Serialize, Default)] pub struct BrandingConfig { /// A human-readable name. Defaults to the server's address. + #[serde(skip_serializing_if = "Option::is_none")] pub service_name: Option, /// Link to a privacy policy, displayed in the footer of web pages and /// emails. It is also advertised to clients through the `op_policy_uri` /// OIDC provider metadata. + #[serde(skip_serializing_if = "Option::is_none")] pub policy_uri: Option, /// Link to a terms of service document, displayed in the footer of web /// pages and emails. It is also advertised to clients through the /// `op_tos_uri` OIDC provider metadata. + #[serde(skip_serializing_if = "Option::is_none")] pub tos_uri: Option, /// Legal imprint, displayed in the footer in the footer of web pages and /// emails. + #[serde(skip_serializing_if = "Option::is_none")] pub imprint: Option, /// Logo displayed in some web pages. + #[serde(skip_serializing_if = "Option::is_none")] pub logo_uri: Option, } -#[async_trait] -impl ConfigurationSection for BrandingConfig { - const PATH: Option<&'static str> = Some("branding"); - - async fn generate(_rng: R) -> anyhow::Result - where - R: Rng + Send, - { - Ok(Self::default()) +impl BrandingConfig { + /// Returns true if the configuration is the default one + pub(crate) fn is_default(&self) -> bool { + self.service_name.is_none() + && self.policy_uri.is_none() + && self.tos_uri.is_none() + && self.imprint.is_none() + && self.logo_uri.is_none() } +} - fn test() -> Self { - Self::default() - } +impl ConfigurationSection for BrandingConfig { + const PATH: Option<&'static str> = Some("branding"); } diff --git a/crates/config/src/sections/clients.rs b/crates/config/src/sections/clients.rs index 18d9ab76f..14f07a967 100644 --- a/crates/config/src/sections/clients.rs +++ b/crates/config/src/sections/clients.rs @@ -14,11 +14,9 @@ use std::ops::Deref; -use async_trait::async_trait; use figment::Figment; use mas_iana::oauth::OAuthClientAuthenticationMethod; use mas_jose::jwk::PublicJsonWebKeySet; -use rand::Rng; use schemars::JsonSchema; use serde::{de::Error, Deserialize, Serialize}; use ulid::Ulid; @@ -211,6 +209,13 @@ impl ClientConfig { #[serde(transparent)] pub struct ClientsConfig(#[schemars(with = "Vec::")] Vec); +impl ClientsConfig { + /// Returns true if all fields are at their default values + pub(crate) fn is_default(&self) -> bool { + self.0.is_empty() + } +} + impl Deref for ClientsConfig { type Target = Vec; @@ -228,17 +233,9 @@ impl IntoIterator for ClientsConfig { } } -#[async_trait] impl ConfigurationSection for ClientsConfig { const PATH: Option<&'static str> = Some("clients"); - async fn generate(_rng: R) -> anyhow::Result - where - R: Rng + Send, - { - Ok(Self::default()) - } - fn validate(&self, figment: &Figment) -> Result<(), figment::error::Error> { for (index, client) in self.0.iter().enumerate() { client.validate().map_err(|mut err| { @@ -253,10 +250,6 @@ impl ConfigurationSection for ClientsConfig { Ok(()) } - - fn test() -> Self { - Self::default() - } } #[cfg(test)] diff --git a/crates/config/src/sections/database.rs b/crates/config/src/sections/database.rs index beb82ae21..4b171131a 100644 --- a/crates/config/src/sections/database.rs +++ b/crates/config/src/sections/database.rs @@ -14,9 +14,7 @@ use std::{num::NonZeroU32, time::Duration}; -use async_trait::async_trait; use camino::Utf8PathBuf; -use rand::Rng; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_with::serde_as; @@ -150,17 +148,9 @@ pub struct DatabaseConfig { pub max_lifetime: Option, } -#[async_trait] impl ConfigurationSection for DatabaseConfig { const PATH: Option<&'static str> = Some("database"); - async fn generate(_rng: R) -> anyhow::Result - where - R: Rng + Send, - { - Ok(Self::default()) - } - fn validate(&self, figment: &figment::Figment) -> Result<(), figment::error::Error> { let metadata = figment.find_metadata(Self::PATH.unwrap()); @@ -185,10 +175,6 @@ impl ConfigurationSection for DatabaseConfig { Ok(()) } - - fn test() -> Self { - Self::default() - } } #[cfg(test)] diff --git a/crates/config/src/sections/email.rs b/crates/config/src/sections/email.rs index f67fdaa20..274d6c916 100644 --- a/crates/config/src/sections/email.rs +++ b/crates/config/src/sections/email.rs @@ -16,8 +16,6 @@ use std::num::NonZeroU16; -use async_trait::async_trait; -use rand::Rng; use schemars::JsonSchema; use serde::{de::Error, Deserialize, Serialize}; @@ -181,17 +179,9 @@ impl Default for EmailConfig { } } -#[async_trait] impl ConfigurationSection for EmailConfig { const PATH: Option<&'static str> = Some("email"); - async fn generate(_rng: R) -> anyhow::Result - where - R: Rng + Send, - { - Ok(Self::default()) - } - fn validate(&self, figment: &figment::Figment) -> Result<(), figment::error::Error> { let metadata = figment.find_metadata(Self::PATH.unwrap()); @@ -283,8 +273,4 @@ impl ConfigurationSection for EmailConfig { Ok(()) } - - fn test() -> Self { - Self::default() - } } diff --git a/crates/config/src/sections/experimental.rs b/crates/config/src/sections/experimental.rs index c98a435cd..ad1b5eb4c 100644 --- a/crates/config/src/sections/experimental.rs +++ b/crates/config/src/sections/experimental.rs @@ -12,9 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use async_trait::async_trait; use chrono::Duration; -use rand::Rng; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_with::serde_as; @@ -25,6 +23,10 @@ fn default_token_ttl() -> Duration { Duration::microseconds(5 * 60 * 1000 * 1000) } +fn is_default_token_ttl(value: &Duration) -> bool { + *value == default_token_ttl() +} + /// Configuration sections for experimental options /// /// Do not change these options unless you know what you are doing. @@ -33,14 +35,20 @@ fn default_token_ttl() -> Duration { pub struct ExperimentalConfig { /// Time-to-live of access tokens in seconds. Defaults to 5 minutes. #[schemars(with = "u64", range(min = 60, max = 86400))] - #[serde(default = "default_token_ttl")] + #[serde( + default = "default_token_ttl", + skip_serializing_if = "is_default_token_ttl" + )] #[serde_as(as = "serde_with::DurationSeconds")] pub access_token_ttl: Duration, /// Time-to-live of compatibility access tokens in seconds. Defaults to 5 /// minutes. #[schemars(with = "u64", range(min = 60, max = 86400))] - #[serde(default = "default_token_ttl")] + #[serde( + default = "default_token_ttl", + skip_serializing_if = "is_default_token_ttl" + )] #[serde_as(as = "serde_with::DurationSeconds")] pub compat_token_ttl: Duration, } @@ -54,18 +62,12 @@ impl Default for ExperimentalConfig { } } -#[async_trait] -impl ConfigurationSection for ExperimentalConfig { - const PATH: Option<&'static str> = Some("experimental"); - - async fn generate(_rng: R) -> anyhow::Result - where - R: Rng + Send, - { - Ok(Self::default()) +impl ExperimentalConfig { + pub(crate) fn is_default(&self) -> bool { + is_default_token_ttl(&self.access_token_ttl) && is_default_token_ttl(&self.compat_token_ttl) } +} - fn test() -> Self { - Self::default() - } +impl ConfigurationSection for ExperimentalConfig { + const PATH: Option<&'static str> = Some("experimental"); } diff --git a/crates/config/src/sections/http.rs b/crates/config/src/sections/http.rs index 90c8b198c..f60d1b5c8 100644 --- a/crates/config/src/sections/http.rs +++ b/crates/config/src/sections/http.rs @@ -17,11 +17,9 @@ use std::{borrow::Cow, io::Cursor}; use anyhow::bail; -use async_trait::async_trait; use camino::Utf8PathBuf; use ipnetwork::IpNetwork; use mas_keystore::PrivateKey; -use rand::Rng; use rustls_pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -61,6 +59,10 @@ fn http_listener_assets_path_default() -> Utf8PathBuf { "./share/assets/".into() } +fn is_default_http_listener_assets_path(value: &Utf8PathBuf) -> bool { + *value == http_listener_assets_path_default() +} + fn default_trusted_proxies() -> Vec { vec![ IpNetwork::new([192, 128, 0, 0].into(), 16).unwrap(), @@ -302,7 +304,10 @@ pub enum Resource { /// Static files Assets { /// Path to the directory to serve. - #[serde(default = "http_listener_assets_path_default")] + #[serde( + default = "http_listener_assets_path_default", + skip_serializing_if = "is_default_http_listener_assets_path" + )] #[schemars(with = "String")] path: Utf8PathBuf, }, @@ -356,6 +361,7 @@ pub struct HttpConfig { pub public_base: Url, /// OIDC issuer URL. Defaults to `public_base` if not set. + #[serde(skip_serializing_if = "Option::is_none")] pub issuer: Option, } @@ -401,17 +407,9 @@ impl Default for HttpConfig { } } -#[async_trait] impl ConfigurationSection for HttpConfig { const PATH: Option<&'static str> = Some("http"); - async fn generate(_rng: R) -> anyhow::Result - where - R: Rng + Send, - { - Ok(Self::default()) - } - fn validate(&self, figment: &figment::Figment) -> Result<(), figment::Error> { for (index, listener) in self.listeners.iter().enumerate() { let annotate = |mut error: figment::Error| { @@ -473,8 +471,4 @@ impl ConfigurationSection for HttpConfig { Ok(()) } - - fn test() -> Self { - Self::default() - } } diff --git a/crates/config/src/sections/matrix.rs b/crates/config/src/sections/matrix.rs index 465a70cfa..031b30ca2 100644 --- a/crates/config/src/sections/matrix.rs +++ b/crates/config/src/sections/matrix.rs @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -use async_trait::async_trait; use rand::{ distributions::{Alphanumeric, DistString}, Rng, @@ -48,22 +47,23 @@ pub struct MatrixConfig { pub endpoint: Url, } -#[async_trait] impl ConfigurationSection for MatrixConfig { const PATH: Option<&'static str> = Some("matrix"); +} - async fn generate(mut rng: R) -> anyhow::Result +impl MatrixConfig { + pub(crate) fn generate(mut rng: R) -> Self where R: Rng + Send, { - Ok(Self { + Self { homeserver: default_homeserver(), secret: Alphanumeric.sample_string(&mut rng, 32), endpoint: default_endpoint(), - }) + } } - fn test() -> Self { + pub(crate) fn test() -> Self { Self { homeserver: default_homeserver(), secret: "test".to_owned(), diff --git a/crates/config/src/sections/mod.rs b/crates/config/src/sections/mod.rs index 5c69d1ac1..17bbb8ad8 100644 --- a/crates/config/src/sections/mod.rs +++ b/crates/config/src/sections/mod.rs @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -use async_trait::async_trait; use rand::Rng; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -63,7 +62,7 @@ use crate::util::ConfigurationSection; #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct RootConfig { /// List of OAuth 2.0/OIDC clients config - #[serde(default)] + #[serde(default, skip_serializing_if = "ClientsConfig::is_default")] pub clients: ClientsConfig, /// Configuration of the HTTP server @@ -75,11 +74,11 @@ pub struct RootConfig { pub database: DatabaseConfig, /// Configuration related to sending monitoring data - #[serde(default)] + #[serde(default, skip_serializing_if = "TelemetryConfig::is_default")] pub telemetry: TelemetryConfig, /// Configuration related to templates - #[serde(default)] + #[serde(default, skip_serializing_if = "TemplatesConfig::is_default")] pub templates: TemplatesConfig, /// Configuration related to sending emails @@ -97,46 +96,24 @@ pub struct RootConfig { pub matrix: MatrixConfig, /// Configuration related to the OPA policies - #[serde(default)] + #[serde(default, skip_serializing_if = "PolicyConfig::is_default")] pub policy: PolicyConfig, /// Configuration related to upstream OAuth providers - #[serde(default)] + #[serde(default, skip_serializing_if = "UpstreamOAuth2Config::is_default")] pub upstream_oauth2: UpstreamOAuth2Config, /// Configuration section for tweaking the branding of the service - #[serde(default)] + #[serde(default, skip_serializing_if = "BrandingConfig::is_default")] pub branding: BrandingConfig, /// Experimental configuration options - #[serde(default)] + #[serde(default, skip_serializing_if = "ExperimentalConfig::is_default")] pub experimental: ExperimentalConfig, } -#[async_trait] impl ConfigurationSection for RootConfig { - async fn generate(mut rng: R) -> anyhow::Result - where - R: Rng + Send, - { - Ok(Self { - clients: ClientsConfig::generate(&mut rng).await?, - http: HttpConfig::generate(&mut rng).await?, - database: DatabaseConfig::generate(&mut rng).await?, - telemetry: TelemetryConfig::generate(&mut rng).await?, - templates: TemplatesConfig::generate(&mut rng).await?, - email: EmailConfig::generate(&mut rng).await?, - passwords: PasswordsConfig::generate(&mut rng).await?, - secrets: SecretsConfig::generate(&mut rng).await?, - matrix: MatrixConfig::generate(&mut rng).await?, - policy: PolicyConfig::generate(&mut rng).await?, - upstream_oauth2: UpstreamOAuth2Config::generate(&mut rng).await?, - branding: BrandingConfig::generate(&mut rng).await?, - experimental: ExperimentalConfig::generate(&mut rng).await?, - }) - } - - fn validate(&self, figment: &figment::Figment) -> Result<(), figment::error::Error> { + fn validate(&self, figment: &figment::Figment) -> Result<(), figment::Error> { self.clients.validate(figment)?; self.http.validate(figment)?; self.database.validate(figment)?; @@ -153,29 +130,59 @@ impl ConfigurationSection for RootConfig { Ok(()) } +} - fn test() -> Self { +impl RootConfig { + /// Generate a new configuration with random secrets + /// + /// # Errors + /// + /// Returns an error if the secrets could not be generated + pub async fn generate(mut rng: R) -> anyhow::Result + where + R: Rng + Send, + { + Ok(Self { + clients: ClientsConfig::default(), + http: HttpConfig::default(), + database: DatabaseConfig::default(), + telemetry: TelemetryConfig::default(), + templates: TemplatesConfig::default(), + email: EmailConfig::default(), + passwords: PasswordsConfig::default(), + secrets: SecretsConfig::generate(&mut rng).await?, + matrix: MatrixConfig::generate(&mut rng), + policy: PolicyConfig::default(), + upstream_oauth2: UpstreamOAuth2Config::default(), + branding: BrandingConfig::default(), + experimental: ExperimentalConfig::default(), + }) + } + + /// Configuration used in tests + #[must_use] + pub fn test() -> Self { Self { - clients: ClientsConfig::test(), - http: HttpConfig::test(), - database: DatabaseConfig::test(), - telemetry: TelemetryConfig::test(), - templates: TemplatesConfig::test(), - passwords: PasswordsConfig::test(), - email: EmailConfig::test(), + clients: ClientsConfig::default(), + http: HttpConfig::default(), + database: DatabaseConfig::default(), + telemetry: TelemetryConfig::default(), + templates: TemplatesConfig::default(), + passwords: PasswordsConfig::default(), + email: EmailConfig::default(), secrets: SecretsConfig::test(), matrix: MatrixConfig::test(), - policy: PolicyConfig::test(), - upstream_oauth2: UpstreamOAuth2Config::test(), - branding: BrandingConfig::test(), - experimental: ExperimentalConfig::test(), + policy: PolicyConfig::default(), + upstream_oauth2: UpstreamOAuth2Config::default(), + branding: BrandingConfig::default(), + experimental: ExperimentalConfig::default(), } } } /// Partial configuration actually used by the server #[allow(missing_docs)] -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize)] pub struct AppConfig { #[serde(default)] pub http: HttpConfig, @@ -206,27 +213,8 @@ pub struct AppConfig { pub experimental: ExperimentalConfig, } -#[async_trait] impl ConfigurationSection for AppConfig { - async fn generate(mut rng: R) -> anyhow::Result - where - R: Rng + Send, - { - Ok(Self { - http: HttpConfig::generate(&mut rng).await?, - database: DatabaseConfig::generate(&mut rng).await?, - templates: TemplatesConfig::generate(&mut rng).await?, - email: EmailConfig::generate(&mut rng).await?, - passwords: PasswordsConfig::generate(&mut rng).await?, - secrets: SecretsConfig::generate(&mut rng).await?, - matrix: MatrixConfig::generate(&mut rng).await?, - policy: PolicyConfig::generate(&mut rng).await?, - branding: BrandingConfig::generate(&mut rng).await?, - experimental: ExperimentalConfig::generate(&mut rng).await?, - }) - } - - fn validate(&self, figment: &figment::Figment) -> Result<(), figment::error::Error> { + fn validate(&self, figment: &figment::Figment) -> Result<(), figment::Error> { self.http.validate(figment)?; self.database.validate(figment)?; self.templates.validate(figment)?; @@ -240,26 +228,11 @@ impl ConfigurationSection for AppConfig { Ok(()) } - - fn test() -> Self { - Self { - http: HttpConfig::test(), - database: DatabaseConfig::test(), - templates: TemplatesConfig::test(), - passwords: PasswordsConfig::test(), - email: EmailConfig::test(), - secrets: SecretsConfig::test(), - matrix: MatrixConfig::test(), - policy: PolicyConfig::test(), - branding: BrandingConfig::test(), - experimental: ExperimentalConfig::test(), - } - } } /// Partial config used by the `mas-cli config sync` command #[allow(missing_docs)] -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize)] pub struct SyncConfig { #[serde(default)] pub database: DatabaseConfig, @@ -273,26 +246,13 @@ pub struct SyncConfig { pub upstream_oauth2: UpstreamOAuth2Config, } -#[async_trait] impl ConfigurationSection for SyncConfig { - async fn generate(mut rng: R) -> anyhow::Result - where - R: Rng + Send, - { - Ok(Self { - database: DatabaseConfig::generate(&mut rng).await?, - secrets: SecretsConfig::generate(&mut rng).await?, - clients: ClientsConfig::generate(&mut rng).await?, - upstream_oauth2: UpstreamOAuth2Config::generate(&mut rng).await?, - }) - } + fn validate(&self, figment: &figment::Figment) -> Result<(), figment::Error> { + self.database.validate(figment)?; + self.secrets.validate(figment)?; + self.clients.validate(figment)?; + self.upstream_oauth2.validate(figment)?; - fn test() -> Self { - Self { - database: DatabaseConfig::test(), - secrets: SecretsConfig::test(), - clients: ClientsConfig::test(), - upstream_oauth2: UpstreamOAuth2Config::test(), - } + Ok(()) } } diff --git a/crates/config/src/sections/passwords.rs b/crates/config/src/sections/passwords.rs index 1915f982f..981565727 100644 --- a/crates/config/src/sections/passwords.rs +++ b/crates/config/src/sections/passwords.rs @@ -13,9 +13,7 @@ // limitations under the License. use anyhow::bail; -use async_trait::async_trait; use camino::Utf8PathBuf; -use rand::Rng; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -55,17 +53,9 @@ impl Default for PasswordsConfig { } } -#[async_trait] impl ConfigurationSection for PasswordsConfig { const PATH: Option<&'static str> = Some("passwords"); - async fn generate(_rng: R) -> anyhow::Result - where - R: Rng + Send, - { - Ok(Self::default()) - } - fn validate(&self, figment: &figment::Figment) -> Result<(), figment::Error> { let annotate = |mut error: figment::Error| { error.metadata = figment.find_metadata(Self::PATH.unwrap()).cloned(); @@ -95,10 +85,6 @@ impl ConfigurationSection for PasswordsConfig { Ok(()) } - - fn test() -> Self { - Self::default() - } } impl PasswordsConfig { diff --git a/crates/config/src/sections/policy.rs b/crates/config/src/sections/policy.rs index a99eb0d22..a3d3a93a0 100644 --- a/crates/config/src/sections/policy.rs +++ b/crates/config/src/sections/policy.rs @@ -12,9 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use async_trait::async_trait; use camino::Utf8PathBuf; -use rand::Rng; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_with::serde_as; @@ -36,61 +34,107 @@ fn default_policy_path() -> Utf8PathBuf { "./share/policy.wasm".into() } -fn default_client_registration_endpoint() -> String { +fn is_default_policy_path(value: &Utf8PathBuf) -> bool { + *value == default_policy_path() +} + +fn default_client_registration_entrypoint() -> String { "client_registration/violation".to_owned() } -fn default_register_endpoint() -> String { +fn is_default_client_registration_entrypoint(value: &String) -> bool { + *value == default_client_registration_entrypoint() +} + +fn default_register_entrypoint() -> String { "register/violation".to_owned() } -fn default_authorization_grant_endpoint() -> String { +fn is_default_register_entrypoint(value: &String) -> bool { + *value == default_register_entrypoint() +} + +fn default_authorization_grant_entrypoint() -> String { "authorization_grant/violation".to_owned() } -fn default_password_endpoint() -> String { +fn is_default_authorization_grant_entrypoint(value: &String) -> bool { + *value == default_authorization_grant_entrypoint() +} + +fn default_password_entrypoint() -> String { "password/violation".to_owned() } -fn default_email_endpoint() -> String { +fn is_default_password_entrypoint(value: &String) -> bool { + *value == default_password_entrypoint() +} + +fn default_email_entrypoint() -> String { "email/violation".to_owned() } +fn is_default_email_entrypoint(value: &String) -> bool { + *value == default_email_entrypoint() +} + fn default_data() -> serde_json::Value { serde_json::json!({}) } +fn is_default_data(value: &serde_json::Value) -> bool { + *value == default_data() +} + /// Application secrets #[serde_as] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct PolicyConfig { /// Path to the WASM module - #[serde(default = "default_policy_path")] + #[serde( + default = "default_policy_path", + skip_serializing_if = "is_default_policy_path" + )] #[schemars(with = "String")] pub wasm_module: Utf8PathBuf, /// Entrypoint to use when evaluating client registrations - #[serde(default = "default_client_registration_endpoint")] + #[serde( + default = "default_client_registration_entrypoint", + skip_serializing_if = "is_default_client_registration_entrypoint" + )] pub client_registration_entrypoint: String, /// Entrypoint to use when evaluating user registrations - #[serde(default = "default_register_endpoint")] + #[serde( + default = "default_register_entrypoint", + skip_serializing_if = "is_default_register_entrypoint" + )] pub register_entrypoint: String, /// Entrypoint to use when evaluating authorization grants - #[serde(default = "default_authorization_grant_endpoint")] + #[serde( + default = "default_authorization_grant_entrypoint", + skip_serializing_if = "is_default_authorization_grant_entrypoint" + )] pub authorization_grant_entrypoint: String, /// Entrypoint to use when changing password - #[serde(default = "default_password_endpoint")] + #[serde( + default = "default_password_entrypoint", + skip_serializing_if = "is_default_password_entrypoint" + )] pub password_entrypoint: String, /// Entrypoint to use when adding an email address - #[serde(default = "default_email_endpoint")] + #[serde( + default = "default_email_entrypoint", + skip_serializing_if = "is_default_email_entrypoint" + )] pub email_entrypoint: String, /// Arbitrary data to pass to the policy - #[serde(default = "default_data")] + #[serde(default = "default_data", skip_serializing_if = "is_default_data")] pub data: serde_json::Value, } @@ -98,28 +142,29 @@ impl Default for PolicyConfig { fn default() -> Self { Self { wasm_module: default_policy_path(), - client_registration_entrypoint: default_client_registration_endpoint(), - register_entrypoint: default_register_endpoint(), - authorization_grant_entrypoint: default_authorization_grant_endpoint(), - password_entrypoint: default_password_endpoint(), - email_entrypoint: default_email_endpoint(), + client_registration_entrypoint: default_client_registration_entrypoint(), + register_entrypoint: default_register_entrypoint(), + authorization_grant_entrypoint: default_authorization_grant_entrypoint(), + password_entrypoint: default_password_entrypoint(), + email_entrypoint: default_email_entrypoint(), data: default_data(), } } } -#[async_trait] -impl ConfigurationSection for PolicyConfig { - const PATH: Option<&'static str> = Some("policy"); - - async fn generate(_rng: R) -> anyhow::Result - where - R: Rng + Send, - { - Ok(Self::default()) +impl PolicyConfig { + /// Returns true if the configuration is the default one + pub(crate) fn is_default(&self) -> bool { + is_default_policy_path(&self.wasm_module) + && is_default_client_registration_entrypoint(&self.client_registration_entrypoint) + && is_default_register_entrypoint(&self.register_entrypoint) + && is_default_authorization_grant_entrypoint(&self.authorization_grant_entrypoint) + && is_default_password_entrypoint(&self.password_entrypoint) + && is_default_email_entrypoint(&self.email_entrypoint) + && is_default_data(&self.data) } +} - fn test() -> Self { - Self::default() - } +impl ConfigurationSection for PolicyConfig { + const PATH: Option<&'static str> = Some("policy"); } diff --git a/crates/config/src/sections/secrets.rs b/crates/config/src/sections/secrets.rs index f6bdf2da9..3e1294e48 100644 --- a/crates/config/src/sections/secrets.rs +++ b/crates/config/src/sections/secrets.rs @@ -15,7 +15,6 @@ use std::borrow::Cow; use anyhow::{bail, Context}; -use async_trait::async_trait; use camino::Utf8PathBuf; use mas_jose::jwk::{JsonWebKey, JsonWebKeySet}; use mas_keystore::{Encrypter, Keystore, PrivateKey}; @@ -132,12 +131,50 @@ impl SecretsConfig { } } -#[async_trait] impl ConfigurationSection for SecretsConfig { const PATH: Option<&'static str> = Some("secrets"); + fn validate(&self, figment: &figment::Figment) -> Result<(), figment::Error> { + for (index, key) in self.keys.iter().enumerate() { + let annotate = |mut error: figment::Error| { + error.metadata = figment + .find_metadata(&format!("{root}.keys", root = Self::PATH.unwrap())) + .cloned(); + error.profile = Some(figment::Profile::Default); + error.path = vec![ + Self::PATH.unwrap().to_owned(), + "keys".to_owned(), + index.to_string(), + ]; + Err(error) + }; + + if key.key.is_none() && key.key_file.is_none() { + return annotate(figment::Error::from( + "Missing `key` or `key_file`".to_owned(), + )); + } + + if key.key.is_some() && key.key_file.is_some() { + return annotate(figment::Error::from( + "Cannot specify both `key` and `key_file`".to_owned(), + )); + } + + if key.password.is_some() && key.password_file.is_some() { + return annotate(figment::Error::from( + "Cannot specify both `password` and `password_file`".to_owned(), + )); + } + } + + Ok(()) + } +} + +impl SecretsConfig { #[tracing::instrument(skip_all)] - async fn generate(mut rng: R) -> anyhow::Result + pub(crate) async fn generate(mut rng: R) -> anyhow::Result where R: Rng + Send, { @@ -221,44 +258,7 @@ impl ConfigurationSection for SecretsConfig { }) } - fn validate(&self, figment: &figment::Figment) -> Result<(), figment::Error> { - for (index, key) in self.keys.iter().enumerate() { - let annotate = |mut error: figment::Error| { - error.metadata = figment - .find_metadata(&format!("{root}.keys", root = Self::PATH.unwrap())) - .cloned(); - error.profile = Some(figment::Profile::Default); - error.path = vec![ - Self::PATH.unwrap().to_owned(), - "keys".to_owned(), - index.to_string(), - ]; - Err(error) - }; - - if key.key.is_none() && key.key_file.is_none() { - return annotate(figment::Error::from( - "Missing `key` or `key_file`".to_owned(), - )); - } - - if key.key.is_some() && key.key_file.is_some() { - return annotate(figment::Error::from( - "Cannot specify both `key` and `key_file`".to_owned(), - )); - } - - if key.password.is_some() && key.password_file.is_some() { - return annotate(figment::Error::from( - "Cannot specify both `password` and `password_file`".to_owned(), - )); - } - } - - Ok(()) - } - - fn test() -> Self { + pub(crate) fn test() -> Self { let rsa_key = KeyConfig { kid: "abcdef".to_owned(), password: None, diff --git a/crates/config/src/sections/telemetry.rs b/crates/config/src/sections/telemetry.rs index 4690a36a1..670791c43 100644 --- a/crates/config/src/sections/telemetry.rs +++ b/crates/config/src/sections/telemetry.rs @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -use async_trait::async_trait; -use rand::Rng; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; @@ -72,6 +70,15 @@ pub struct TracingConfig { pub propagators: Vec, } +impl TracingConfig { + /// Returns true if all fields are at their default values + fn is_default(&self) -> bool { + matches!(self.exporter, TracingExporterKind::None) + && self.endpoint.is_none() + && self.propagators.is_empty() + } +} + /// Exporter to use when exporting metrics #[skip_serializing_none] #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, Default)] @@ -105,6 +112,13 @@ pub struct MetricsConfig { pub endpoint: Option, } +impl MetricsConfig { + /// Returns true if all fields are at their default values + fn is_default(&self) -> bool { + matches!(self.exporter, MetricsExporterKind::None) && self.endpoint.is_none() + } +} + fn sentry_dsn_example() -> &'static str { "https://public@host:port/1" } @@ -118,34 +132,36 @@ pub struct SentryConfig { pub dsn: Option, } +impl SentryConfig { + /// Returns true if all fields are at their default values + fn is_default(&self) -> bool { + self.dsn.is_none() + } +} + /// Configuration related to sending monitoring data #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] pub struct TelemetryConfig { /// Configuration related to exporting traces - #[serde(default)] + #[serde(default, skip_serializing_if = "TracingConfig::is_default")] pub tracing: TracingConfig, /// Configuration related to exporting metrics - #[serde(default)] + #[serde(default, skip_serializing_if = "MetricsConfig::is_default")] pub metrics: MetricsConfig, /// Configuration related to the Sentry integration - #[serde(default)] + #[serde(default, skip_serializing_if = "SentryConfig::is_default")] pub sentry: SentryConfig, } -#[async_trait] -impl ConfigurationSection for TelemetryConfig { - const PATH: Option<&'static str> = Some("telemetry"); - - async fn generate(_rng: R) -> anyhow::Result - where - R: Rng + Send, - { - Ok(Self::default()) +impl TelemetryConfig { + /// Returns true if all fields are at their default values + pub(crate) fn is_default(&self) -> bool { + self.tracing.is_default() && self.metrics.is_default() && self.sentry.is_default() } +} - fn test() -> Self { - Self::default() - } +impl ConfigurationSection for TelemetryConfig { + const PATH: Option<&'static str> = Some("telemetry"); } diff --git a/crates/config/src/sections/templates.rs b/crates/config/src/sections/templates.rs index b146dd7b9..d01dae88f 100644 --- a/crates/config/src/sections/templates.rs +++ b/crates/config/src/sections/templates.rs @@ -12,9 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use async_trait::async_trait; use camino::Utf8PathBuf; -use rand::Rng; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -35,6 +33,10 @@ fn default_path() -> Utf8PathBuf { "./share/templates/".into() } +fn is_default_path(value: &Utf8PathBuf) -> bool { + *value == default_path() +} + #[cfg(not(any(feature = "docker", feature = "dist")))] fn default_assets_path() -> Utf8PathBuf { "./frontend/dist/manifest.json".into() @@ -50,6 +52,10 @@ fn default_assets_path() -> Utf8PathBuf { "./share/manifest.json".into() } +fn is_default_assets_path(value: &Utf8PathBuf) -> bool { + *value == default_assets_path() +} + #[cfg(not(any(feature = "docker", feature = "dist")))] fn default_translations_path() -> Utf8PathBuf { "./translations/".into() @@ -65,21 +71,31 @@ fn default_translations_path() -> Utf8PathBuf { "./share/translations/".into() } +fn is_default_translations_path(value: &Utf8PathBuf) -> bool { + *value == default_translations_path() +} + /// Configuration related to templates #[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)] pub struct TemplatesConfig { /// Path to the folder which holds the templates - #[serde(default = "default_path")] + #[serde(default = "default_path", skip_serializing_if = "is_default_path")] #[schemars(with = "Option")] pub path: Utf8PathBuf, /// Path to the assets manifest - #[serde(default = "default_assets_path")] + #[serde( + default = "default_assets_path", + skip_serializing_if = "is_default_assets_path" + )] #[schemars(with = "Option")] pub assets_manifest: Utf8PathBuf, /// Path to the translations - #[serde(default = "default_translations_path")] + #[serde( + default = "default_translations_path", + skip_serializing_if = "is_default_translations_path" + )] #[schemars(with = "Option")] pub translations_path: Utf8PathBuf, } @@ -94,18 +110,15 @@ impl Default for TemplatesConfig { } } -#[async_trait] -impl ConfigurationSection for TemplatesConfig { - const PATH: Option<&'static str> = Some("templates"); - - async fn generate(_rng: R) -> anyhow::Result - where - R: Rng + Send, - { - Ok(Self::default()) +impl TemplatesConfig { + /// Returns true if all fields are at their default values + pub(crate) fn is_default(&self) -> bool { + is_default_path(&self.path) + && is_default_assets_path(&self.assets_manifest) + && is_default_translations_path(&self.translations_path) } +} - fn test() -> Self { - Self::default() - } +impl ConfigurationSection for TemplatesConfig { + const PATH: Option<&'static str> = Some("templates"); } diff --git a/crates/config/src/sections/upstream_oauth2.rs b/crates/config/src/sections/upstream_oauth2.rs index 716297ce1..0b1d4d82c 100644 --- a/crates/config/src/sections/upstream_oauth2.rs +++ b/crates/config/src/sections/upstream_oauth2.rs @@ -14,9 +14,7 @@ use std::collections::BTreeMap; -use async_trait::async_trait; use mas_iana::{jose::JsonWebSignatureAlg, oauth::OAuthClientAuthenticationMethod}; -use rand::Rng; use schemars::JsonSchema; use serde::{de::Error, Deserialize, Serialize}; use serde_with::skip_serializing_none; @@ -32,17 +30,16 @@ pub struct UpstreamOAuth2Config { pub providers: Vec, } -#[async_trait] +impl UpstreamOAuth2Config { + /// Returns true if the configuration is the default one + pub(crate) fn is_default(&self) -> bool { + self.providers.is_empty() + } +} + impl ConfigurationSection for UpstreamOAuth2Config { const PATH: Option<&'static str> = Some("upstream_oauth2"); - async fn generate(_rng: R) -> anyhow::Result - where - R: Rng + Send, - { - Ok(Self::default()) - } - fn validate(&self, figment: &figment::Figment) -> Result<(), figment::Error> { for (index, provider) in self.providers.iter().enumerate() { let annotate = |mut error: figment::Error| { @@ -95,10 +92,6 @@ impl ConfigurationSection for UpstreamOAuth2Config { Ok(()) } - - fn test() -> Self { - Self::default() - } } /// Authentication methods used against the OAuth 2.0 provider diff --git a/crates/config/src/util.rs b/crates/config/src/util.rs index 5c01e433a..0603ec439 100644 --- a/crates/config/src/util.rs +++ b/crates/config/src/util.rs @@ -12,23 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -use async_trait::async_trait; use figment::{error::Error as FigmentError, Figment}; -use rand::Rng; -use serde::{de::DeserializeOwned, Serialize}; +use serde::de::DeserializeOwned; -#[async_trait] /// Trait implemented by all configuration section to help loading specific part /// of the config and generate the sample config. -pub trait ConfigurationSection: Sized + DeserializeOwned + Serialize { +pub trait ConfigurationSection: Sized + DeserializeOwned { /// Specify where this section should live relative to the root. const PATH: Option<&'static str> = None; - /// Generate a sample configuration for this section. - async fn generate(rng: R) -> anyhow::Result - where - R: Rng + Send; - /// Validate the configuration section /// /// # Errors @@ -53,7 +45,4 @@ pub trait ConfigurationSection: Sized + DeserializeOwned + Serialize { this.validate(figment)?; Ok(this) } - - /// Generate config used in unit tests - fn test() -> Self; } diff --git a/docs/config.schema.json b/docs/config.schema.json index 8eafebb18..57b1c9356 100644 --- a/docs/config.schema.json +++ b/docs/config.schema.json @@ -10,7 +10,6 @@ "properties": { "clients": { "description": "List of OAuth 2.0/OIDC clients config", - "default": [], "type": "array", "items": { "$ref": "#/definitions/ClientConfig" @@ -40,8 +39,7 @@ "playground": true }, { - "name": "assets", - "path": "./frontend/dist/" + "name": "assets" } ], "binds": [ @@ -102,16 +100,6 @@ }, "telemetry": { "description": "Configuration related to sending monitoring data", - "default": { - "tracing": { - "exporter": "none", - "propagators": [] - }, - "metrics": { - "exporter": "none" - }, - "sentry": {} - }, "allOf": [ { "$ref": "#/definitions/TelemetryConfig" @@ -120,11 +108,6 @@ }, "templates": { "description": "Configuration related to templates", - "default": { - "path": "./templates/", - "assets_manifest": "./frontend/dist/manifest.json", - "translations_path": "./translations/" - }, "allOf": [ { "$ref": "#/definitions/TemplatesConfig" @@ -179,15 +162,6 @@ }, "policy": { "description": "Configuration related to the OPA policies", - "default": { - "wasm_module": "./policies/policy.wasm", - "client_registration_entrypoint": "client_registration/violation", - "register_entrypoint": "register/violation", - "authorization_grant_entrypoint": "authorization_grant/violation", - "password_entrypoint": "password/violation", - "email_entrypoint": "email/violation", - "data": {} - }, "allOf": [ { "$ref": "#/definitions/PolicyConfig" @@ -196,9 +170,6 @@ }, "upstream_oauth2": { "description": "Configuration related to upstream OAuth providers", - "default": { - "providers": [] - }, "allOf": [ { "$ref": "#/definitions/UpstreamOAuth2Config" @@ -207,13 +178,6 @@ }, "branding": { "description": "Configuration section for tweaking the branding of the service", - "default": { - "service_name": null, - "policy_uri": null, - "tos_uri": null, - "imprint": null, - "logo_uri": null - }, "allOf": [ { "$ref": "#/definitions/BrandingConfig" @@ -222,10 +186,6 @@ }, "experimental": { "description": "Experimental configuration options", - "default": { - "access_token_ttl": 300, - "compat_token_ttl": 300 - }, "allOf": [ { "$ref": "#/definitions/ExperimentalConfig" @@ -815,7 +775,6 @@ }, "path": { "description": "Path to the directory to serve.", - "default": "./frontend/dist/", "type": "string" } } @@ -1083,10 +1042,6 @@ "properties": { "tracing": { "description": "Configuration related to exporting traces", - "default": { - "exporter": "none", - "propagators": [] - }, "allOf": [ { "$ref": "#/definitions/TracingConfig" @@ -1095,9 +1050,6 @@ }, "metrics": { "description": "Configuration related to exporting metrics", - "default": { - "exporter": "none" - }, "allOf": [ { "$ref": "#/definitions/MetricsConfig" @@ -1106,7 +1058,6 @@ }, "sentry": { "description": "Configuration related to the Sentry integration", - "default": {}, "allOf": [ { "$ref": "#/definitions/SentryConfig" @@ -1272,17 +1223,14 @@ "properties": { "path": { "description": "Path to the folder which holds the templates", - "default": "./templates/", "type": "string" }, "assets_manifest": { "description": "Path to the assets manifest", - "default": "./frontend/dist/manifest.json", "type": "string" }, "translations_path": { "description": "Path to the translations", - "default": "./translations/", "type": "string" } } @@ -1561,37 +1509,30 @@ "properties": { "wasm_module": { "description": "Path to the WASM module", - "default": "./policies/policy.wasm", "type": "string" }, "client_registration_entrypoint": { "description": "Entrypoint to use when evaluating client registrations", - "default": "client_registration/violation", "type": "string" }, "register_entrypoint": { "description": "Entrypoint to use when evaluating user registrations", - "default": "register/violation", "type": "string" }, "authorization_grant_entrypoint": { "description": "Entrypoint to use when evaluating authorization grants", - "default": "authorization_grant/violation", "type": "string" }, "password_entrypoint": { "description": "Entrypoint to use when changing password", - "default": "password/violation", "type": "string" }, "email_entrypoint": { "description": "Entrypoint to use when adding an email address", - "default": "email/violation", "type": "string" }, "data": { - "description": "Arbitrary data to pass to the policy", - "default": {} + "description": "Arbitrary data to pass to the policy" } } }, @@ -2010,7 +1951,6 @@ "properties": { "access_token_ttl": { "description": "Time-to-live of access tokens in seconds. Defaults to 5 minutes.", - "default": 300, "type": "integer", "format": "uint64", "maximum": 86400.0, @@ -2018,7 +1958,6 @@ }, "compat_token_ttl": { "description": "Time-to-live of compatibility access tokens in seconds. Defaults to 5 minutes.", - "default": 300, "type": "integer", "format": "uint64", "maximum": 86400.0,