Skip to content

Conversation

@joshcoughlan
Copy link

Add SAS-only constructors and a SAS query policy to ensure SAS is applied to every request

Motivation

  • Enable SAS-only authentication without requiring a TokenCredential/AAD for Azure Storage Blob clients.
  • Fix 401 NoAuthenticationInformation errors caused by SAS query parameters being dropped when request URLs are rebuilt in the pipeline.
  • Ensure SAS from the initial endpoint is consistently present on all per-blob operations (e.g., upload), including clients created via get_blob_client, by carrying SAS through the pipeline.

What’s changed

  • New SAS query policy
    • Introduces a per-call policy that appends SAS query parameters from the client’s endpoint to every outgoing request if they are missing.
    • Preserves existing query parameters and does not overwrite any already present on the request.
/* 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.
/// ...
#[derive(Debug, Clone)]
pub struct SasQueryPolicy {
    pairs: Vec<(String, String)>,
}
...
impl Policy for SasQueryPolicy {
    async fn send(
        &self,
        ctx: &Context,
        request: &mut Request,
        next: &[Arc<dyn Policy>],
    ) -> PolicyResult {
        if self.pairs.is_empty() {
            return next[0].send(ctx, request, &next[1..]).await;
        }
        let existing: HashSet<String> = request
            .url()
            .query_pairs()
            .map(|(k, _)| k.to_string())
            .collect();
        {
            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
    }
}
  • Pipeline exports
    • Expose the new policy from the pipeline module.
// 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;
  • Apply SAS policy in SAS-only constructor
    • In the SAS-only constructor of the generated BlobContainerClient, parse the endpoint’s query and push the SAS policy into the per-call policies so it’s applied to every request.
    • The BlobClient produced by get_blob_client inherits the same pipeline and thus also carries the SAS policy.
    #[tracing::new("Storage.Blob.Container")]
    pub fn new_sas(
        endpoint: &str,
        container_name: String,
        options: Option<BlobContainerClientOptions>,
    ) -> Result<Self> {
        let options = options.unwrap_or_default();
        let endpoint = Url::parse(endpoint)?;
        if !endpoint.scheme().starts_with("http") {
            return Err(azure_core::Error::with_message(
                azure_core::error::ErrorKind::Other,
                format!("{endpoint} must use http(s)"),
            ));
        }

        // Build per-call policies and ensure SAS query is present on every request.
        let mut per_call: Vec<Arc<dyn Policy>> = Vec::new();
        if let Some(q) = endpoint.query() {
            let sas = SasQueryPolicy::from_query_str(q);
            if !sas.is_empty() {
                per_call.push(Arc::new(sas));
            }
        }

        Ok(Self {
            container_name,
            endpoint,
            version: options.version.clone(),
            pipeline: Pipeline::new(
                option_env!("CARGO_PKG_NAME"),
                option_env!("CARGO_PKG_VERSION"),
                options.client_options,
                per_call,
                Vec::new(), // no bearer auth policy for SAS-only
                None,
            ),
        })
    }
    pub fn get_blob_client(&self, blob_name: String) -> BlobClient {
        BlobClient {
            blob_name,
            container_name: self.container_name.clone(),
            endpoint: self.endpoint.clone(),
            pipeline: self.pipeline.clone(),
            version: self.version.clone(),
        }
    }

Usage example

  • Build a SAS-only container client and inject an HTTP transport via azure_core::http::ClientOptions if desired:
use std::sync::Arc;
use azure_core::http::{ClientOptions, Transport};
use azure_storage_blob::{BlobContainerClient, BlobContainerClientOptions};

fn example() -> azure_core::Result<()> {
    // endpoint includes SAS: https://<account>.blob.core.windows.net/?sv=...&ss=b&...
    let endpoint = "https://myaccount.blob.core.windows.net/?<SAS>";
    let container = "my-container".to_string();

    let client_options = ClientOptions {
        // Optional: inject custom transport (e.g., reqwest) here
        // transport: Some(Transport::new(Arc::new(reqwest_client))),
        ..Default::default()
    };
    let options = BlobContainerClientOptions { client_options, ..Default::default() };

    let client = BlobContainerClient::new_sas(endpoint, container, Some(options))?;
    // use client.blob_client("name".into()).upload(...).await?;
    Ok(())
}

Why this fixes 401 NoAuthenticationInformation

  • Some operations rebuild request URLs and can inadvertently drop the base endpoint query. Without SAS in those requests, the service returns 401 and prompts for Bearer auth.
  • The SAS query policy ensures the SAS parameters are present on every outgoing request, preventing accidental omission even when paths/queries are recomposed.

Backwards compatibility

  • No breaking changes to existing TokenCredential-based constructors.
  • Adds a SAS-only constructor and an internal policy; existing users are unaffected.
  • Clients created via get_blob_client continue to inherit the parent pipeline (now including SAS policy when using new_sas).

Security considerations

  • SAS remains in the URL’s query (as expected for SAS auth).
  • Logging policies continue to redact sensitive data; this change does not log SAS values.
  • Policy only appends missing SAS parameters; it does not overwrite existing query params.

Testing

  • Manually validated with:
    • curl against a container using the same SAS (200 OK).
    • a SAS-only client performing Put Blob without 401s.
  • Integrations verified from a consumer (Vector’s azure_blob sink) using SAS and a custom transport.

@github-actions github-actions bot added Community Contribution Community members are working on the issue customer-reported Issues that are reported by GitHub users external to the Azure organization. Storage Storage Service (Queues, Blobs, Files) labels Nov 11, 2025
@github-actions
Copy link

Thank you for your contribution @joshcoughlan! We will review the pull request and get back to you soon.

@joshcoughlan
Copy link
Author

joshcoughlan commented Nov 11, 2025

Working on getting authorization from my employer to agree to the CLA. I don't think there will be any issue there.

This PR resolves #2465

@jalauzon-msft
Copy link
Member

Hi @joshcoughlan Josh, thanks for your contribution however we actually just added SAS support to the Blob SDK. It came as part of #3176 and was just released yesterday. Basically, clients now have a from_url method that allows you to pass a full URL with a SAS. This design closely matches our other SDKs and so it what we wanted to go with for Rust as well. Please give it a try and let us know if you have any feedback.

For now, I will close this PR, thanks again for your contribution.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Community Contribution Community members are working on the issue customer-reported Issues that are reported by GitHub users external to the Azure organization. Storage Storage Service (Queues, Blobs, Files)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants