scufflecloud_big_bin/
main.rs

1//! Big binary for scuffle.cloud that contains all services.
2#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
3#![cfg_attr(docsrs, feature(doc_auto_cfg))]
4// #![deny(missing_docs)]
5#![deny(unsafe_code)]
6#![deny(unreachable_pub)]
7#![deny(clippy::mod_module_files)]
8
9use std::sync::Arc;
10
11use anyhow::Context;
12use diesel_async::pooled_connection::bb8;
13use geo_ip::resolver::GeoIpResolver;
14use scuffle_batching::DataLoader;
15use scuffle_bootstrap_telemetry::opentelemetry;
16use scuffle_bootstrap_telemetry::opentelemetry_sdk::logs::SdkLoggerProvider;
17use scuffle_bootstrap_telemetry::opentelemetry_sdk::trace::SdkTracerProvider;
18use tonic::transport::ClientTlsConfig;
19use tracing_subscriber::Layer;
20use tracing_subscriber::layer::SubscriberExt;
21use tracing_subscriber::util::SubscriberInitExt;
22
23mod config;
24mod dataloaders;
25
26type EmailClientPb = pb::scufflecloud::email::v1::email_service_client::EmailServiceClient<tonic::transport::Channel>;
27
28struct Global {
29    config: config::Config,
30    database: bb8::Pool<diesel_async::AsyncPgConnection>,
31    user_loader: DataLoader<dataloaders::UserLoader>,
32    organization_loader: DataLoader<dataloaders::OrganizationLoader>,
33    organization_member_by_user_id_loader: DataLoader<dataloaders::OrganizationMemberByUserIdLoader>,
34    external_http_client: reqwest::Client,
35    webauthn: webauthn_rs::Webauthn,
36    open_telemetry: opentelemetry::OpenTelemetry,
37    redis: fred::clients::Pool,
38    email_service_client: EmailClientPb,
39    geoip_resolver: GeoIpResolver,
40    aws_ses_req_signer: reqsign::Signer<reqsign::aws::Credential>,
41    mtls_root_cert: Vec<u8>,
42    mtls_core_cert: Vec<u8>,
43    mtls_core_private_key: Vec<u8>,
44    mtls_email_cert: Vec<u8>,
45    mtls_email_private_key: Vec<u8>,
46}
47
48impl scuffle_signal::SignalConfig for Global {}
49
50impl core_traits::ConfigInterface for Global {
51    fn dashboard_origin(&self) -> &url::Url {
52        &self.config.dashboard_origin
53    }
54
55    fn email_from_name(&self) -> &str {
56        &self.config.email_from_name
57    }
58
59    fn email_from_address(&self) -> &str {
60        &self.config.email_from_address
61    }
62
63    fn google_oauth2_config(&self) -> core_traits::GoogleOAuth2Config<'_> {
64        core_traits::GoogleOAuth2Config {
65            client_id: self.config.google_oauth2.client_id.as_str().into(),
66            client_secret: self.config.google_oauth2.client_secret.as_str().into(),
67        }
68    }
69
70    fn service_bind(&self) -> std::net::SocketAddr {
71        self.config.core_bind
72    }
73
74    fn swagger_ui_enabled(&self) -> bool {
75        self.config.swagger_ui
76    }
77
78    fn timeout_config(&self) -> core_traits::TimeoutConfig {
79        core_traits::TimeoutConfig {
80            new_user_email_request: self.config.timeouts.new_user_email_request,
81            magic_link_request: self.config.timeouts.magic_link_request,
82            max_request: self.config.timeouts.max_request_lifetime,
83            mfa: self.config.timeouts.mfa,
84            user_session: self.config.timeouts.user_session,
85            user_session_request: self.config.timeouts.user_session_request,
86            user_session_token: self.config.timeouts.user_session_token,
87        }
88    }
89
90    fn turnstile_secret_key(&self) -> &str {
91        &self.config.turnstile_secret_key
92    }
93}
94
95impl core_traits::DatabaseInterface for Global {
96    type Connection<'a>
97        = diesel_async::pooled_connection::bb8::PooledConnection<'a, diesel_async::pg::AsyncPgConnection>
98    where
99        Self: 'a;
100
101    async fn db(&self) -> anyhow::Result<Self::Connection<'_>> {
102        self.database.get().await.context("failed to get database connection")
103    }
104}
105
106#[allow(refining_impl_trait)]
107impl core_traits::DataloaderInterface for Global {
108    fn organization_loader(&self) -> &DataLoader<dataloaders::OrganizationLoader> {
109        &self.organization_loader
110    }
111
112    fn user_loader(&self) -> &DataLoader<dataloaders::UserLoader> {
113        &self.user_loader
114    }
115
116    fn organization_member_by_user_id_loader(&self) -> &DataLoader<dataloaders::OrganizationMemberByUserIdLoader> {
117        &self.organization_member_by_user_id_loader
118    }
119}
120
121impl core_traits::HttpClientInterface for Global {
122    fn external_http_client(&self) -> &reqwest::Client {
123        &self.external_http_client
124    }
125}
126
127impl geo_ip::GeoIpInterface for Global {
128    fn geo_ip_resolver(&self) -> &GeoIpResolver {
129        &self.geoip_resolver
130    }
131
132    fn reverse_proxy_config(&self) -> Option<geo_ip::ReverseProxyConfig<'_>> {
133        let config = self.config.reverse_proxy.as_ref()?;
134        Some(geo_ip::ReverseProxyConfig {
135            internal_networks: config.internal_networks.as_slice().into(),
136            ip_header: config.ip_header.as_str().into(),
137            trusted_proxies: config.trusted_proxies.as_slice().into(),
138        })
139    }
140}
141
142impl core_traits::EmailInterface for Global {
143    fn email_service(&self) -> impl core_traits::EmailServiceClient {
144        struct EmailServiceClient<'a>(&'a EmailClientPb);
145
146        impl core_traits::EmailServiceClient for EmailServiceClient<'_> {
147            fn send_email(
148                &self,
149                email: impl tonic::IntoRequest<pb::scufflecloud::email::v1::SendEmailRequest>,
150            ) -> impl Future<Output = Result<tonic::Response<()>, tonic::Status>> + Send {
151                let email = email.into_request();
152                let mut client = self.0.clone();
153                async move { client.send_email(email).await }
154            }
155        }
156
157        EmailServiceClient(&self.email_service_client)
158    }
159}
160
161impl core_traits::RedisInterface for Global {
162    type RedisConnection<'a>
163        = fred::clients::Pool
164    where
165        Self: 'a;
166
167    fn redis(&self) -> &Self::RedisConnection<'_> {
168        &self.redis
169    }
170}
171
172impl core_traits::WebAuthnInterface for Global {
173    fn webauthn(&self) -> &webauthn_rs::Webauthn {
174        &self.webauthn
175    }
176}
177
178impl core_traits::MtlsInterface for Global {
179    fn mtls_root_cert_pem(&self) -> &[u8] {
180        &self.mtls_root_cert
181    }
182
183    fn mtls_cert_pem(&self) -> &[u8] {
184        &self.mtls_core_cert
185    }
186
187    fn mtls_private_key_pem(&self) -> &[u8] {
188        &self.mtls_core_private_key
189    }
190}
191
192impl core_traits::Global for Global {}
193
194impl email_traits::ConfigInterface for Global {
195    fn service_bind(&self) -> std::net::SocketAddr {
196        self.config.email_bind
197    }
198}
199
200impl email_traits::AwsInterface for Global {
201    fn aws_region(&self) -> &str {
202        &self.config.aws.region
203    }
204
205    fn aws_ses_req_signer(&self) -> &reqsign::Signer<reqsign::aws::Credential> {
206        &self.aws_ses_req_signer
207    }
208}
209
210impl email_traits::HttpClientInterface for Global {
211    fn external_http_client(&self) -> &reqwest::Client {
212        &self.external_http_client
213    }
214}
215
216impl email_traits::MtlsInterface for Global {
217    fn mtls_root_cert_pem(&self) -> &[u8] {
218        &self.mtls_root_cert
219    }
220
221    fn mtls_cert_pem(&self) -> &[u8] {
222        &self.mtls_email_cert
223    }
224
225    fn mtls_private_key_pem(&self) -> &[u8] {
226        &self.mtls_email_private_key
227    }
228}
229
230impl email_traits::Global for Global {}
231
232impl scuffle_bootstrap_telemetry::TelemetryConfig for Global {
233    fn enabled(&self) -> bool {
234        self.config.telemetry.is_some()
235    }
236
237    fn bind_address(&self) -> Option<std::net::SocketAddr> {
238        self.config.telemetry.as_ref().map(|telemetry| telemetry.bind)
239    }
240
241    fn http_server_name(&self) -> &str {
242        "scufflecloud-telemetry"
243    }
244
245    fn opentelemetry(&self) -> Option<&opentelemetry::OpenTelemetry> {
246        Some(&self.open_telemetry)
247    }
248}
249
250impl scuffle_bootstrap::Global for Global {
251    type Config = config::Config;
252
253    async fn init(config: Self::Config) -> anyhow::Result<Arc<Self>> {
254        tracing_subscriber::registry()
255            .with(
256                tracing_subscriber::fmt::layer()
257                    .with_filter(tracing_subscriber::EnvFilter::from_default_env().add_directive(config.level.parse()?)),
258            )
259            .init();
260
261        if rustls::crypto::aws_lc_rs::default_provider().install_default().is_err() {
262            anyhow::bail!("failed to install aws-lc-rs as default TLS provider");
263        }
264
265        let maxminddb_data = tokio::fs::read(&config.maxminddb_path)
266            .await
267            .context("failed to read maxmind db path")?;
268        let geoip_resolver = GeoIpResolver::new_from_data(maxminddb_data).context("failed to parse maxmind db")?;
269
270        // TODO: Remove mTLS from this binary once we don't use a real connection anymore.
271        // mTLS
272        let root_cert = std::fs::read(&config.mtls.root_cert_path).context("failed to read mTLS root cert file")?;
273        let core_cert = std::fs::read(&config.mtls.core_cert_path).context("failed to read core mTLS cert file")?;
274        let core_private_key =
275            std::fs::read(&config.mtls.core_key_path).context("failed to read core mTLS private key file")?;
276        let email_cert = std::fs::read(&config.mtls.email_cert_path).context("failed to read email mTLS cert file")?;
277        let email_private_key =
278            std::fs::read(&config.mtls.email_key_path).context("failed to read email mTLS private key file")?;
279
280        let client_tls_config = ClientTlsConfig::new()
281            .ca_certificate(tonic::transport::Certificate::from_pem(&root_cert))
282            .identity(tonic::transport::Identity::from_pem(&core_cert, &core_private_key));
283
284        let email_service_address = format!("http://{}", config.email_bind);
285        // Connect lazily because the service isn't up yet.
286        let email_service_channel = tonic::transport::Endpoint::from_shared(email_service_address)
287            .context("create channel to email service")?
288            .tls_config(client_tls_config)
289            .context("configure TLS for email service channel")?
290            .connect_lazy();
291        let email_service_client =
292            pb::scufflecloud::email::v1::email_service_client::EmailServiceClient::new(email_service_channel);
293
294        let Some(db_url) = config.db_url.as_deref() else {
295            anyhow::bail!("DATABASE_URL is not set");
296        };
297
298        tracing::info!(db_url = config.db_url, "creating database connection pool");
299
300        let database = bb8::Pool::builder()
301            .build(diesel_async::pooled_connection::AsyncDieselConnectionManager::new(db_url))
302            .await
303            .context("build database pool")?;
304
305        let user_loader = dataloaders::UserLoader::new(database.clone());
306        let organization_loader = dataloaders::OrganizationLoader::new(database.clone());
307        let organization_member_by_user_id_loader = dataloaders::OrganizationMemberByUserIdLoader::new(database.clone());
308
309        // TODO: find someway to restrict this client to only making requests to external ips.
310        // likely via dns.
311        let external_http_client = reqwest::Client::builder()
312            .user_agent(&config.service_name)
313            .tls_built_in_root_certs(true)
314            .use_rustls_tls()
315            .build()
316            .context("create HTTP client")?;
317
318        let webauthn = webauthn_rs::WebauthnBuilder::new(&config.rp_id, &config.dashboard_origin)
319            .context("build webauthn")?
320            .allow_subdomains(true)
321            .timeout(config.timeouts.mfa)
322            .build()
323            .context("initialize webauthn")?;
324
325        let tracer = SdkTracerProvider::default();
326        opentelemetry::global::set_tracer_provider(tracer.clone());
327
328        let logger = SdkLoggerProvider::builder().build();
329
330        let open_telemetry = crate::opentelemetry::OpenTelemetry::new()
331            .with_traces(tracer)
332            .with_logs(logger);
333
334        let redis = config.redis.setup().await?;
335
336        let provider = reqsign::aws::StaticCredentialProvider::new(&config.aws.access_key_id, &config.aws.secret_access_key);
337        let signer = reqsign::aws::RequestSigner::new("ses", &config.aws.region);
338        let aws_ses_req_signer = reqsign::Signer::new(reqsign::Context::new(), provider, signer);
339
340        Ok(Arc::new(Self {
341            config,
342            database,
343            user_loader,
344            organization_loader,
345            organization_member_by_user_id_loader,
346            external_http_client,
347            webauthn,
348            open_telemetry,
349            redis,
350            email_service_client,
351            geoip_resolver,
352            aws_ses_req_signer,
353            mtls_root_cert: root_cert,
354            mtls_core_cert: core_cert,
355            mtls_core_private_key: core_private_key,
356            mtls_email_cert: email_cert,
357            mtls_email_private_key: email_private_key,
358        }))
359    }
360}
361
362scuffle_bootstrap::main! {
363    Global {
364        scuffle_signal::SignalSvc,
365        scufflecloud_core::services::CoreSvc::<Global>::default(),
366        email::services::EmailSvc::<Global>::default(),
367    }
368}