66from urllib .parse import urlparse
77
88from azure .core .exceptions import ClientAuthenticationError
9+ from azure .identity .aio import DefaultAzureCredential
910from azure .storage .blob import ContentSettings
1011from azure .storage .blob .aio import ContainerClient as AsyncContainerClient
1112
12- from pyrit .auth import AzureStorageAuth
1313from pyrit .common import default_values
1414from pyrit .models import ComponentIdentifier , Message , construct_response_from_request
1515from pyrit .prompt_target .common .prompt_target import PromptTarget
@@ -37,8 +37,9 @@ class AzureBlobStorageTarget(PromptTarget):
3737
3838 Args:
3939 container_url (str): URL to the Azure Blob Storage Container.
40- sas_token (optional[str]): Optional Blob SAS token needed to authenticate blob operations. If not provided, a
41- delegation SAS token will be created using Entra ID authentication.
40+ sas_token (optional[str]): Optional Blob SAS token needed to authenticate blob operations. If not provided,
41+ ``DefaultAzureCredential`` is used directly, which requires the caller to hold a data-plane role such
42+ as Storage Blob Data Contributor on the storage account.
4243 blob_content_type (SupportedContentType): Expected Content Type of the blob, chosen from the
4344 SupportedContentType enum. Set to PLAIN_TEXT by default.
4445 max_requests_per_minute (int, Optional): Number of requests the target can handle per
@@ -96,6 +97,7 @@ def __init__(
9697
9798 self ._sas_token : str | None = sas_token
9899 self ._client_async : AsyncContainerClient | None = None
100+ self ._credential : DefaultAzureCredential | None = None
99101
100102 super ().__init__ (
101103 endpoint = self ._container_url ,
@@ -121,22 +123,42 @@ async def _create_container_client_async(self) -> None:
121123 """
122124 Create an asynchronous ContainerClient for Azure Storage. If a SAS token is provided via the
123125 AZURE_STORAGE_ACCOUNT_SAS_TOKEN environment variable or the init sas_token parameter, it will be used
124- for authentication. Otherwise, a delegation SAS token will be created using Entra ID authentication.
126+ for authentication. Otherwise, ``DefaultAzureCredential`` is used directly, which requires the caller
127+ to hold a data-plane role such as Storage Blob Data Contributor on the storage account.
125128 """
126129 container_url , _ = self ._parse_url ()
127130 try :
128131 sas_token : str = default_values .get_required_value (
129132 env_var_name = self .SAS_TOKEN_ENVIRONMENT_VARIABLE , passed_value = self ._sas_token
130133 )
131- logger .info ("Using SAS token from environment variable or passed parameter." )
132134 except ValueError :
133- logger .info ("SAS token not provided. Creating a delegation SAS token using Entra ID authentication." )
134- sas_token = await AzureStorageAuth .get_sas_token_async (container_url )
135+ logger .info ("SAS token not provided. Using DefaultAzureCredential for direct Entra ID authentication." )
136+ account_url , _ , container_name = container_url .rpartition ("/" )
137+ self ._credential = DefaultAzureCredential ()
138+ self ._client_async = AsyncContainerClient (
139+ account_url = account_url ,
140+ container_name = container_name ,
141+ credential = self ._credential ,
142+ )
143+ return
144+
145+ logger .info ("Using SAS token from environment variable or passed parameter." )
135146 self ._client_async = AsyncContainerClient .from_container_url (
136147 container_url = container_url ,
137148 credential = sas_token ,
138149 )
139150
151+ async def _close_client_async (self ) -> None :
152+ """Close the container client and credential, resetting both to None."""
153+ client , self ._client_async = self ._client_async , None
154+ credential , self ._credential = self ._credential , None
155+ try :
156+ if client :
157+ await client .close ()
158+ finally :
159+ if credential :
160+ await credential .close ()
161+
140162 async def _upload_blob_async (self , file_name : str , data : bytes , content_type : str ) -> None :
141163 """
142164 (Async) Handles uploading blob to given storage container.
@@ -152,14 +174,14 @@ async def _upload_blob_async(self, file_name: str, data: bytes, content_type: st
152174 content_settings = ContentSettings (content_type = f"{ content_type } " )
153175 logger .info (msg = "\n Uploading to Azure Storage as blob:\n \t " + file_name )
154176
155- if not self ._client_async :
156- await self ._create_container_client_async ()
157177 # Parse the Azure Storage Blob URL to extract components
158178 _ , blob_prefix = self ._parse_url ()
159179 # If a blob prefix is provided, prepend it to the file name.
160180 # If not, the file will be put in the root of the container.
161181 blob_path = f"{ blob_prefix } /{ file_name } " if blob_prefix else file_name
162182 try :
183+ if not self ._client_async :
184+ await self ._create_container_client_async ()
163185 if self ._client_async is None :
164186 raise RuntimeError ("Blob storage client not initialized" )
165187 blob_client = self ._client_async .get_blob_client (blob = blob_path )
@@ -171,24 +193,34 @@ async def _upload_blob_async(self, file_name: str, data: bytes, content_type: st
171193 except Exception as exc :
172194 if isinstance (exc , ClientAuthenticationError ):
173195 logger .exception (
174- msg = "Authentication failed. Please check that the container existence in the "
175- "Azure Storage Account and ensure the validity of the provided SAS token. If you "
176- "haven't set the SAS token as an environment variable use `az login` to "
177- "enable delegation-based SAS authentication to connect to the storage account"
196+ msg = "Authentication failed. Please check that the container exists in the "
197+ "Azure Storage Account. If using a SAS token, ensure it is valid. Otherwise, "
198+ "ensure you are logged in via `az login` and hold a data-plane role such as "
199+ "Storage Blob Data Contributor on the storage account. "
178200 )
179201 raise
180202 logger .exception (msg = f"An unexpected error occurred: { exc } " )
181203 raise
204+ finally :
205+ await self ._close_client_async ()
182206
183207 def _parse_url (self ) -> tuple [str , str ]:
184208 """
185209 Parse the Azure Storage Blob URL to extract components.
186210
187211 Returns:
188212 tuple: A tuple containing the container URL and blob prefix.
213+
214+ Raises:
215+ ValueError: If the container URL does not include a container name in its path.
189216 """
190217 parsed_url = urlparse (self ._container_url )
191218 path_parts = parsed_url .path .split ("/" )
219+ if len (path_parts ) < 2 or not path_parts [1 ]:
220+ raise ValueError (
221+ f"Invalid Azure Storage container URL '{ self ._container_url } ': expected a container name in the "
222+ "path, e.g. https://<account>.blob.core.windows.net/<container>."
223+ )
192224 container_name = path_parts [1 ]
193225 blob_prefix = "/" .join (path_parts [2 :])
194226 container_url = f"https://{ parsed_url .netloc } /{ container_name } "
0 commit comments