Skip to content

Commit

Permalink
Allow anonymous access / Remove RSA crate (#217)
Browse files Browse the repository at this point in the history
  • Loading branch information
yoshidan authored Dec 1, 2023
1 parent 1e96776 commit 6525195
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 33 deletions.
2 changes: 1 addition & 1 deletion foundation/token/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ pub struct NopeTokenSourceProvider {}

impl TokenSourceProvider for NopeTokenSourceProvider {
fn token_source(&self) -> Arc<dyn TokenSource> {
panic!("This is dummy token source provider. you can use 'google_cloud_default' crate")
panic!("This is dummy token source provider. you can use 'google_cloud_auth' crate")
}
}
7 changes: 4 additions & 3 deletions storage/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "google-cloud-storage"
version = "0.14.0"
version = "0.15.0"
edition = "2021"
authors = ["yoshidan <naohiro.y@gmail.com>"]
repository = "https://github.com/yoshidan/google-cloud-rust/tree/main/storage"
Expand All @@ -12,13 +12,13 @@ documentation = "https://docs.rs/google-cloud-storage/latest/google_cloud_storag

[dependencies]
google-cloud-token = { version = "0.1.1", path = "../foundation/token" }
rsa = "0.6"
pkcs8 = {version="0.10", features=["pem"]}
thiserror = "1.0"
time = { version = "0.3", features = ["std", "macros", "formatting", "parsing", "serde"] }
base64 = "0.21"
regex = "1.9"
sha2 = "0.10"
ring = "0.16"
ring = "0.17"
tokio = { version="1.32", features=["macros"] }
async-stream = "0.3"
once_cell = "1.18"
Expand All @@ -31,6 +31,7 @@ serde_json = "1.0"
percent-encoding = "2.3"
futures-util = "0.3"
bytes = "1.5"
async-trait = "0.1"

google-cloud-metadata = { optional = true, version = "0.4", path = "../foundation/metadata" }
google-cloud-auth = { optional = true, version = "0.13", path="../foundation/auth", default-features=false }
Expand Down
13 changes: 13 additions & 0 deletions storage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,19 @@ async fn run(cred: CredentialsFile) {
}
```

### Anonymous Access

To provide [anonymous access without authentication](https://cloud.google.com/storage/docs/authentication), do the following.

```rust
use google_cloud_storage::client::{ClientConfig, Client};

async fn run() {
let config = ClientConfig::default().anonymous();
let client = Client::new(config);
}
```

### Usage

```rust
Expand Down
55 changes: 39 additions & 16 deletions storage/src/client.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
use std::ops::Deref;

use ring::{rand, signature};
use rsa::pkcs8::{DecodePrivateKey, EncodePrivateKey};

use google_cloud_token::{NopeTokenSourceProvider, TokenSourceProvider};

use crate::http::service_account_client::ServiceAccountClient;
use crate::http::storage_client::StorageClient;
use crate::sign::SignBy::PrivateKey;
use crate::sign::{create_signed_buffer, SignBy, SignedURLError, SignedURLOptions};
use crate::sign::{create_signed_buffer, RsaKeyPair, SignBy, SignedURLError, SignedURLOptions};

#[derive(Debug)]
pub struct ClientConfig {
pub http: Option<reqwest::Client>,
pub storage_endpoint: String,
pub service_account_endpoint: String,
pub token_source_provider: Box<dyn TokenSourceProvider>,
pub token_source_provider: Option<Box<dyn TokenSourceProvider>>,
pub default_google_access_id: Option<String>,
pub default_sign_by: Option<SignBy>,
pub project_id: Option<String>,
Expand All @@ -26,7 +25,7 @@ impl Default for ClientConfig {
Self {
http: None,
storage_endpoint: "https://storage.googleapis.com".to_string(),
token_source_provider: Box::new(NopeTokenSourceProvider {}),
token_source_provider: Some(Box::new(NopeTokenSourceProvider {})),
service_account_endpoint: "https://iamcredentials.googleapis.com".to_string(),
default_google_access_id: None,
default_sign_by: None,
Expand All @@ -35,6 +34,13 @@ impl Default for ClientConfig {
}
}

impl ClientConfig {
pub fn anonymous(mut self) -> Self {
self.token_source_provider = None;
self
}
}

#[cfg(feature = "auth")]
pub use google_cloud_auth;

Expand Down Expand Up @@ -74,7 +80,7 @@ impl ClientConfig {
self.default_google_access_id = google_cloud_metadata::email("default").await.ok();
}
}
self.token_source_provider = Box::new(ts);
self.token_source_provider = Some(Box::new(ts));
self
}

Expand Down Expand Up @@ -112,7 +118,13 @@ impl Default for Client {
impl Client {
/// New client
pub fn new(config: ClientConfig) -> Self {
let ts = config.token_source_provider.token_source();
let ts = match config.token_source_provider {
Some(tsp) => Some(tsp.token_source()),
None => {
tracing::trace!("Use anonymous access due to lack of token");
None
}
};
let http = config.http.unwrap_or_default();

let service_account_client =
Expand Down Expand Up @@ -208,16 +220,8 @@ impl Client {
if private_key.is_empty() {
return Err(SignedURLError::InvalidOption("No keys present"));
}

let str = String::from_utf8_lossy(private_key);
let pkcs = rsa::RsaPrivateKey::from_pkcs8_pem(str.as_ref())
.map_err(|e| SignedURLError::CertError(e.to_string()))?;
let der = pkcs
.to_pkcs8_der()
.map_err(|e| SignedURLError::CertError(e.to_string()))?;
let key_pair = ring::signature::RsaKeyPair::from_pkcs8(der.as_ref())
.map_err(|e| SignedURLError::CertError(e.to_string()))?;
let mut signed = vec![0; key_pair.public_modulus_len()];
let key_pair = &RsaKeyPair::try_from(private_key)?;
let mut signed = vec![0; key_pair.public().modulus_len()];
key_pair
.sign(
&signature::RSA_PKCS1_SHA256,
Expand Down Expand Up @@ -249,6 +253,7 @@ mod test {
use serial_test::serial;

use crate::client::{Client, ClientConfig};
use crate::http::buckets::get::GetBucketRequest;

use crate::http::storage_client::test::bucket_name;
use crate::sign::{SignedURLMethod, SignedURLOptions};
Expand Down Expand Up @@ -371,4 +376,22 @@ mod test {
.unwrap();
assert_eq!(result, data);
}

#[tokio::test]
#[serial]
async fn test_anonymous() {
let project = ClientConfig::default().with_auth().await.unwrap().project_id.unwrap();
let bucket = bucket_name(&project, "anonymous");

let config = ClientConfig::default().anonymous();
let client = Client::new(config);
let result = client
.get_bucket(&GetBucketRequest {
bucket: bucket.clone(),
..Default::default()
})
.await
.unwrap();
assert_eq!(result.name, bucket);
}
}
17 changes: 11 additions & 6 deletions storage/src/http/service_account_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ use crate::http::{check_response_status, Error};

#[derive(Clone)]
pub struct ServiceAccountClient {
ts: Arc<dyn TokenSource>,
ts: Option<Arc<dyn TokenSource>>,
v1_endpoint: String,
http: reqwest::Client,
}

impl ServiceAccountClient {
pub(crate) fn new(ts: Arc<dyn TokenSource>, endpoint: &str, http: reqwest::Client) -> Self {
pub(crate) fn new(ts: Option<Arc<dyn TokenSource>>, endpoint: &str, http: reqwest::Client) -> Self {
Self {
ts,
v1_endpoint: format!("{endpoint}/v1"),
Expand All @@ -24,14 +24,19 @@ impl ServiceAccountClient {
pub async fn sign_blob(&self, name: &str, payload: &[u8]) -> Result<Vec<u8>, Error> {
let url = format!("{}/{}:signBlob", self.v1_endpoint, name);
let request = SignBlobRequest { payload };
let token = self.ts.token().await.map_err(Error::TokenSource)?;
let request = self
.http
.post(url)
.json(&request)
.header("X-Goog-Api-Client", "rust")
.header(reqwest::header::USER_AGENT, "google-cloud-storage")
.header(reqwest::header::AUTHORIZATION, token);
.header(reqwest::header::USER_AGENT, "google-cloud-storage");
let request = match &self.ts {
Some(ts) => {
let token = ts.token().await.map_err(Error::TokenSource)?;
request.header(reqwest::header::AUTHORIZATION, token)
}
None => request,
};
let response = request.send().await?;
let response = check_response_status(response).await?;
Ok(response.json::<SignBlobResponse>().await?.signed_blob)
Expand Down Expand Up @@ -73,7 +78,7 @@ mod test {
let email = tsp.source_credentials.clone().unwrap().client_email.unwrap();
let ts = tsp.token_source();
(
ServiceAccountClient::new(ts, "https://iamcredentials.googleapis.com", Client::default()),
ServiceAccountClient::new(Some(ts), "https://iamcredentials.googleapis.com", Client::default()),
email,
)
}
Expand Down
20 changes: 13 additions & 7 deletions storage/src/http/storage_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,14 @@ pub const SCOPES: [&str; 2] = [

#[derive(Clone)]
pub struct StorageClient {
ts: Arc<dyn TokenSource>,
ts: Option<Arc<dyn TokenSource>>,
v1_endpoint: String,
v1_upload_endpoint: String,
http: Client,
}

impl StorageClient {
pub(crate) fn new(ts: Arc<dyn TokenSource>, endpoint: &str, http: Client) -> Self {
pub(crate) fn new(ts: Option<Arc<dyn TokenSource>>, endpoint: &str, http: Client) -> Self {
Self {
ts,
v1_endpoint: format!("{endpoint}/storage/v1"),
Expand Down Expand Up @@ -1282,11 +1282,17 @@ impl StorageClient {
}

async fn with_headers(&self, builder: RequestBuilder) -> Result<RequestBuilder, Error> {
let token = self.ts.token().await.map_err(Error::TokenSource)?;
Ok(builder
let builder = builder
.header("X-Goog-Api-Client", "rust")
.header(reqwest::header::USER_AGENT, "google-cloud-storage")
.header(reqwest::header::AUTHORIZATION, token))
.header(reqwest::header::USER_AGENT, "google-cloud-storage");
let builder = match &self.ts {
Some(ts) => {
let token = ts.token().await.map_err(Error::TokenSource)?;
builder.header(reqwest::header::AUTHORIZATION, token)
}
None => builder,
};
Ok(builder)
}

async fn send_request<T>(&self, request: Request) -> Result<T, Error>
Expand Down Expand Up @@ -1409,7 +1415,7 @@ pub(crate) mod test {
.unwrap();
let cred = tsp.source_credentials.clone();
let ts = tsp.token_source();
let client = StorageClient::new(ts, "https://storage.googleapis.com", reqwest::Client::new());
let client = StorageClient::new(Some(ts), "https://storage.googleapis.com", reqwest::Client::new());
let cred = cred.unwrap();
(client, cred.project_id.unwrap(), cred.client_email.unwrap())
}
Expand Down
13 changes: 13 additions & 0 deletions storage/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,19 @@
//! }
//! ```
//!
//! ### Anonymous Access
//!
//! To provide [anonymous access without authentication](https://cloud.google.com/storage/docs/authentication), do the following.
//!
//! ```rust
//! use google_cloud_storage::client::{ClientConfig, Client};
//!
//! async fn run() {
//! let config = ClientConfig::default().anonymous();
//! let client = Client::new(config);
//! }
//! ```
//!
//! ### Usage
//!
//! ```
Expand Down
32 changes: 32 additions & 0 deletions storage/src/sign.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
use std::collections::HashMap;
use std::fmt::{Debug, Formatter};
use std::ops::Deref;
use std::time::Duration;

use base64::prelude::*;
use once_cell::sync::Lazy;
use pkcs8::der::pem::PemLabel;
use pkcs8::SecretDocument;
use regex::Regex;
use sha2::{Digest, Sha256};
use time::format_description::well_known::iso8601::{EncodedConfig, TimePrecision};
Expand Down Expand Up @@ -326,6 +329,35 @@ fn validate_options(opts: &SignedURLOptions) -> Result<(), SignedURLError> {
Ok(())
}

pub struct RsaKeyPair {
inner: ring::signature::RsaKeyPair,
}

impl PemLabel for RsaKeyPair {
const PEM_LABEL: &'static str = "PRIVATE KEY";
}

impl TryFrom<&Vec<u8>> for RsaKeyPair {
type Error = SignedURLError;

fn try_from(pem: &Vec<u8>) -> Result<Self, Self::Error> {
let str = String::from_utf8_lossy(pem);
let (label, doc) = SecretDocument::from_pem(&str).map_err(|v| SignedURLError::CertError(v.to_string()))?;
Self::validate_pem_label(label).map_err(|_| SignedURLError::CertError(label.to_string()))?;
let key_pair = ring::signature::RsaKeyPair::from_pkcs8(doc.as_bytes())
.map_err(|e| SignedURLError::CertError(e.to_string()))?;
Ok(Self { inner: key_pair })
}
}

impl Deref for RsaKeyPair {
type Target = ring::signature::RsaKeyPair;

fn deref(&self) -> &ring::signature::RsaKeyPair {
&self.inner
}
}

#[cfg(test)]
mod test {
use std::collections::HashMap;
Expand Down

0 comments on commit 6525195

Please sign in to comment.