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

feat: allow create SAS token for azure storage #306

Merged
merged 5 commits into from
May 8, 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
9 changes: 9 additions & 0 deletions src/azure/storage/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
//! Azure Storage Singer

mod signer;

pub use signer::Signer as AzureStorageSigner;

mod config;

pub use config::Config as AzureStorageConfig;

mod credential;

pub use credential::Credential as AzureStorageCredential;

mod loader;

pub use loader::Loader as AzureStorageLoader;

mod sas;
Xuanwo marked this conversation as resolved.
Show resolved Hide resolved
123 changes: 123 additions & 0 deletions src/azure/storage/sas/account_sas.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
use crate::time::DateTime;
use crate::{hash, time};

/// The default parameters that make up a SAS token
/// https://learn.microsoft.com/en-us/rest/api/storageservices/create-account-sas#specify-the-account-sas-parameters
const ACCOUNT_SAS_VERSION: &str = "2018-11-09";
const ACCOUNT_SAS_RESOURCE: &str = "bqtf";
const ACCOUNT_SAS_RESOURCE_TYPE: &str = "sco";
const ACCOUNT_SAS_PERMISSIONS: &str = "rwdlacu";

pub struct AccountSharedAccessSignature {
account: String,
key: String,
version: String,
resource: String,
resource_type: String,
permissions: String,
expiry: DateTime,
start: Option<DateTime>,
ip: Option<String>,
protocol: Option<String>,
}

impl AccountSharedAccessSignature {
/// Create a SAS token signer with default parameters
pub fn new(account: String, key: String, expiry: DateTime) -> Self {
Self {
account,
key,
expiry,
start: None,
ip: None,
protocol: None,
version: ACCOUNT_SAS_VERSION.to_string(),
resource: ACCOUNT_SAS_RESOURCE.to_string(),
resource_type: ACCOUNT_SAS_RESOURCE_TYPE.to_string(),
permissions: ACCOUNT_SAS_PERMISSIONS.to_string(),
}
}

// Azure documentation: https://learn.microsoft.com/en-us/rest/api/storageservices/create-account-sas#construct-the-signature-string
fn signature(&self) -> String {
let string_to_sign = format!(
"{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n",
self.account,
self.permissions,
self.resource,
self.resource_type,
self.start
.as_ref()
.map_or("".to_string(), |v| urlencoded(time::format_rfc3339(*v))),
time::format_rfc3339(self.expiry),
self.ip.clone().unwrap_or_default(),
self.protocol
.as_ref()
.map_or("".to_string(), |v| v.to_string()),
self.version,
);

hash::base64_hmac_sha256(
&hash::base64_decode(self.key.clone().as_str()),
string_to_sign.as_bytes(),
)
}

/// [Example](https://docs.microsoft.com/rest/api/storageservices/create-service-sas#service-sas-example) from Azure documentation.
pub fn token(&self) -> Vec<(String, String)> {
let mut elements: Vec<(String, String)> = vec![
("sv".to_string(), self.version.to_string()),
("ss".to_string(), self.resource.to_string()),
("srt".to_string(), self.resource_type.to_string()),
(
"se".to_string(),
urlencoded(time::format_rfc3339(self.expiry)),
),
("sp".to_string(), self.permissions.to_string()),
];

if let Some(start) = &self.start {
elements.push(("st".to_string(), urlencoded(time::format_rfc3339(*start))))
}
if let Some(ip) = &self.ip {
elements.push(("sip".to_string(), ip.to_string()))
}
if let Some(protocol) = &self.protocol {
elements.push(("spr".to_string(), protocol.to_string()))
}

let sig = AccountSharedAccessSignature::signature(self);
elements.push(("sig".to_string(), urlencoded(sig)));

elements
}
}

#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;

fn test_time() -> DateTime {
DateTime::from_str("2022-03-01T08:12:34Z").unwrap()
}

#[test]
fn test_can_generate_sas_token() {
let key = hash::base64_encode("key".as_bytes());
let expiry = test_time() + chrono::Duration::minutes(5);
let sign = AccountSharedAccessSignature::new("account".to_string(), key, expiry);
let token = sign
.token()
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<String>>()
.join("&");

assert_eq!(token, "sv=2018-11-09&ss=bqtf&srt=sco&se=2022-03-01T08%3A17%3A34Z&sp=rwdlacu&sig=jgK9nDUT0ntH%2Fp28LPs0jzwxsk91W6hePLPlfrElv4k%3D");
}
}

fn urlencoded(s: String) -> String {
form_urlencoded::byte_serialize(s.as_bytes()).collect()
}
1 change: 1 addition & 0 deletions src/azure/storage/sas/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod account_sas;
29 changes: 23 additions & 6 deletions src/azure/storage/signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ use std::fmt::Debug;
use std::fmt::Write;
use std::time::Duration;

use anyhow::anyhow;
use anyhow::Result;
use http::header::*;
use log::debug;

use super::super::constants::*;
use super::credential::Credential;
use crate::azure::storage::sas::account_sas;
use crate::ctx::SigningContext;
use crate::ctx::SigningMethod;
use crate::hash::base64_decode;
Expand Down Expand Up @@ -68,8 +68,17 @@ impl Signer {
return Ok(ctx);
}
Credential::SharedKey(ak, sk) => match method {
SigningMethod::Query(_) => {
return Err(anyhow!("SAS token is required for query signing"));
SigningMethod::Query(d) => {
// try sign request use account_sas token
let signer = account_sas::AccountSharedAccessSignature::new(
ak.to_string(),
sk.to_string(),
time::now() + chrono::Duration::from_std(d)?,
);

signer.token().iter().for_each(|(k, v)| {
ctx.query_push(k, v);
});
}
SigningMethod::Header => {
let now = self.time.unwrap_or_else(time::now);
Expand Down Expand Up @@ -127,8 +136,13 @@ impl Signer {
}

/// Signing request with query.
pub fn sign_query(&self, req: &mut impl SignableRequest, cred: &Credential) -> Result<()> {
let ctx = self.build(req, SigningMethod::Query(Duration::from_secs(1)), cred)?;
pub fn sign_query(
&self,
req: &mut impl SignableRequest,
expire: Duration,
cred: &Credential,
) -> Result<()> {
let ctx = self.build(req, SigningMethod::Query(expire), cred)?;
Comment on lines +139 to +145
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Xuanwo Please note that we have modified the signature of this function, and we need to pay attention to compatibility when releasing the version.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! We should release v0.10 instead!

req.apply(ctx)
}
}
Expand Down Expand Up @@ -247,6 +261,7 @@ fn canonicalize_resource(ctx: &mut SigningContext, ak: &str) -> String {
#[cfg(test)]
mod tests {
use http::Request;
use std::time::Duration;

use super::super::config::Config;
use crate::azure::storage::loader::Loader;
Expand All @@ -273,7 +288,9 @@ mod tests {
.unwrap();

// Signing request with Signer
assert!(signer.sign_query(&mut req, &cred).is_ok());
assert!(signer
.sign_query(&mut req, Duration::from_secs(1), &cred)
.is_ok());
assert_eq!(req.uri(), "https://test.blob.core.windows.net/testbucket/testblob?sv=2021-01-01&ss=b&srt=c&sp=rwdlaciytfx&se=2022-01-01T11:00:14Z&st=2022-01-02T03:00:14Z&spr=https&sig=KEllk4N8f7rJfLjQCmikL2fRVt%2B%2Bl73UBkbgH%2FK3VGE%3D")
}
}
93 changes: 90 additions & 3 deletions tests/azure/storage.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
use std::env;
use std::str::FromStr;
use std::time::Duration;

use anyhow::Result;
use http::StatusCode;
use log::debug;
use log::warn;
use percent_encoding::utf8_percent_encode;
use percent_encoding::NON_ALPHANUMERIC;
use reqsign::AzureStorageConfig;
use reqsign::AzureStorageLoader;
use reqsign::AzureStorageSigner;
use reqsign::{AzureStorageConfig, AzureStorageLoader};
use reqwest::Client;

fn init_signer() -> Option<(AzureStorageLoader, AzureStorageSigner)> {
Expand Down Expand Up @@ -122,7 +122,7 @@ async fn test_head_object_with_encoded_characters() -> Result<()> {
}

#[tokio::test]
async fn test_list_blobs() -> Result<()> {
async fn test_list_container_blobs() -> Result<()> {
let signer = init_signer();
if signer.is_none() {
warn!("REQSIGN_AZURE_STORAGE_ON_TEST is not set, skipped");
Expand Down Expand Up @@ -169,3 +169,90 @@ async fn test_list_blobs() -> Result<()> {

Ok(())
}

#[tokio::test]
async fn test_can_head_blob_with_sas() -> Result<()> {
Xuanwo marked this conversation as resolved.
Show resolved Hide resolved
let signer = init_signer();
if signer.is_none() {
warn!("REQSIGN_AZURE_STORAGE_ON_TEST is not set, skipped");
return Ok(());
}
let (loader, signer) = signer.unwrap();

let url =
&env::var("REQSIGN_AZURE_STORAGE_URL").expect("env REQSIGN_AZURE_STORAGE_URL must set");

let mut builder = http::Request::builder();
builder = builder.method(http::Method::HEAD);
builder = builder.uri(format!("{}/{}", url, "not_exist_file"));
let mut req = builder.body("")?;

let cred = loader
.load()
.await
.expect("load credential must success")
.unwrap();
signer
.sign_query(&mut req, Duration::from_secs(60), &cred)
.expect("sign request must success");

println!("signed request: {:?}", req);

let client = Client::new();
let resp = client
.execute(req.try_into()?)
.await
.expect("request must success");

println!("got response: {:?}", resp);
assert_eq!(StatusCode::NOT_FOUND, resp.status());
Ok(())
}

#[tokio::test]
async fn test_can_list_container_blobs() -> Result<()> {
// API https://learn.microsoft.com/en-us/rest/api/storageservices/list-blobs?tabs=azure-ad
let signer = init_signer();
if signer.is_none() {
warn!("REQSIGN_AZURE_STORAGE_ON_TEST is not set, skipped");
return Ok(());
}
let (loader, signer) = signer.unwrap();

let url =
&env::var("REQSIGN_AZURE_STORAGE_URL").expect("env REQSIGN_AZURE_STORAGE_URL must set");

for query in [
// Without prefix
"restype=container&comp=list",
// With not encoded prefix
"restype=container&comp=list&prefix=test/path/to/dir",
// With encoded prefix
"restype=container&comp=list&prefix=test%2Fpath%2Fto%2Fdir",
] {
let mut builder = http::Request::builder();
builder = builder.method(http::Method::GET);
builder = builder.uri(format!("{url}?{query}"));
let mut req = builder.body("")?;

let cred = loader
.load()
.await
.expect("load credential must success")
.unwrap();
signer
.sign_query(&mut req, Duration::from_secs(60), &cred)
.expect("sign request must success");

let client = Client::new();
let resp = client
.execute(req.try_into()?)
.await
.expect("request must success");

debug!("got response: {:?}", resp);
assert_eq!(StatusCode::OK, resp.status());
}

Ok(())
}