diff --git a/shared/comm-lib/src/auth/mod.rs b/shared/comm-lib/src/auth/mod.rs index 6af54b612..76de0baad 100644 --- a/shared/comm-lib/src/auth/mod.rs +++ b/shared/comm-lib/src/auth/mod.rs @@ -1,7 +1,27 @@ #[cfg(feature = "aws")] mod service; mod types; #[cfg(feature = "aws")] pub use service::*; pub use types::*; + +use crate::constants::DISABLE_CSAT_VERIFICATION_ENV_VAR; +use once_cell::sync::Lazy; + +static CSAT_VERIFICATION_DISABLED: Lazy = Lazy::new(|| { + let is_disabled = std::env::var(DISABLE_CSAT_VERIFICATION_ENV_VAR) + .is_ok_and(|value| ["1", "true"].contains(&value.as_str())); + + if is_disabled { + tracing::warn!( + "CSAT verification is disabled! All requests will be unauthenticated!" + ); + } + + is_disabled +}); + +pub fn is_csat_verification_disabled() -> bool { + *Lazy::force(&CSAT_VERIFICATION_DISABLED) +} diff --git a/shared/comm-lib/src/constants.rs b/shared/comm-lib/src/constants.rs index 903526d4e..7e5ada746 100644 --- a/shared/comm-lib/src/constants.rs +++ b/shared/comm-lib/src/constants.rs @@ -1,19 +1,24 @@ // 400KiB limit (in docs there is KB but they mean KiB) - // https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ServiceQuotas.html // This includes both attribute names' and values' lengths pub const DDB_ITEM_SIZE_LIMIT: usize = 1024 * 400; // 4MB limit // WARNING: use keeping in mind that grpc adds its own headers to messages // https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md // so the message that actually is being sent over the network looks like this // [Compressed-Flag] [Message-Length] [Message] // [Compressed-Flag] 1 byte - added by grpc // [Message-Length] 4 bytes - added by grpc // [Message] N bytes - actual data // so for every message we get 5 additional bytes of data // as mentioned here // https://github.com/grpc/grpc/issues/15734#issuecomment-396962671 // grpc stream may contain more than one message pub const GRPC_CHUNK_SIZE_LIMIT: usize = 4 * 1024 * 1024; pub const GRPC_METADATA_SIZE_PER_MESSAGE: usize = 5; + +/// Environment variable, that when set to `true` disables verifying access +/// token in service requests. +pub const DISABLE_CSAT_VERIFICATION_ENV_VAR: &str = + "COMM_SERVICES_DISABLE_CSAT_VERIFICATION"; diff --git a/shared/comm-lib/src/http/auth.rs b/shared/comm-lib/src/http/auth.rs index 5f9694aa2..8df83bac2 100644 --- a/shared/comm-lib/src/http/auth.rs +++ b/shared/comm-lib/src/http/auth.rs @@ -1,134 +1,176 @@ use actix_web::{ body::{EitherBody, MessageBody}, dev::{Service, ServiceRequest, ServiceResponse, Transform}, FromRequest, HttpMessage, }; use actix_web_httpauth::{ extractors::{bearer::BearerAuth, AuthenticationError}, headers::www_authenticate::bearer::Bearer, middleware::HttpAuthentication, }; use futures_util::future::{ready, Ready}; use http::StatusCode; use std::str::FromStr; use tracing::debug; -use crate::auth::{AuthorizationCredential, UserIdentity}; +use crate::auth::{ + is_csat_verification_disabled, AuthorizationCredential, UserIdentity, +}; impl FromRequest for AuthorizationCredential { type Error = actix_web::Error; type Future = Ready>; fn from_request( req: &actix_web::HttpRequest, _: &mut actix_web::dev::Payload, ) -> Self::Future { if let Some(credential) = req.extensions().get::() { return ready(Ok(credential.clone())); } let f = || { let bearer = BearerAuth::extract(req).into_inner()?; let credential = match AuthorizationCredential::from_str(bearer.token()) { Ok(credential) => credential, Err(err) => { debug!("HTTP authorization error: {err}"); return Err(AuthenticationError::new(Bearer::default()).into()); } }; Ok(credential) }; ready(f()) } } impl FromRequest for UserIdentity { type Error = actix_web::Error; type Future = Ready>; fn from_request( req: &actix_web::HttpRequest, payload: &mut actix_web::dev::Payload, ) -> Self::Future { use futures_util::future::{err, ok}; match AuthorizationCredential::from_request(req, payload).into_inner() { Ok(AuthorizationCredential::UserToken(user)) => ok(user.clone()), Ok(_) => { debug!("Authorization provided, but it's not UserIdentity"); let mut error = AuthenticationError::new(Bearer::default()); *error.status_code_mut() = StatusCode::FORBIDDEN; err(error.into()) } Err(e) => err(e), } } } -pub async fn validation_function( +/// Counterpart of [`actix_web_httpauth::extractors::bearer::BearerAuth`] that +/// handles parsing Authorization header into [`AuthorizationCredential`]. +/// The value can be `None` when CSAT verification is disabled. +#[derive(Clone, Debug)] +struct CommServicesBearerAuth { + credential: Option, +} + +impl FromRequest for CommServicesBearerAuth { + type Error = actix_web::Error; + type Future = Ready>; + + fn from_request( + req: &actix_web::HttpRequest, + _payload: &mut actix_web::dev::Payload, + ) -> Self::Future { + use futures_util::future::{err, ok}; + if is_csat_verification_disabled() { + return ok(Self { credential: None }); + } + + match AuthorizationCredential::extract(req).into_inner() { + Ok(credential) => ok(Self { + credential: Some(credential), + }), + Err(e) => err(e), + } + } +} + +/// Function used by auth middleware to validate authenticated requests. +async fn middleware_validation_function( req: ServiceRequest, - bearer: BearerAuth, + auth: CommServicesBearerAuth, ) -> Result { - let credential = match AuthorizationCredential::from_str(bearer.token()) { - Ok(credential) => credential, - Err(err) => { - debug!("HTTP authorization error: {err}"); - return Err((AuthenticationError::new(Bearer::default()).into(), req)); - } + let Some(credential) = auth.credential else { + return if is_csat_verification_disabled() { + Ok(req) + } else { + // This branch should be normally unreachable. If this happens, + // it means that `MiddlewareCredentialExtractor::from_request()` + // implementation is incorrect. + tracing::error!( + "CSAT verification enabled, but no credential was extracted!" + ); + let mut error = AuthenticationError::new(Bearer::default()); + *error.status_code_mut() = StatusCode::INTERNAL_SERVER_ERROR; + Err((error.into(), req)) + }; }; // TODO: call identity service, for now just allow every request req.extensions_mut().insert(credential); Ok(req) } /// Use this to add Authentication Middleware. It's going to parse Authorization /// header and call the identity service to check if the provided credentials /// are correct. If not it's going to reject the request. /// Note that this requires `AuthService` to be present in the app data. /// /// # Example /// ```ignore /// let auth_service = AuthService::new(&aws_config, &config.identity_endpoint); /// let auth_middleware = get_comm_authentication_middleware(); /// App::new() /// .app_data(auth_service.clone()) /// .wrap(auth_middleware) /// ``` /// If you don't want all of the routes to require authentication you can wrap /// individual resources or scopes: /// ```ignore /// App::new().service( /// web::resource("/endpoint").route(web::get().to(handler)).wrap(auth_middleware), /// ) /// ``` // This type is very complicated, but unfortunately typing this directly // requires https://github.com/rust-lang/rust/issues/99697 to be merged. // The issue is that we can't specify the second generic argument of // HttpAuthentication, because it look something like this: // ``` // impl Fn(ServiceRequest, BearerAuth) -> impl Future< // Output = Result, // > // `` // which isn't valid (until the linked issue is merged). pub fn get_comm_authentication_middleware() -> impl Transform< S, ServiceRequest, Response = ServiceResponse>, Error = actix_web::Error, InitError = (), -> + 'static +> + Clone + + 'static where B: MessageBody + 'static, S: Service< ServiceRequest, Response = ServiceResponse, Error = actix_web::Error, > + 'static, { - HttpAuthentication::bearer(validation_function) + HttpAuthentication::with_fn(middleware_validation_function) }