Repository: jschneier/django-storages

Issue:
AzureStorage fails with valid connection string using shared access signature
In version 4.2, additional checks have been added to the initialiser of `AzureStorage`

https://github.com/jschneier/django-storages/blob/d1cd2db41fd3bec01fe97d4b095159f974b91673/storages/backends/azure_storage.py#L132-L134

However, when using a connection string with shared access signature, the account name, and specifically, the account key should be omitted. 

From azure docs https://learn.microsoft.com/en-gb/rest/api/storageservices/delegate-access-with-shared-access-signature:

> A shared access signature (SAS) is a URI that grants restricted access rights to Azure Storage resources. You can provide a shared access signature to clients **who should not be trusted with your storage account key but whom you wish to delegate access to certain storage account resources**. By distributing a shared access signature URI to these clients, you grant them access to a resource for a specified period of time.

Because of the additional checks, a connection string that worked fine on v1.13.2 now fails on v1.14


Discussion:

Resolving patch:
diff --git a/storages/backends/azure_storage.py b/storages/backends/azure_storage.py
--- a/storages/backends/azure_storage.py
+++ b/storages/backends/azure_storage.py
@@ -1,10 +1,10 @@
 import mimetypes
-import re
 from datetime import datetime
 from datetime import timedelta
 from tempfile import SpooledTemporaryFile
 
 from azure.core.exceptions import ResourceNotFoundError
+from azure.core.utils import parse_connection_string
 from azure.storage.blob import BlobClient
 from azure.storage.blob import BlobSasPermissions
 from azure.storage.blob import BlobServiceClient
@@ -124,10 +124,13 @@ def __init__(self, **settings):
         self._user_delegation_key = None
         self._user_delegation_key_expiry = datetime.utcnow()
         if self.connection_string and (not self.account_name or not self.account_key):
-            self.account_name = re.search(
-                r"AccountName=(\w+);", self.connection_string
-            )[1]
-            self.account_key = re.search(r"AccountKey=(.*);", self.connection_string)[1]
+            parsed = parse_connection_string(
+                self.connection_string, case_sensitive_keys=True
+            )
+            if not self.account_name and "AccountName" in parsed:
+                self.account_name = parsed["AccountName"]
+            if not self.account_key and "AccountKey" in parsed:
+                self.account_key = parsed["AccountKey"]
 
     def get_default_settings(self):
         return {


Test patch:
diff --git a/tests/test_azure.py b/tests/test_azure.py
--- a/tests/test_azure.py
+++ b/tests/test_azure.py
@@ -292,6 +292,13 @@ def test_container_client_params_token_credential(self):
                 "https://test.blob.core.windows.net", credential="foo_cred"
             )
 
+    def test_connection_string_can_have_missing(self):
+        storage = azure_storage.AzureStorage(
+            connection_string="AccountKey=abc;Foobar=xyz;"
+        )
+        self.assertEqual(storage.account_key, "abc")
+        self.assertIsNone(storage.account_name)
+
     def test_container_client_params_connection_string(self):
         storage = azure_storage.AzureStorage()
         storage.account_name = self.account_name