diff --git a/services/blob/src/config.rs b/services/blob/src/config.rs index 12dfe5622..3899c1174 100644 --- a/services/blob/src/config.rs +++ b/services/blob/src/config.rs @@ -1,51 +1,55 @@ use anyhow::Result; use clap::Parser; use once_cell::sync::Lazy; use tracing::info; use crate::constants::{ DEFAULT_HTTP_PORT, DEFAULT_S3_BUCKET_NAME, S3_BUCKET_ENV_VAR, }; #[derive(Parser)] #[command(version, about, long_about = None)] pub struct AppConfig { /// HTTP server listening port #[arg(long, default_value_t = DEFAULT_HTTP_PORT)] pub http_port: u16, /// AWS Localstack service URL #[arg(env = "LOCALSTACK_ENDPOINT")] #[arg(long)] pub localstack_endpoint: Option, #[arg(env = S3_BUCKET_ENV_VAR)] #[arg(long, default_value_t = DEFAULT_S3_BUCKET_NAME.to_string())] pub s3_bucket_name: String, + /// Identity service endpoint + #[arg(env = "IDENTITY_SERVICE_ENDPOINT")] + #[arg(long, default_value = "http://localhost:50054")] + pub identity_endpoint: String, } /// Stores configuration parsed from command-line arguments /// and environment variables pub static CONFIG: Lazy = Lazy::new(AppConfig::parse); /// Processes the command-line arguments and environment variables. /// Should be called at the beginning of the `main()` function. pub(super) fn parse_cmdline_args() -> Result<&'static AppConfig> { // force evaluation of the lazy initialized config let cfg = Lazy::force(&CONFIG); if cfg.s3_bucket_name != DEFAULT_S3_BUCKET_NAME { info!("Using custom S3 bucket: {}", &cfg.s3_bucket_name); } Ok(cfg) } /// Provides region/credentials configuration for AWS SDKs pub async fn load_aws_config() -> aws_config::SdkConfig { let mut config_builder = aws_config::from_env(); if let Some(endpoint) = &CONFIG.localstack_endpoint { info!("Using Localstack. AWS endpoint URL: {}", endpoint); config_builder = config_builder.endpoint_url(endpoint); } config_builder.load().await } diff --git a/services/blob/src/http/mod.rs b/services/blob/src/http/mod.rs index a6e577255..8d60e2ce2 100644 --- a/services/blob/src/http/mod.rs +++ b/services/blob/src/http/mod.rs @@ -1,42 +1,47 @@ use crate::{config::CONFIG, service::BlobService}; use actix_web::{web, App, HttpServer}; use anyhow::Result; +use comm_services_lib::auth::AuthService; use tracing::info; mod errors; mod utils; mod handlers { pub(super) mod blob; } -pub async fn run_http_server(blob_service: BlobService) -> Result<()> { +pub async fn run_http_server( + blob_service: BlobService, + auth_service: AuthService, +) -> Result<()> { info!( "Starting HTTP server listening at port {}", CONFIG.http_port ); HttpServer::new(move || { App::new() .wrap(tracing_actix_web::TracingLogger::default()) .wrap(comm_services_lib::http::cors_config( CONFIG.localstack_endpoint.is_some(), )) + .app_data(auth_service.to_owned()) .app_data(web::Data::new(blob_service.to_owned())) .service( web::resource("/blob/{holder}") .route(web::get().to(handlers::blob::get_blob_handler)), ) .service( web::resource("/blob") .route(web::put().to(handlers::blob::upload_blob_handler)) .route(web::post().to(handlers::blob::assign_holder_handler)) .route(web::delete().to(handlers::blob::remove_holder_handler)), ) }) .bind(("0.0.0.0", CONFIG.http_port))? .run() .await?; Ok(()) } diff --git a/services/blob/src/main.rs b/services/blob/src/main.rs index 05c888c9d..2f01c0e0a 100644 --- a/services/blob/src/main.rs +++ b/services/blob/src/main.rs @@ -1,44 +1,46 @@ pub mod config; pub mod constants; pub mod database; pub mod http; pub mod s3; pub mod service; pub mod tools; use anyhow::Result; +use comm_services_lib::auth::AuthService; use tracing_subscriber::filter::{EnvFilter, LevelFilter}; use crate::service::BlobServiceConfig; fn configure_logging() -> Result<()> { let filter = EnvFilter::builder() .with_default_directive(LevelFilter::INFO.into()) .with_env_var(constants::LOG_LEVEL_ENV_VAR) .from_env_lossy(); let subscriber = tracing_subscriber::fmt().with_env_filter(filter).finish(); tracing::subscriber::set_global_default(subscriber)?; Ok(()) } #[tokio::main] async fn main() -> Result<()> { configure_logging()?; - config::parse_cmdline_args()?; + let config = config::parse_cmdline_args()?; let aws_config = config::load_aws_config().await; let db = database::DatabaseClient::new(&aws_config); let s3 = s3::S3Client::new(&aws_config); + let auth_service = AuthService::new(&aws_config, &config.identity_endpoint); - let service = service::BlobService::new( + let blob_service = service::BlobService::new( db, s3, BlobServiceConfig { instant_delete_orphaned_blobs: true, ..Default::default() }, ); - crate::http::run_http_server(service).await + crate::http::run_http_server(blob_service, auth_service).await } diff --git a/services/reports/src/config.rs b/services/reports/src/config.rs index e8b26e1ca..b6e4ecd4f 100644 --- a/services/reports/src/config.rs +++ b/services/reports/src/config.rs @@ -1,99 +1,104 @@ use anyhow::Result; use clap::{ArgAction, Parser}; use comm_services_lib::blob::client::Url; use once_cell::sync::Lazy; use tracing::{info, warn}; use crate::email::config::{EmailArgs, EmailConfig}; // environment variable names const ENV_LOCALSTACK_ENDPOINT: &str = "LOCALSTACK_ENDPOINT"; const ENV_BLOB_SERVICE_URL: &str = "BLOB_SERVICE_URL"; const ENV_PUBLIC_URL: &str = "PUBLIC_URL"; /// Base URL on which Reports service is accessible. /// Used for sending e-mail links. pub static SERVICE_PUBLIC_URL: Lazy = Lazy::new(|| { std::env::var(ENV_PUBLIC_URL) .ok() .filter(|s| !s.is_empty()) .unwrap_or_else(|| "http://localhost:50056".to_string()) }); #[derive(Parser)] #[command(version, about, long_about = None)] pub struct AppConfig { /// HTTP server listening port #[arg(long, default_value_t = 50056)] pub http_port: u16, #[arg(env = ENV_BLOB_SERVICE_URL)] #[arg(long, default_value = "http://localhost:50053")] pub blob_service_url: Url, + /// Identity service endpoint + #[arg(env = "IDENTITY_SERVICE_ENDPOINT")] + #[arg(long, default_value = "http://localhost:50054")] + pub identity_endpoint: String, + /// Should reports be encrypted? Note that this flag disables encryption /// which is enabled by default. #[arg(long = "no-encrypt", action = ArgAction::SetFalse)] pub encrypt_reports: bool, /// AWS Localstack service URL #[arg(env = ENV_LOCALSTACK_ENDPOINT)] #[arg(long)] localstack_endpoint: Option, /// This config shouldn't be used directly. It's used for parsing purposes /// only. Use [`AppConfig::email_config()`] instead. #[command(flatten)] email_args: EmailArgs, } impl AppConfig { pub fn is_dev(&self) -> bool { self.localstack_endpoint.is_some() } pub fn email_config(&self) -> Option { // we return None in case of error because this should've already been // checked by parse_cmdline_args() self.email_args.parse().ok().flatten() } } /// Stores configuration parsed from command-line arguments /// and environment variables pub static CONFIG: Lazy = Lazy::new(AppConfig::parse); /// Processes the command-line arguments and environment variables. /// Should be called at the beginning of the `main()` function. pub(super) fn parse_cmdline_args() -> Result<&'static AppConfig> { // force evaluation of the lazy initialized config let cfg = Lazy::force(&CONFIG); // initialize e-mail config to check for errors match cfg.email_args.parse()? { Some(_) => { info!("E-mail config found. E-mail notifications are enabled."); } None => { warn!("E-mail config is disabled or missing! E-mails will not be sent."); } } if !cfg.encrypt_reports { warn!("Encryption disabled. Reports will be stored in plaintext!"); } Ok(cfg) } /// Provides region/credentials configuration for AWS SDKs pub async fn load_aws_config() -> aws_config::SdkConfig { let mut config_builder = aws_config::from_env(); if let Some(endpoint) = &CONFIG.localstack_endpoint { info!("Using Localstack. AWS Endpoint URL: {}", endpoint); config_builder = config_builder.endpoint_url(endpoint); } config_builder.load().await } diff --git a/services/reports/src/http/mod.rs b/services/reports/src/http/mod.rs index 524d095ae..8caccae05 100644 --- a/services/reports/src/http/mod.rs +++ b/services/reports/src/http/mod.rs @@ -1,118 +1,123 @@ use actix_web::error::{ ErrorBadRequest, ErrorInternalServerError, ErrorNotFound, ErrorServiceUnavailable, ErrorUnsupportedMediaType, }; use actix_web::{web, App, HttpResponse, HttpServer, ResponseError}; use anyhow::Result; +use comm_services_lib::auth::AuthService; use http::StatusCode; use tracing::{debug, error, info, trace, warn}; use crate::config::CONFIG; use crate::constants::REQUEST_BODY_JSON_SIZE_LIMIT; use crate::service::{ReportsService, ReportsServiceError}; mod handlers; -pub async fn run_http_server(service: ReportsService) -> Result<()> { +pub async fn run_http_server( + reports_service: ReportsService, + auth_service: AuthService, +) -> Result<()> { use actix_web::middleware::{Logger, NormalizePath}; use comm_services_lib::http::cors_config; use tracing_actix_web::TracingLogger; info!( "Starting HTTP server listening at port {}", CONFIG.http_port ); HttpServer::new(move || { let json_cfg = web::JsonConfig::default().limit(REQUEST_BODY_JSON_SIZE_LIMIT); App::new() .app_data(json_cfg) - .app_data(service.to_owned()) + .app_data(reports_service.to_owned()) + .app_data(auth_service.to_owned()) .wrap(Logger::default()) .wrap(TracingLogger::default()) .wrap(NormalizePath::trim()) .wrap(cors_config(CONFIG.is_dev())) // Health endpoint for load balancers checks .route("/health", web::get().to(HttpResponse::Ok)) .service( web::scope("/reports") .service(handlers::post_reports) .service(handlers::query_reports) .service(handlers::get_single_report) .service(handlers::redux_devtools_import), ) }) .bind(("0.0.0.0", CONFIG.http_port))? .run() .await?; Ok(()) } fn handle_reports_service_error(err: &ReportsServiceError) -> actix_web::Error { use aws_sdk_dynamodb::Error as DynamoDBError; use comm_services_lib::database::Error as DBError; trace!("Handling reports service error: {:?}", err); match err { ReportsServiceError::UnsupportedReportType => { ErrorUnsupportedMediaType("unsupported report type") } ReportsServiceError::SerdeError(err) => { error!("Serde error: {0:?} - {0}", err); ErrorInternalServerError("internal error") } ReportsServiceError::ParseError(err) => { debug!("Parse error: {0:?} - {0}", err); ErrorBadRequest("invalid input format") } ReportsServiceError::BlobError(err) => { error!("Blob Service error: {0:?} - {0}", err); ErrorInternalServerError("internal error") } ReportsServiceError::DatabaseError(db_err) => match db_err { // retriable errors DBError::MaxRetriesExceeded | DBError::AwsSdk( DynamoDBError::InternalServerError(_) | DynamoDBError::ProvisionedThroughputExceededException(_) | DynamoDBError::RequestLimitExceeded(_), ) => { warn!("AWS transient error occurred"); ErrorServiceUnavailable("please retry") } err => { error!("Unexpected database error: {0:?} - {0}", err); ErrorInternalServerError("internal error") } }, err => { error!("Received an unexpected error: {0:?} - {0}", err); ErrorInternalServerError("server error") } } } /// This allow us to `await?` blob service calls in HTTP handlers impl ResponseError for ReportsServiceError { fn error_response(&self) -> HttpResponse { handle_reports_service_error(self).error_response() } fn status_code(&self) -> StatusCode { handle_reports_service_error(self) .as_response_error() .status_code() } } trait NotFoundHandler { /// Returns `Ok(T)` if `self` is `Some(T)`, /// otherwise returns a `404 Not Found` error. fn unwrap_or_404(self) -> actix_web::Result; } impl NotFoundHandler for Option { fn unwrap_or_404(self) -> actix_web::Result { self.ok_or_else(|| ErrorNotFound("not found")) } } diff --git a/services/reports/src/main.rs b/services/reports/src/main.rs index fefdb80f2..967d6c118 100644 --- a/services/reports/src/main.rs +++ b/services/reports/src/main.rs @@ -1,42 +1,43 @@ pub mod config; pub mod constants; pub mod database; pub mod email; pub mod http; pub mod report_types; pub mod report_utils; pub mod service; use anyhow::Result; -use comm_services_lib::blob::client::BlobServiceClient; +use comm_services_lib::{auth::AuthService, blob::client::BlobServiceClient}; use service::ReportsService; use tracing_subscriber::filter::{EnvFilter, LevelFilter}; fn configure_logging() -> Result<()> { let filter = EnvFilter::builder() .with_default_directive(LevelFilter::INFO.into()) .with_env_var(EnvFilter::DEFAULT_ENV) .from_env_lossy(); // init HTTP logger - it relies on 'log' instead of 'tracing' // so we have to initialize a polyfill tracing_log::LogTracer::init()?; let subscriber = tracing_subscriber::fmt().with_env_filter(filter).finish(); tracing::subscriber::set_global_default(subscriber)?; Ok(()) } #[tokio::main] async fn main() -> Result<()> { configure_logging()?; let cfg = config::parse_cmdline_args()?; let aws_config = config::load_aws_config().await; let email_config = cfg.email_config(); let db = database::client::DatabaseClient::new(&aws_config); let blob_client = BlobServiceClient::new(cfg.blob_service_url.clone()); - let service = ReportsService::new(db, blob_client, email_config); + let reports_service = ReportsService::new(db, blob_client, email_config); + let auth_service = AuthService::new(&aws_config, &cfg.identity_endpoint); - crate::http::run_http_server(service).await + crate::http::run_http_server(reports_service, auth_service).await }