Skip to content

Conversation

@joshcoughlan
Copy link

@joshcoughlan joshcoughlan commented Oct 28, 2025

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.

@alexcriss
Copy link

this looks very cool

@joshcoughlan joshcoughlan merged commit 7026713 into main Nov 11, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants