Skip to content
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
3 changes: 3 additions & 0 deletions sdk/storage/azure_storage_blob/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

### Features Added

- Added a SAS-only constructor to `BlobContainerClient` (`new_sas`) that builds a pipeline without a bearer token policy. This enables using SAS in the endpoint without requiring a `TokenCredential`.
- Introduced a SAS query policy that ensures SAS query parameters from the client endpoint are appended to every outgoing request. Sub-clients created via `get_blob_client` inherit this policy.

### Breaking Changes

### Bugs Fixed
Expand Down
63 changes: 63 additions & 0 deletions sdk/storage/azure_storage_blob/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,37 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}
```

#### SAS-only authentication

You can use Shared Access Signatures (SAS) without providing a `TokenCredential`. The SAS-only constructors build a pipeline without a bearer-token policy and apply a SAS query policy that ensures the SAS query parameters are present on every outgoing request, even when URLs are recomposed internally. Clients created via `get_blob_client` inherit this pipeline.

```rust no_run
use azure_core::http::{ClientOptions, Transport};
use azure_storage_blob::{BlobContainerClient, BlobContainerClientOptions};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Endpoint must include a valid SAS query string.
let endpoint = "https://<account>.blob.core.windows.net/?<SAS>";
let container = "container_name".to_string();

// Optional: inject a custom HTTP transport (e.g., reqwest). Otherwise, use defaults.
// let http = std::sync::Arc::new(reqwest::Client::builder().build()?);
// let client_options = ClientOptions { transport: Some(Transport::new(http)), ..Default::default() };

let client_options = ClientOptions::default();
let options = BlobContainerClientOptions { client_options, ..Default::default() };

// No TokenCredential required for SAS-only.
let container_client = BlobContainerClient::new_sas(endpoint, container, Some(options))?;

// The SAS query policy ensures SAS params are attached on every request.
// Example operation:
let _exists = container_client.exists().await?;
Ok(())
}
```

#### Permissions

You may need to specify RBAC roles to access Blob Storage via Microsoft Entra ID. Please see [Assign an Azure role for access to blob data] for more details.
Expand Down Expand Up @@ -115,6 +146,38 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}
```

### SAS-only: Upload Blob via `BlobContainerClient`

```rust no_run
use azure_core::http::{ClientOptions, RequestContent};
use azure_storage_blob::{BlobContainerClient, BlobContainerClientOptions};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Endpoint must include a valid SAS query string.
let endpoint = "https://<account>.blob.core.windows.net/?<SAS>";
let container = "container_name".to_string();

let options = BlobContainerClientOptions {
client_options: ClientOptions::default(),
..Default::default()
};
let container_client = BlobContainerClient::new_sas(endpoint, container, Some(options))?;

// Create a per-blob client from the container client; it inherits the SAS policy.
let blob = container_client.blob_client("blob_name".to_string());

let data = b"hello world";
blob.upload(
RequestContent::from(data.to_vec()),
false, // overwrite
u64::try_from(data.len())?, // content length
None, // upload options
).await?;
Ok(())
}
```

### Get Blob Properties

```rust no_run
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,41 @@ impl BlobContainerClient {
})
}

/// Creates a new BlobContainerClient using SAS-only authentication.
///
/// The `endpoint` must include a valid SAS query string; no Entra ID token is requested.
/// The pipeline is constructed without a bearer-token policy.
///
/// # Arguments
///
/// * `endpoint` - The full URL of the Azure storage account, for example `https://myaccount.blob.core.windows.net/`
/// with a SAS query string appended.
/// * `container_name` - The name of the container.
/// * `options` - Optional configuration for the client.
pub fn new_sas(
endpoint: &str,
container_name: String,
options: Option<BlobContainerClientOptions>,
) -> Result<Self> {
let mut options = options.unwrap_or_default();

// Ensure storage-specific headers are applied on each call, same as in `new`.
let storage_headers_policy = Arc::new(StorageHeadersPolicy);
options
.client_options
.per_call_policies
.push(storage_headers_policy);

// Construct generated client without bearer token auth (SAS-only).
let client =
GeneratedBlobContainerClient::new_sas(endpoint, container_name.clone(), Some(options))?;

Ok(Self {
endpoint: endpoint.parse()?,
client,
})
}

/// Returns a new instance of BlobClient.
///
/// # Arguments
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions sdk/storage/azure_storage_blob/src/pipeline/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

mod sas_query_policy;
mod storage_headers_policy;

pub use sas_query_policy::SasQueryPolicy;
pub use storage_headers_policy::StorageHeadersPolicy;
91 changes: 91 additions & 0 deletions sdk/storage/azure_storage_blob/src/pipeline/sas_query_policy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/* Copyright (c) Microsoft Corporation.
Licensed under the MIT License. */

use async_trait::async_trait;
use azure_core::http::{
policies::{Policy, PolicyResult},
Context, Request,
};
use std::collections::HashSet;
use std::sync::Arc;

/// A policy that ensures SAS query parameters from the client endpoint
/// are present on every outgoing request.
///
/// This is intended for use in SAS-only client constructors. The SAS query
/// string from the endpoint is parsed at construction time and cached in the
/// policy. For each request, any missing SAS parameters are appended to the
/// request URL, preserving existing query parameters added by the operation.
///
/// Notes:
/// - Existing query parameters are preserved and take precedence; SAS params
/// that are already present on the request will NOT be duplicated or
/// overwritten.
/// - Parameter name comparison is case-sensitive (Azure SAS parameters are
/// conventionally lower-case, e.g. `sv`, `ss`, `srt`, `sp`, `se`, `st`,
/// `spr`, `sig`).
#[derive(Debug, Clone)]
pub struct SasQueryPolicy {
pairs: Vec<(String, String)>,
}

impl SasQueryPolicy {
/// Create a policy from a raw query string (e.g., everything after the `?`).
///
/// If `query` is empty, the resulting policy will be a no-op.
pub fn from_query_str(query: &str) -> Self {
// Parse the query into key/value pairs, percent-decoding as necessary.
// The `append_pair` API will handle re-encoding on write.
let pairs = if query.is_empty() {
Vec::new()
} else {
url::form_urlencoded::parse(query.as_bytes())
.map(|(k, v)| (k.into_owned(), v.into_owned()))
.collect()
};
Self { pairs }
}

/// Returns true if the policy has no SAS parameters and will be a no-op.
pub fn is_empty(&self) -> bool {
self.pairs.is_empty()
}
}

#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl Policy for SasQueryPolicy {
async fn send(
&self,
ctx: &Context,
request: &mut Request,
next: &[Arc<dyn Policy>],
) -> PolicyResult {
// If we have nothing to append, skip quickly.
if self.pairs.is_empty() {
return next[0].send(ctx, request, &next[1..]).await;
}

// Snapshot existing query parameter names to avoid duplicates.
// We only check for the presence of the key (name); if present, we
// do not attempt to overwrite the value. This preserves any operation-
// specific overrides (e.g., pre-existing SAS on the request).
let existing: HashSet<String> = request
.url()
.query_pairs()
.map(|(k, _)| k.to_string())
.collect();

// Append missing SAS pairs.
{
let mut qp = request.url_mut().query_pairs_mut();
for (k, v) in &self.pairs {
if !existing.contains(k) {
qp.append_pair(k, v);
}
}
}

next[0].send(ctx, request, &next[1..]).await
}
}