scufflecloud_big_bin/
main.rs1#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
3#![cfg_attr(docsrs, feature(doc_auto_cfg))]
4#![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 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 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 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}