Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow anonymous access / Remove RSA crate #217

Merged
merged 7 commits into from
Dec 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 <[email protected]>"]
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
Loading