diff --git a/services/backup/Cargo.lock b/services/backup/Cargo.lock --- a/services/backup/Cargo.lock +++ b/services/backup/Cargo.lock @@ -459,6 +459,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + [[package]] name = "base64-simd" version = "0.8.0" @@ -587,8 +593,11 @@ "aws-config", "aws-sdk-dynamodb", "aws-types", + "base64 0.21.2", "chrono", "derive_more", + "serde", + "serde_json", "tracing", ] @@ -1565,7 +1574,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55" dependencies = [ - "base64", + "base64 0.13.1", ] [[package]] @@ -1640,6 +1649,31 @@ version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "256b9932320c590e707b94576e3cc1f7c9024d0ee6612dfbcf1cb106cbe8e055" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4eae9b04cbffdfd550eb462ed33bc6a1b68c935127d008b27444d08380f94e4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46266871c240a00b8f503b877622fe33430b3c7d963bdc0f2adc511e54a1eae3" +dependencies = [ + "itoa", + "ryu", + "serde", +] [[package]] name = "sha2" @@ -1874,7 +1908,7 @@ "async-stream", "async-trait", "axum", - "base64", + "base64 0.13.1", "bytes", "futures-core", "futures-util", diff --git a/services/blob/Cargo.lock b/services/blob/Cargo.lock --- a/services/blob/Cargo.lock +++ b/services/blob/Cargo.lock @@ -978,8 +978,11 @@ "aws-config", "aws-sdk-dynamodb", "aws-types", + "base64 0.21.0", "chrono", "derive_more", + "serde", + "serde_json", "tracing", ] diff --git a/services/comm-services-lib/Cargo.lock b/services/comm-services-lib/Cargo.lock --- a/services/comm-services-lib/Cargo.lock +++ b/services/comm-services-lib/Cargo.lock @@ -441,6 +441,7 @@ "aws-config", "aws-sdk-dynamodb", "aws-types", + "base64", "chrono", "derive_more", "futures-core", diff --git a/services/comm-services-lib/Cargo.toml b/services/comm-services-lib/Cargo.toml --- a/services/comm-services-lib/Cargo.toml +++ b/services/comm-services-lib/Cargo.toml @@ -5,20 +5,17 @@ license = "BSD-3-Clause" [features] -blob-client = [ - "dep:reqwest", - "dep:futures-core", - "dep:futures-util", - "dep:serde", - "dep:serde_json", -] +blob-client = ["dep:reqwest", "dep:futures-core", "dep:futures-util"] [dependencies] aws-config = "0.55" aws-sdk-dynamodb = "0.27" aws-types = "0.55" +base64 = "0.21" chrono = "0.4" derive_more = "0.99" +serde = { version = "1.0", features = ["derive"] } +serde_json = { version = "1.0" } tracing = "0.1" # blob client dependencies futures-core = { version = "0.3", optional = true } @@ -28,5 +25,3 @@ "multipart", "stream", ], optional = true } -serde = { version = "1.0", features = ["derive"], optional = true } -serde_json = { version = "1.0", optional = true } diff --git a/services/comm-services-lib/src/auth.rs b/services/comm-services-lib/src/auth.rs new file mode 100644 --- /dev/null +++ b/services/comm-services-lib/src/auth.rs @@ -0,0 +1,32 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UserIdentity { + #[serde(alias = "userID")] + user_id: String, + #[serde(alias = "accessToken")] + access_token: String, + #[serde(alias = "signingPublicKey")] + signing_public_key: String, +} + +impl UserIdentity { + /// Retrieves the Identity Service user ID + pub fn user_id(&self) -> &str { + &self.user_id + } + + /// Gets the access token value, usable in bearer authorization + /// + /// # Example + /// ```rust + /// reqwest::get(url).beaerer_auth(user.authorization_token()?).send().await?; + /// ``` + pub fn authorization_token(&self) -> Result { + use base64::Engine; + + let json = serde_json::to_string(self)?; + let base64_str = base64::prelude::BASE64_STANDARD.encode(json); + Ok(base64_str) + } +} diff --git a/services/comm-services-lib/src/blob/client.rs b/services/comm-services-lib/src/blob/client.rs --- a/services/comm-services-lib/src/blob/client.rs +++ b/services/comm-services-lib/src/blob/client.rs @@ -3,7 +3,7 @@ use futures_util::{StreamExt, TryStreamExt}; use reqwest::{ multipart::{Form, Part}, - Body, Url, + Body, Method, RequestBuilder, Url, }; use tracing::{debug, trace, warn}; @@ -11,6 +11,8 @@ pub use reqwest::Error as ReqwestError; pub use reqwest::StatusCode; +use crate::auth::UserIdentity; + #[derive(From, Error, Debug, Display)] pub enum BlobServiceError { /// HTTP Client errors, this includes: @@ -45,23 +47,56 @@ /// The `BlobServiceClient` holds a connection pool internally, so it is advised that /// you create one and **reuse** it, by **cloning**. /// +/// A clone is recommended for each individual client identity, that has +/// a different `UserIdentity`. +/// +/// # Example +/// ```rust +/// // create client for user A +/// let clientA = BlobServiceClient::new(blob_endpoint).with_user_identity(userA); +/// +/// // reuse the client connection with different credentials +/// let clientB = clientA.clone().with_user_identity(userB); +/// +/// // clientA still uses userA credentials +/// ``` +/// +/// A clone is recommended when the client concurrently handles multiple identities - +/// e.g. a HTTP service handling requests from different users +/// /// You should **not** wrap the `BlobServiceClient` in an `Rc` or `Arc` to **reuse** it, /// because it already uses an `Arc` internally. #[derive(Clone)] pub struct BlobServiceClient { http_client: reqwest::Client, blob_service_url: reqwest::Url, + user_identity: Option, } impl BlobServiceClient { + /// Creates a new Blob Service client instance. It is unauthenticated by default + /// so you need to call `with_user_identity()` afterwards: + /// ```rust + /// let client = BlobServiceClient::new(blob_endpoint).with_user_identity(user); + /// ``` + /// + /// **Note**: It is advised to create this client once and reuse by **cloning**. + /// See [`BlobServiceClient`] docs for details pub fn new(blob_service_url: reqwest::Url) -> Self { debug!("Creating BlobServiceClient. URL: {}", blob_service_url); Self { http_client: reqwest::Client::new(), blob_service_url, + user_identity: None, } } + pub fn with_user_identity(mut self, user_identity: UserIdentity) -> Self { + trace!("Set user_identity: {:?}", &user_identity); + self.user_identity = Some(user_identity); + self + } + /// Downloads blob with given [`blob_hash`]. /// /// @returns a stream of blob bytes @@ -88,8 +123,7 @@ let url = self.get_blob_url(Some(blob_hash))?; let response = self - .http_client - .get(url) + .request(Method::GET, url) .send() .await .map_err(BlobServiceError::ClientError)?; @@ -129,7 +163,11 @@ blob_hash: blob_hash.to_string(), }; debug!("Request payload: {:?}", payload); - let response = self.http_client.post(url).json(&payload).send().await?; + let response = self + .request(Method::POST, url) + .json(&payload) + .send() + .await?; debug!("Response status: {}", response.status()); if response.status().is_success() { @@ -162,7 +200,11 @@ }; debug!("Request payload: {:?}", payload); - let response = self.http_client.delete(url).json(&payload).send().await?; + let response = self + .request(Method::DELETE, url) + .json(&payload) + .send() + .await?; debug!("Response status: {}", response.status()); if response.status().is_success() { @@ -214,7 +256,11 @@ .text("blob_hash", blob_hash.into()) .part("blob_data", Part::stream(streaming_body)); - let response = self.http_client.put(url).multipart(form).send().await?; + let response = self + .request(Method::PUT, url) + .multipart(form) + .send() + .await?; debug!("Response status: {}", response.status()); if response.status().is_success() { @@ -273,6 +319,16 @@ trace!("Constructed request URL: {}", url); Ok(url) } + + fn request(&self, http_method: Method, url: Url) -> RequestBuilder { + let request = self.http_client.request(http_method, url); + match &self.user_identity { + Some(user) => { + request.bearer_auth(user.authorization_token().expect("TODO")) + } + None => request, + } + } } fn handle_http_error(status_code: StatusCode) -> BlobServiceError { diff --git a/services/comm-services-lib/src/lib.rs b/services/comm-services-lib/src/lib.rs --- a/services/comm-services-lib/src/lib.rs +++ b/services/comm-services-lib/src/lib.rs @@ -1,3 +1,4 @@ +pub mod auth; pub mod blob; pub mod database; pub mod tools; diff --git a/services/feature-flags/Cargo.lock b/services/feature-flags/Cargo.lock --- a/services/feature-flags/Cargo.lock +++ b/services/feature-flags/Cargo.lock @@ -730,8 +730,11 @@ "aws-config", "aws-sdk-dynamodb", "aws-types", + "base64", "chrono", "derive_more", + "serde", + "serde_json", "tracing", ]