opentalk_controller_settings/
lib.rs

1// SPDX-FileCopyrightText: OpenTalk GmbH <mail@opentalk.eu>
2//
3// SPDX-License-Identifier: EUPL-1.2
4
5//! Contains the application settings.
6//!
7//! The application settings are set with a TOML config file. Settings specified in the config file
8//! can be overwritten by environment variables. To do so, set an environment variable
9//! with the prefix `OPENTALK_CTRL_` followed by the field names you want to set. Nested fields are separated by two underscores `__`.
10//! ```sh
11//! OPENTALK_CTRL_<field>__<field-of-field>...
12//! ```
13//!
14//! # Example
15//!
16//! set the `database.url` field:
17//! ```sh
18//! OPENTALK_CTRL_DATABASE__URL=postgres://postgres:password123@localhost:5432/opentalk
19//! ```
20//!
21//! So the field 'database.max_connections' would resolve to:
22//! ```sh
23//! OPENTALK_CTRL_DATABASE__MAX_CONNECTIONS=5
24//! ```
25//!
26//! # Note
27//!
28//! Fields set via environment variables do not affect the underlying config file.
29//!
30//! # Implementation Details:
31//!
32//! Setting categories, in which all properties implement a default value, should also implement the [`Default`] trait.
33
34use std::{
35    collections::{BTreeSet, HashMap},
36    convert::TryFrom,
37    net::IpAddr,
38    path::PathBuf,
39    sync::Arc,
40    time::Duration,
41};
42
43use arc_swap::ArcSwap;
44use config::{Config, Environment, File, FileFormat};
45use openidconnect::{ClientId, ClientSecret};
46use opentalk_types_common::{features::ModuleFeatureId, users::Language};
47use rustc_hash::FxHashSet;
48use serde::{Deserialize, Deserializer};
49use snafu::{ResultExt, Snafu};
50use url::Url;
51
52#[derive(Debug, Snafu)]
53pub enum SettingsError {
54    #[snafu(display("Failed to read data as config: {}", source), context(false))]
55    BuildConfig { source: config::ConfigError },
56
57    #[snafu(display("Failed to apply configuration from {} or environment", file_name))]
58    DeserializeConfig {
59        file_name: String,
60        #[snafu(source(from(serde_path_to_error::Error<config::ConfigError>, Box::new)))]
61        source: Box<serde_path_to_error::Error<config::ConfigError>>,
62    },
63
64    #[snafu(display("Given base URL is not a base: {}", url))]
65    NotBaseUrl { url: Url },
66
67    #[snafu(display("Inconsistent configuration for OIDC and user search, check [keycloak], [endpoints], [oidc] and [user_search] sections"))]
68    InconsistentOidcAndUserSearchConfig,
69}
70
71type Result<T, E = SettingsError> = std::result::Result<T, E>;
72
73pub type SharedSettings = Arc<ArcSwap<Settings>>;
74
75#[derive(Debug, Clone, Deserialize)]
76pub struct Settings {
77    pub database: Database,
78    #[serde(default)]
79    pub keycloak: Option<Keycloak>,
80    #[serde(default)]
81    pub oidc: Option<Oidc>,
82    #[serde(default)]
83    pub user_search: Option<UserSearch>,
84    pub http: Http,
85    #[serde(default)]
86    pub turn: Option<Turn>,
87    #[serde(default)]
88    pub stun: Option<Stun>,
89    #[serde(default)]
90    pub redis: Option<RedisConfig>,
91    #[serde(default)]
92    pub rabbit_mq: RabbitMqConfig,
93    #[serde(default)]
94    pub logging: Logging,
95    #[serde(default)]
96    pub authz: Authz,
97    #[serde(default)]
98    pub avatar: Avatar,
99    #[serde(default)]
100    pub metrics: Metrics,
101
102    #[serde(default)]
103    pub etcd: Option<Etcd>,
104
105    #[serde(default)]
106    pub etherpad: Option<Etherpad>,
107
108    #[serde(default)]
109    pub spacedeck: Option<Spacedeck>,
110
111    #[serde(default)]
112    pub subroom_audio: Option<SubroomAudio>,
113
114    #[serde(default)]
115    pub reports: Option<Reports>,
116
117    #[serde(default)]
118    pub shared_folder: Option<SharedFolder>,
119
120    #[serde(default)]
121    pub call_in: Option<CallIn>,
122
123    #[serde(default)]
124    pub defaults: Defaults,
125
126    #[serde(default)]
127    pub endpoints: Endpoints,
128
129    pub minio: MinIO,
130
131    #[serde(default)]
132    pub monitoring: Option<MonitoringSettings>,
133
134    #[serde(default)]
135    pub tenants: Tenants,
136
137    #[serde(default)]
138    pub tariffs: Tariffs,
139
140    pub livekit: LiveKitSettings,
141
142    #[serde(flatten)]
143    pub extensions: HashMap<String, config::Value>,
144}
145
146#[derive(Debug, Clone)]
147struct WarningSource<T: Clone>(T);
148
149impl<T> config::Source for WarningSource<T>
150where
151    T: config::Source + Send + Sync + Clone + 'static,
152{
153    fn clone_into_box(&self) -> Box<dyn config::Source + Send + Sync> {
154        Box::new((*self).clone())
155    }
156
157    fn collect(&self) -> Result<config::Map<String, config::Value>, config::ConfigError> {
158        let values = self.0.collect()?;
159        if !values.is_empty() {
160            use owo_colors::OwoColorize as _;
161
162            anstream::eprintln!(
163                "{}: The following environment variables have been deprecated and \
164                will not work in a future release. Please change them as suggested below:",
165                "DEPRECATION WARNING".yellow().bold(),
166            );
167
168            for key in values.keys() {
169                let env_var = key.replace('.', "__").to_uppercase();
170                anstream::eprintln!(
171                    "{}: rename environment variable {} to {}",
172                    "DEPRECATION WARNING".yellow().bold(),
173                    format!("K3K_CTRL_{}", env_var).yellow(),
174                    format!("OPENTALK_CTRL_{}", env_var).green().bold(),
175                );
176            }
177        }
178
179        Ok(values)
180    }
181}
182
183#[derive(Debug, Clone, Deserialize)]
184pub struct MonitoringSettings {
185    #[serde(default = "default_monitoring_port")]
186    pub port: u16,
187    #[serde(default = "default_monitoring_addr")]
188    pub addr: IpAddr,
189}
190
191fn default_monitoring_port() -> u16 {
192    11411
193}
194
195fn default_monitoring_addr() -> IpAddr {
196    [0, 0, 0, 0].into()
197}
198
199/// OIDC and user search configuration
200#[derive(Debug, Clone, Deserialize)]
201pub struct OidcAndUserSearchConfiguration {
202    pub oidc: OidcConfiguration,
203    pub user_search: UserSearchConfiguration,
204}
205
206/// OIDC configuration
207#[derive(Debug, Clone, Deserialize)]
208pub struct OidcConfiguration {
209    pub frontend: FrontendOidcConfiguration,
210    pub controller: ControllerOidcConfiguration,
211}
212
213/// OIDC configuration for frontend
214#[derive(Debug, Clone, Deserialize)]
215pub struct FrontendOidcConfiguration {
216    pub auth_base_url: Url,
217    pub client_id: ClientId,
218}
219
220/// OIDC configuration for controller
221#[derive(Debug, Clone, Deserialize)]
222pub struct ControllerOidcConfiguration {
223    pub auth_base_url: Url,
224    pub client_id: ClientId,
225    pub client_secret: ClientSecret,
226}
227
228/// User search configuration
229#[derive(Debug, Clone, Deserialize)]
230pub struct UserSearchConfiguration {
231    pub backend: UserSearchBackend,
232    pub api_base_url: Url,
233    pub client_id: ClientId,
234    pub client_secret: ClientSecret,
235    pub external_id_user_attribute_name: Option<String>,
236    pub users_find_behavior: UsersFindBehavior,
237}
238
239impl Settings {
240    /// internal url builder
241    fn build_url<I>(base_url: Url, path_segments: I) -> Result<Url>
242    where
243        I: IntoIterator,
244        I::Item: AsRef<str>,
245    {
246        let err_url = base_url.clone();
247        let mut url = base_url;
248        url.path_segments_mut()
249            .map_err(|_| SettingsError::NotBaseUrl { url: err_url })?
250            .extend(path_segments);
251        Ok(url)
252    }
253
254    /// Builds the effective OIDC and user search configuration, either from the deprecated `[keycloak]` section
255    /// and some deprecated `[endpoints]` settings or from the new `[oidc]` and `[user_search]` sections.
256    pub fn build_oidc_and_user_search_configuration(
257        &self,
258    ) -> Result<OidcAndUserSearchConfiguration> {
259        let keycloak = self.keycloak.clone();
260        let disable_users_find = self.endpoints.disable_users_find;
261        let users_find_use_kc = self.endpoints.users_find_use_kc;
262        let oidc = self.oidc.clone();
263        let user_search = self.user_search.clone();
264
265        match (
266            keycloak,
267            disable_users_find,
268            users_find_use_kc,
269            oidc,
270            user_search,
271        ) {
272            // Only the new OIDC and user search configuration is present
273            (None, None, None, Some(oidc), Some(user_search)) => {
274                Self::build_new_oidc_and_user_search_configuration(oidc, user_search)
275            }
276            // Only the legacy OIDC and user search configuration is present
277            (Some(keycloak), _, _, None, None) => {
278                Self::build_legacy_oidc_and_user_search_configuration(
279                    &keycloak,
280                    disable_users_find,
281                    users_find_use_kc,
282                )?
283            }
284            // The OIDC and user search configuration is inconsistent
285            _ => Err(SettingsError::InconsistentOidcAndUserSearchConfig),
286        }
287    }
288
289    /// Builds the effective OIDC and user search configuration from the new `[oidc]` and `[user_search]` sections.
290    fn build_new_oidc_and_user_search_configuration(
291        oidc: Oidc,
292        user_search: UserSearch,
293    ) -> Result<OidcAndUserSearchConfiguration, SettingsError> {
294        // Frontend-specific OIDC configuration
295        let frontend_auth_base_url = oidc.frontend.authority.unwrap_or(oidc.authority.clone());
296        let frontend_client_id = oidc.frontend.client_id.clone();
297
298        // Controller-specific OIDC configuration
299        let controller_auth_base_url = oidc.controller.authority.unwrap_or(oidc.authority);
300        let controller_client_id = oidc.controller.client_id.clone();
301        let controller_client_secret = oidc.controller.client_secret.clone();
302
303        // User search configuration
304        let backend = user_search.backend;
305        let api_base_url = user_search.api_base_url;
306        let user_search_client_id = user_search
307            .client_id
308            .unwrap_or(controller_client_id.clone());
309        let user_search_client_secret = user_search
310            .client_secret
311            .unwrap_or(controller_client_secret.clone());
312        let external_id_user_attribute_name = user_search.external_id_user_attribute_name.clone();
313        let users_find_behavior = user_search.users_find_behavior;
314
315        // Assemble the entire effective OIDC and user search configuration
316        let frontend = FrontendOidcConfiguration {
317            auth_base_url: frontend_auth_base_url,
318            client_id: frontend_client_id,
319        };
320        let controller = ControllerOidcConfiguration {
321            auth_base_url: controller_auth_base_url,
322            client_id: controller_client_id,
323            client_secret: controller_client_secret.clone(),
324        };
325        let oidc = OidcConfiguration {
326            frontend,
327            controller,
328        };
329        let api = UserSearchConfiguration {
330            backend,
331            api_base_url,
332            client_id: user_search_client_id,
333            client_secret: user_search_client_secret,
334            external_id_user_attribute_name,
335            users_find_behavior,
336        };
337        Ok(OidcAndUserSearchConfiguration {
338            oidc,
339            user_search: api,
340        })
341    }
342
343    /// Builds the effective OIDC and user search configuration from the deprecated `[keycloak]` section
344    /// and some deprecated `[endpoints]` settings.
345    fn build_legacy_oidc_and_user_search_configuration(
346        keycloak: &Keycloak,
347        disable_users_find: Option<bool>,
348        users_find_use_kc: Option<bool>,
349    ) -> Result<Result<OidcAndUserSearchConfiguration, SettingsError>, SettingsError> {
350        log::warn!(
351                    "You are using deprecated OIDC and user search settings. See docs for [oidc] and [user_search] configuration sections."
352                );
353
354        // Collect legacy OIDC and user search settings
355        let backend = UserSearchBackend::KeycloakWebapi;
356        let api_base_url = Self::build_url(
357            keycloak.base_url.clone(),
358            ["admin", "realms", &keycloak.realm],
359        )?;
360        let auth_base_url =
361            Self::build_url(keycloak.base_url.clone(), ["realms", &keycloak.realm])?;
362        let client_id = keycloak.client_id.clone();
363        let client_secret = keycloak.client_secret.clone();
364        let external_id_user_attribute_name = keycloak.external_id_user_attribute_name.clone();
365        let users_find_behavior = match (
366            disable_users_find.unwrap_or_default(),
367            users_find_use_kc.unwrap_or_default(),
368        ) {
369            (true, _) => UsersFindBehavior::Disabled,
370            (false, false) => UsersFindBehavior::FromDatabase,
371            (false, true) => UsersFindBehavior::FromUserSearchBackend,
372        };
373
374        // Assemble the entire effective OIDC and user search configuration
375        let frontend = FrontendOidcConfiguration {
376            auth_base_url: auth_base_url.clone(),
377            client_id: client_id.clone(),
378        };
379        let controller = ControllerOidcConfiguration {
380            auth_base_url,
381            client_id: client_id.clone(),
382            client_secret: client_secret.clone().clone(),
383        };
384        let oidc = OidcConfiguration {
385            frontend,
386            controller,
387        };
388        let api = UserSearchConfiguration {
389            backend,
390            api_base_url,
391            client_id,
392            client_secret,
393            external_id_user_attribute_name,
394            users_find_behavior,
395        };
396        Ok(Ok(OidcAndUserSearchConfiguration {
397            oidc,
398            user_search: api,
399        }))
400    }
401
402    /// Creates a new Settings instance from the provided TOML file.
403    /// Specific fields can be set or overwritten with environment variables (See struct level docs for more details).
404    pub fn load(file_name: &str) -> Result<Self> {
405        let config = Config::builder()
406            .add_source(File::new(file_name, FileFormat::Toml))
407            .add_source(WarningSource(
408                Environment::with_prefix("K3K_CTRL")
409                    .prefix_separator("_")
410                    .separator("__"),
411            ))
412            .add_source(
413                Environment::with_prefix("OPENTALK_CTRL")
414                    .prefix_separator("_")
415                    .separator("__"),
416            )
417            .build()?;
418
419        let this: Self =
420            serde_path_to_error::deserialize(config).context(DeserializeConfigSnafu {
421                file_name: file_name.to_owned(),
422            })?;
423
424        Ok(this)
425    }
426}
427
428#[derive(Debug, Clone, Deserialize)]
429pub struct Database {
430    pub url: String,
431    #[serde(default = "default_max_connections")]
432    pub max_connections: u32,
433}
434
435fn default_max_connections() -> u32 {
436    100
437}
438
439/// Settings for Keycloak
440#[derive(Debug, Clone, Deserialize)]
441pub struct Keycloak {
442    pub base_url: Url,
443    pub realm: String,
444    pub client_id: ClientId,
445    pub client_secret: ClientSecret,
446    pub external_id_user_attribute_name: Option<String>,
447}
448
449#[derive(Debug, Clone, Deserialize)]
450pub struct Oidc {
451    pub authority: Url,
452    pub frontend: OidcFrontend,
453    pub controller: OidcController,
454}
455
456#[derive(Debug, Clone, Deserialize)]
457pub struct OidcFrontend {
458    pub authority: Option<Url>,
459    pub client_id: ClientId,
460}
461
462#[derive(Debug, Clone, Deserialize)]
463pub struct OidcController {
464    pub authority: Option<Url>,
465    pub client_id: ClientId,
466    pub client_secret: ClientSecret,
467}
468
469#[derive(Debug, Clone, Deserialize)]
470pub struct UserSearch {
471    #[serde(flatten)]
472    pub backend: UserSearchBackend,
473    pub api_base_url: Url,
474    pub client_id: Option<ClientId>,
475    pub client_secret: Option<ClientSecret>,
476    pub external_id_user_attribute_name: Option<String>,
477    #[serde(flatten)]
478    pub users_find_behavior: UsersFindBehavior,
479}
480
481#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
482#[serde(rename_all = "snake_case", tag = "backend")]
483pub enum UserSearchBackend {
484    KeycloakWebapi,
485}
486
487#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
488#[serde(rename_all = "snake_case", tag = "users_find_behavior")]
489pub enum UsersFindBehavior {
490    Disabled,
491    FromDatabase,
492    FromUserSearchBackend,
493}
494
495#[derive(Debug, Clone, Deserialize)]
496pub struct Http {
497    #[serde(default = "default_http_port")]
498    pub port: u16,
499    #[serde(default)]
500    pub tls: Option<HttpTls>,
501}
502
503impl Default for Http {
504    fn default() -> Self {
505        Self {
506            port: default_http_port(),
507            tls: None,
508        }
509    }
510}
511
512const fn default_http_port() -> u16 {
513    11311
514}
515
516#[derive(Debug, Clone, Deserialize)]
517pub struct HttpTls {
518    pub certificate: PathBuf,
519    pub private_key: PathBuf,
520}
521
522#[derive(Default, Debug, Clone, Deserialize)]
523pub struct Logging {
524    pub default_directives: Option<Vec<String>>,
525
526    pub otlp_tracing_endpoint: Option<String>,
527
528    pub service_name: Option<String>,
529
530    pub service_namespace: Option<String>,
531
532    pub service_instance_id: Option<String>,
533}
534
535#[derive(Debug, Clone, Deserialize)]
536pub struct Turn {
537    /// How long should a credential pair be valid, in seconds
538    #[serde(
539        deserialize_with = "duration_from_secs",
540        default = "default_turn_credential_lifetime"
541    )]
542    pub lifetime: Duration,
543    /// List of configured TURN servers.
544    pub servers: Vec<TurnServer>,
545}
546
547impl Default for Turn {
548    fn default() -> Self {
549        Self {
550            lifetime: default_turn_credential_lifetime(),
551            servers: vec![],
552        }
553    }
554}
555
556fn default_turn_credential_lifetime() -> Duration {
557    Duration::from_secs(60)
558}
559
560#[derive(Debug, Clone, Deserialize)]
561pub struct TurnServer {
562    // TURN URIs for this TURN server following rfc7065
563    pub uris: Vec<String>,
564    pub pre_shared_key: String,
565}
566
567#[derive(Clone, Debug, Deserialize)]
568pub struct Stun {
569    // STUN URIs for this TURN server following rfc7065
570    pub uris: Vec<String>,
571}
572
573#[derive(Debug, Clone, Deserialize)]
574pub struct RedisConfig {
575    #[serde(default = "redis_default_url")]
576    pub url: url::Url,
577}
578
579impl Default for RedisConfig {
580    fn default() -> Self {
581        Self {
582            url: redis_default_url(),
583        }
584    }
585}
586
587fn redis_default_url() -> url::Url {
588    url::Url::try_from("redis://localhost:6379/").expect("Invalid default redis URL")
589}
590
591#[derive(Debug, Clone, Deserialize)]
592pub struct RabbitMqConfig {
593    #[serde(default = "rabbitmq_default_url")]
594    pub url: String,
595    #[serde(default = "rabbitmq_default_min_connections")]
596    pub min_connections: u32,
597    #[serde(default = "rabbitmq_default_max_channels")]
598    pub max_channels_per_connection: u32,
599    /// Mail sending is disabled when this is None
600    #[serde(default)]
601    pub mail_task_queue: Option<String>,
602
603    /// Recording is disabled if this isn't set
604    #[serde(default)]
605    pub recording_task_queue: Option<String>,
606}
607
608impl Default for RabbitMqConfig {
609    fn default() -> Self {
610        Self {
611            url: rabbitmq_default_url(),
612            min_connections: rabbitmq_default_min_connections(),
613            max_channels_per_connection: rabbitmq_default_max_channels(),
614            mail_task_queue: None,
615            recording_task_queue: None,
616        }
617    }
618}
619
620fn rabbitmq_default_url() -> String {
621    "amqp://guest:guest@localhost:5672".to_owned()
622}
623
624fn rabbitmq_default_min_connections() -> u32 {
625    10
626}
627
628fn rabbitmq_default_max_channels() -> u32 {
629    100
630}
631
632#[derive(Clone, Debug, Deserialize)]
633pub struct Authz {
634    #[serde(default = "authz_default_synchronize_controller")]
635    pub synchronize_controllers: bool,
636}
637
638impl Default for Authz {
639    fn default() -> Self {
640        Self {
641            synchronize_controllers: authz_default_synchronize_controller(),
642        }
643    }
644}
645
646fn authz_default_synchronize_controller() -> bool {
647    true
648}
649
650#[derive(Clone, Debug, Deserialize)]
651pub struct Etcd {
652    pub urls: Vec<url::Url>,
653}
654
655#[derive(Clone, Debug, Deserialize)]
656pub struct Etherpad {
657    pub url: url::Url,
658    pub api_key: String,
659}
660
661#[derive(Clone, Debug, Deserialize)]
662pub struct Spacedeck {
663    pub url: url::Url,
664    pub api_key: String,
665}
666
667#[derive(Clone, Debug, Deserialize)]
668pub struct SubroomAudio {
669    #[serde(default)]
670    pub enable_whisper: bool,
671}
672
673#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
674pub struct Reports {
675    pub url: url::Url,
676    #[serde(default)]
677    pub template: ReportsTemplate,
678}
679
680#[derive(Clone, Debug, Deserialize, Default, PartialEq, Eq)]
681#[serde(rename_all = "snake_case")]
682pub enum ReportsTemplate {
683    /// Use the Template included with the application.
684    #[default]
685    BuiltIn,
686
687    /// Use the Template provided by the user configuration.
688    Inline(String),
689}
690
691#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
692#[serde(tag = "provider", rename_all = "snake_case")]
693pub enum SharedFolder {
694    Nextcloud {
695        url: url::Url,
696        username: String,
697        password: String,
698        #[serde(default)]
699        directory: String,
700        #[serde(default)]
701        expiry: Option<u64>,
702    },
703}
704
705fn duration_from_secs<'de, D>(deserializer: D) -> Result<Duration, D::Error>
706where
707    D: Deserializer<'de>,
708{
709    let duration: u64 = Deserialize::deserialize(deserializer)?;
710
711    Ok(Duration::from_secs(duration))
712}
713
714#[derive(Clone, Debug, Deserialize)]
715pub struct Avatar {
716    #[serde(default = "default_libravatar_url")]
717    pub libravatar_url: String,
718}
719
720impl Default for Avatar {
721    fn default() -> Self {
722        Self {
723            libravatar_url: default_libravatar_url(),
724        }
725    }
726}
727
728fn default_libravatar_url() -> String {
729    "https://seccdn.libravatar.org/avatar/".into()
730}
731
732#[derive(Clone, Debug, Deserialize)]
733pub struct CallIn {
734    pub tel: String,
735    pub enable_phone_mapping: bool,
736    pub default_country_code: phonenumber::country::Id,
737}
738
739#[derive(Clone, Default, Debug, Deserialize)]
740pub struct Defaults {
741    #[serde(default = "default_user_language")]
742    pub user_language: Language,
743    #[serde(default)]
744    pub screen_share_requires_permission: bool,
745    #[serde(default)]
746    pub disabled_features: BTreeSet<ModuleFeatureId>,
747}
748
749fn default_user_language() -> Language {
750    "en-US".parse().expect("valid language")
751}
752
753#[derive(Clone, Default, Debug, Deserialize)]
754pub struct Endpoints {
755    pub disable_users_find: Option<bool>,
756    pub users_find_use_kc: Option<bool>,
757    #[serde(default)]
758    pub event_invite_external_email_address: bool,
759    #[serde(default)]
760    pub disallow_custom_display_name: bool,
761    #[serde(default)]
762    pub disable_openapi: bool,
763}
764
765#[derive(Clone, Debug, Deserialize)]
766pub struct MinIO {
767    pub uri: String,
768    pub bucket: String,
769    pub access_key: String,
770    pub secret_key: String,
771}
772
773#[derive(Debug, Default, Clone, Deserialize)]
774pub struct Metrics {
775    pub allowlist: Vec<cidr::IpInet>,
776}
777
778#[derive(Debug, Clone, Deserialize)]
779#[serde(rename_all = "snake_case", tag = "assignment")]
780pub enum TenantAssignment {
781    Static {
782        static_tenant_id: String,
783    },
784    ByExternalTenantId {
785        #[serde(default = "default_external_tenant_id_user_attribute_name")]
786        external_tenant_id_user_attribute_name: String,
787    },
788}
789
790fn default_external_tenant_id_user_attribute_name() -> String {
791    "tenant_id".to_owned()
792}
793
794impl Default for TenantAssignment {
795    fn default() -> Self {
796        Self::Static {
797            static_tenant_id: String::from("OpenTalkDefaultTenant"),
798        }
799    }
800}
801
802#[derive(Default, Debug, Clone, Deserialize)]
803pub struct Tenants {
804    #[serde(default, flatten)]
805    pub assignment: TenantAssignment,
806}
807
808#[derive(Debug, Clone, Deserialize)]
809#[serde(rename_all = "snake_case", tag = "assignment")]
810pub enum TariffAssignment {
811    Static { static_tariff_name: String },
812    ByExternalTariffId,
813}
814
815impl Default for TariffAssignment {
816    fn default() -> Self {
817        Self::Static {
818            static_tariff_name: String::from("OpenTalkDefaultTariff"),
819        }
820    }
821}
822
823#[derive(Default, Debug, Clone, Deserialize)]
824pub struct TariffStatusMapping {
825    pub downgraded_tariff_name: String,
826    pub default: FxHashSet<String>,
827    pub paid: FxHashSet<String>,
828    pub downgraded: FxHashSet<String>,
829}
830
831#[derive(Default, Debug, Clone, Deserialize)]
832pub struct Tariffs {
833    #[serde(default, flatten)]
834    pub assignment: TariffAssignment,
835
836    #[serde(default)]
837    pub status_mapping: Option<TariffStatusMapping>,
838}
839
840#[derive(Debug, Clone, Deserialize)]
841pub struct LiveKitSettings {
842    pub api_key: String,
843    pub api_secret: String,
844    pub public_url: String,
845
846    // for backwards compatibility
847    #[serde(alias = "url")]
848    pub service_url: String,
849}
850
851#[cfg(test)]
852mod tests {
853    use std::env;
854
855    use pretty_assertions::assert_eq;
856    use serde_json::json;
857
858    use super::*;
859
860    #[test]
861    fn settings_env_vars_overwrite_config() -> Result<()> {
862        // Sanity check
863        let settings = Settings::load("../../extra/example.toml")?;
864
865        assert_eq!(
866            settings.database.url,
867            "postgres://postgres:password123@localhost:5432/opentalk"
868        );
869        assert_eq!(settings.http.port, 11311u16);
870
871        // Set environment variables to overwrite default config file
872        let env_db_url = "postgres://envtest:password@localhost:5432/opentalk".to_string();
873        let env_http_port: u16 = 8000;
874        let screen_share_requires_permission = true;
875        env::set_var("OPENTALK_CTRL_DATABASE__URL", &env_db_url);
876        env::set_var("OPENTALK_CTRL_HTTP__PORT", env_http_port.to_string());
877        env::set_var(
878            "OPENTALK_CTRL_DEFAULTS__SCREEN_SHARE_REQUIRES_PERMISSION",
879            screen_share_requires_permission.to_string(),
880        );
881
882        let settings = Settings::load("../../extra/example.toml")?;
883
884        assert_eq!(settings.database.url, env_db_url);
885        assert_eq!(settings.http.port, env_http_port);
886        assert_eq!(
887            settings.defaults.screen_share_requires_permission,
888            screen_share_requires_permission
889        );
890
891        Ok(())
892    }
893
894    #[test]
895    fn shared_folder_provider_nextcloud() {
896        let shared_folder = SharedFolder::Nextcloud {
897            url: "https://nextcloud.example.org/".parse().unwrap(),
898            username: "exampleuser".to_string(),
899            password: "v3rys3cr3t".to_string(),
900            directory: "meetings/opentalk".to_string(),
901            expiry: Some(34),
902        };
903        let json = json!({
904            "provider": "nextcloud",
905            "url": "https://nextcloud.example.org/",
906            "username": "exampleuser",
907            "password": "v3rys3cr3t",
908            "directory": "meetings/opentalk",
909            "expiry": 34,
910        });
911
912        assert_eq!(
913            serde_json::from_value::<SharedFolder>(json).unwrap(),
914            shared_folder
915        );
916    }
917
918    #[test]
919    fn meeting_report_settings() {
920        let toml_settings: Reports = toml::from_str(
921            r#"
922        url = "http://localhost"
923        "#,
924        )
925        .unwrap();
926        assert_eq!(
927            toml_settings,
928            Reports {
929                url: "http://localhost".parse().unwrap(),
930                template: ReportsTemplate::BuiltIn
931            }
932        );
933
934        let toml_settings: Reports = toml::from_str(
935            r#"
936        url = "http://localhost"
937        template.inline = "lorem ipsum"
938        "#,
939        )
940        .unwrap();
941        assert_eq!(
942            toml_settings,
943            Reports {
944                url: "http://localhost".parse().unwrap(),
945                template: ReportsTemplate::Inline("lorem ipsum".to_string())
946            }
947        );
948    }
949}