diff --git a/sdk/storage/azure-storage-blob/azure/storage/blob/_shared/shared_access_signature.py b/sdk/storage/azure-storage-blob/azure/storage/blob/_shared/shared_access_signature.py index 0dae7284d490..f42b2a194256 100644 --- a/sdk/storage/azure-storage-blob/azure/storage/blob/_shared/shared_access_signature.py +++ b/sdk/storage/azure-storage-blob/azure/storage/blob/_shared/shared_access_signature.py @@ -42,6 +42,8 @@ class QueryStringConstants(object): SIGNED_KEY_SERVICE = "sks" SIGNED_KEY_VERSION = "skv" SIGNED_ENCRYPTION_SCOPE = "ses" + SIGNED_REQUEST_HEADERS = "srh" + SIGNED_REQUEST_QUERY_PARAMS = "srq" SIGNED_KEY_DELEGATED_USER_TID = "skdutid" SIGNED_DELEGATED_USER_OID = "sduoid" @@ -81,6 +83,8 @@ def to_list(): QueryStringConstants.SIGNED_KEY_SERVICE, QueryStringConstants.SIGNED_KEY_VERSION, QueryStringConstants.SIGNED_ENCRYPTION_SCOPE, + QueryStringConstants.SIGNED_REQUEST_HEADERS, + QueryStringConstants.SIGNED_REQUEST_QUERY_PARAMS, QueryStringConstants.SIGNED_KEY_DELEGATED_USER_TID, QueryStringConstants.SIGNED_DELEGATED_USER_OID, # for ADLS @@ -225,6 +229,18 @@ def add_override_response_headers( self._add_query(QueryStringConstants.SIGNED_CONTENT_LANGUAGE, content_language) self._add_query(QueryStringConstants.SIGNED_CONTENT_TYPE, content_type) + def add_request_headers(self, request_headers): + if not request_headers: + return + serialized = [f"{k}:{v}" for k, v in request_headers.items()] + self._add_query(QueryStringConstants.SIGNED_REQUEST_HEADERS, "\n".join(serialized) + "\n") + + def add_request_query_params(self, request_query_params): + if not request_query_params: + return + serialized = [f"{k}:{v}" for k, v in request_query_params.items()] + self._add_query(QueryStringConstants.SIGNED_REQUEST_QUERY_PARAMS, "\n" + "\n".join(serialized)) + def add_account_signature(self, account_name, account_key): def get_value_to_append(query): return_value = self.query_dict.get(query) or "" diff --git a/sdk/storage/azure-storage-blob/azure/storage/blob/_shared_access_signature.py b/sdk/storage/azure-storage-blob/azure/storage/blob/_shared_access_signature.py index b317a3322946..72d510df4453 100644 --- a/sdk/storage/azure-storage-blob/azure/storage/blob/_shared_access_signature.py +++ b/sdk/storage/azure-storage-blob/azure/storage/blob/_shared_access_signature.py @@ -68,6 +68,8 @@ def generate_blob( content_language: Optional[str] = None, content_type: Optional[str] = None, user_delegation_oid: Optional[str] = None, + request_headers: Optional[Dict[str, str]] = None, + request_query_params: Optional[Dict[str, str]] = None, sts_hook: Optional[Callable[[str], None]] = None, **kwargs: Any ) -> str: @@ -141,6 +143,12 @@ def generate_blob( Specifies the Entra ID of the user that is authorized to use the resulting SAS URL. The resulting SAS URL must be used in conjunction with an Entra ID token that has been issued to the user specified in this value. + :param Dict[str, str] request_headers: + Specifies a set of headers and their corresponding values that + must be present in the request when using this SAS. + :param Dict[str, str] request_query_params: + Specifies a set of query parameters and their corresponding values that + must be present in the request when using this SAS. :param sts_hook: For debugging purposes only. If provided, the hook is called with the string to sign that was used to generate the SAS. @@ -166,6 +174,8 @@ def generate_blob( content_type) sas.add_encryption_scope(**kwargs) sas.add_info_for_hns_account(**kwargs) + sas.add_request_headers(request_headers) + sas.add_request_query_params(request_query_params) sas.add_resource_signature(self.account_name, self.account_key, resource_path, user_delegation_key=self.user_delegation_key) @@ -188,6 +198,8 @@ def generate_container( content_language: Optional[str] = None, content_type: Optional[str] = None, user_delegation_oid: Optional[str] = None, + request_headers: Optional[Dict[str, str]] = None, + request_query_params: Optional[Dict[str, str]] = None, sts_hook: Optional[Callable[[str], None]] = None, **kwargs: Any ) -> str: @@ -251,6 +263,12 @@ def generate_container( Specifies the Entra ID of the user that is authorized to use the resulting SAS URL. The resulting SAS URL must be used in conjunction with an Entra ID token that has been issued to the user specified in this value. + :param Dict[str, str] request_headers: + Specifies a set of headers and their corresponding values that + must be present in the request when using this SAS. + :param Dict[str, str] request_query_params: + Specifies a set of query parameters and their corresponding values that + must be present in the request when using this SAS. :param sts_hook: For debugging purposes only. If provided, the hook is called with the string to sign that was used to generate the SAS. @@ -268,6 +286,8 @@ def generate_container( content_type) sas.add_encryption_scope(**kwargs) sas.add_info_for_hns_account(**kwargs) + sas.add_request_headers(request_headers) + sas.add_request_query_params(request_query_params) sas.add_resource_signature(self.account_name, self.account_key, container_name, user_delegation_key=self.user_delegation_key) @@ -336,6 +356,8 @@ def add_resource_signature(self, account_name, account_key, path, user_delegatio self.get_value_to_append(QueryStringConstants.SIGNED_RESOURCE) + self.get_value_to_append(BlobQueryStringConstants.SIGNED_TIMESTAMP) + self.get_value_to_append(QueryStringConstants.SIGNED_ENCRYPTION_SCOPE) + + self.get_value_to_append(QueryStringConstants.SIGNED_REQUEST_HEADERS) + + self.get_value_to_append(QueryStringConstants.SIGNED_REQUEST_QUERY_PARAMS) + self.get_value_to_append(QueryStringConstants.SIGNED_CACHE_CONTROL) + self.get_value_to_append(QueryStringConstants.SIGNED_CONTENT_DISPOSITION) + self.get_value_to_append(QueryStringConstants.SIGNED_CONTENT_ENCODING) + @@ -533,11 +555,11 @@ def generate_container_sas( The resulting SAS URL must be used in conjunction with an Entra ID token that has been issued to the user specified in this value. :keyword Dict[str, str] request_headers: - If specified, both the correct request header(s) and corresponding values must be present, - or the request will fail. + Specifies a set of headers and their corresponding values that + must be present in the request when using this SAS. :keyword Dict[str, str] request_query_params: - If specified, both the correct query parameter(s) and corresponding values must be present, - or the request will fail. + Specifies a set of query parameters and their corresponding values that + must be present in the request when using this SAS. :keyword sts_hook: For debugging purposes only. If provided, the hook is called with the string to sign that was used to generate the SAS. @@ -575,6 +597,8 @@ def generate_container_sas( policy_id=policy_id, ip=ip, user_delegation_oid=user_delegation_oid, + request_headers=request_headers, + request_query_params=request_query_params, sts_hook=sts_hook, **kwargs ) @@ -725,8 +749,10 @@ def generate_blob_sas( start=start, policy_id=policy_id, ip=ip, - sts_hook=sts_hook, user_delegation_oid=user_delegation_oid, + request_headers=request_headers, + request_query_params=request_query_params, + sts_hook=sts_hook, **kwargs ) diff --git a/sdk/storage/azure-storage-blob/tests/test_common_blob.py b/sdk/storage/azure-storage-blob/tests/test_common_blob.py index 5d63ecd1c949..090fbc7f531a 100644 --- a/sdk/storage/azure-storage-blob/tests/test_common_blob.py +++ b/sdk/storage/azure-storage-blob/tests/test_common_blob.py @@ -11,6 +11,7 @@ from datetime import datetime, timedelta from enum import Enum from io import BytesIO +from urllib.parse import urlencode, urlparse, urlunparse from azure.mgmt.storage import StorageManagementClient @@ -3746,4 +3747,59 @@ def test_download_blob_no_decompress_chunks(self, **kwargs): result = blob.download_blob(decompress=False).readall() assert result == compressed_data + @pytest.mark.live_test_only + @BlobPreparer() + def test_dynamic_user_delegation_sas(self, **kwargs): + storage_account_name = kwargs.pop("storage_account_name") + + token_credential = self.get_credential(BlobServiceClient) + service = BlobServiceClient(self.account_url(storage_account_name, "blob"), credential=token_credential) + container_name, blob_name = self.get_resource_name('oauthcontainer'), self.get_resource_name('oauthblob') + container = service.create_container(container_name) + blob = container.get_blob_client(blob_name) + blob.upload_blob(b"abc") + + user_delegation_key = service.get_user_delegation_key( + key_start_time=datetime.utcnow(), + key_expiry_time=datetime.utcnow() + timedelta(hours=1) + ) + + request_headers = { + "foo$": "bar!", + "company": "msft", + "city": "redmond,atlanta,reston", + } + + request_query_params = { + "hello$": "world!", + "abra": "cadabra", + "firstName": "john,Tim", + } + + blob_token = self.generate_sas( + generate_blob_sas, + blob.account_name, + blob.container_name, + blob.blob_name, + permission=BlobSasPermissions(read=True), + expiry=datetime.utcnow() + timedelta(hours=1), + user_delegation_key=user_delegation_key, + request_headers=request_headers, + request_query_params=request_query_params + ) + + def callback(request): + request.http_request.headers["foo$"] = "world" + request.http_request.headers["company"] = "microsoft" + request.http_request.headers["city"] = "redmond,atlanta,reston" + + parsed_url = urlparse(request.http_request.url) + query = urlencode(request_query_params) + url = urlunparse(parsed_url._replace(query=query)) + request.http_request.url = url + + identity_blob = BlobClient.from_blob_url(f"{blob.url}?{blob_token}", credential=token_credential) + props = identity_blob.get_blob_properties(raw_request_hook=callback) + assert props is not None + # ------------------------------------------------------------------------------ \ No newline at end of file diff --git a/sdk/storage/azure-storage-blob/tests/test_common_blob_async.py b/sdk/storage/azure-storage-blob/tests/test_common_blob_async.py index 1d6217ba8fc8..0aab3e36e619 100644 --- a/sdk/storage/azure-storage-blob/tests/test_common_blob_async.py +++ b/sdk/storage/azure-storage-blob/tests/test_common_blob_async.py @@ -12,6 +12,7 @@ from datetime import datetime, timedelta from enum import Enum from io import BytesIO +from urllib.parse import urlencode, urlparse, urlunparse import aiohttp import pytest @@ -3684,4 +3685,59 @@ async def test_download_blob_no_decompress_chunks(self, **kwargs): result = await (await blob.download_blob(decompress=False)).readall() assert result == compressed_data + @pytest.mark.live_test_only + @BlobPreparer() + async def test_dynamic_user_delegation_sas(self, **kwargs): + storage_account_name = kwargs.pop("storage_account_name") + + token_credential = self.get_credential(BlobServiceClient, is_async=True) + service = BlobServiceClient(self.account_url(storage_account_name, "blob"), credential=token_credential) + container_name, blob_name = self.get_resource_name('oauthcontainer'), self.get_resource_name('oauthblob') + container = await service.create_container(container_name) + blob = container.get_blob_client(blob_name) + await blob.upload_blob(b"abc") + + user_delegation_key = await service.get_user_delegation_key( + key_start_time=datetime.utcnow(), + key_expiry_time=datetime.utcnow() + timedelta(hours=1) + ) + + request_headers = { + "foo$": "bar!", + "company": "msft", + "city": "redmond,atlanta,reston", + } + + request_query_params = { + "hello$": "world!", + "abra": "cadabra", + "firstName": "john,Tim", + } + + blob_token = self.generate_sas( + generate_blob_sas, + blob.account_name, + blob.container_name, + blob.blob_name, + permission=BlobSasPermissions(read=True), + expiry=datetime.utcnow() + timedelta(hours=1), + user_delegation_key=user_delegation_key, + request_headers=request_headers, + request_query_params=request_query_params + ) + + def callback(request): + request.http_request.headers["foo$"] = "world" + request.http_request.headers["company"] = "microsoft" + request.http_request.headers["city"] = "redmond,atlanta,reston" + + parsed_url = urlparse(request.http_request.url) + query = urlencode(request_query_params) + url = urlunparse(parsed_url._replace(query=query)) + request.http_request.url = url + + identity_blob = BlobClient.from_blob_url(f"{blob.url}?{blob_token}", credential=token_credential) + props = await identity_blob.get_blob_properties(raw_request_hook=callback) + assert props is not None + # ------------------------------------------------------------------------------ diff --git a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared/shared_access_signature.py b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared/shared_access_signature.py index 6556a066dde2..067107501f5a 100644 --- a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared/shared_access_signature.py +++ b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared/shared_access_signature.py @@ -42,6 +42,8 @@ class QueryStringConstants(object): SIGNED_KEY_SERVICE = "sks" SIGNED_KEY_VERSION = "skv" SIGNED_ENCRYPTION_SCOPE = "ses" + SIGNED_REQUEST_HEADERS = "srh" + SIGNED_REQUEST_QUERY_PARAMS = "srq" SIGNED_KEY_DELEGATED_USER_TID = "skdutid" SIGNED_DELEGATED_USER_OID = "sduoid" @@ -81,6 +83,8 @@ def to_list(): QueryStringConstants.SIGNED_KEY_SERVICE, QueryStringConstants.SIGNED_KEY_VERSION, QueryStringConstants.SIGNED_ENCRYPTION_SCOPE, + QueryStringConstants.SIGNED_REQUEST_HEADERS, + QueryStringConstants.SIGNED_REQUEST_QUERY_PARAMS, QueryStringConstants.SIGNED_KEY_DELEGATED_USER_TID, QueryStringConstants.SIGNED_DELEGATED_USER_OID, # for ADLS @@ -225,6 +229,18 @@ def add_override_response_headers( self._add_query(QueryStringConstants.SIGNED_CONTENT_LANGUAGE, content_language) self._add_query(QueryStringConstants.SIGNED_CONTENT_TYPE, content_type) + def add_request_headers(self, request_headers): + if not request_headers: + return + serialized = [f"{k}:{v}" for k, v in request_headers.items()] + self._add_query(QueryStringConstants.SIGNED_REQUEST_HEADERS, "\n".join(serialized) + "\n") + + def add_request_query_params(self, request_query_params): + if not request_query_params: + return + serialized = [f"{k}:{v}" for k, v in request_query_params.items()] + self._add_query(QueryStringConstants.SIGNED_REQUEST_QUERY_PARAMS, "\n" + "\n".join(serialized)) + def add_account_signature(self, account_name, account_key): def get_value_to_append(query): return_value = self.query_dict.get(query) or "" diff --git a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared_access_signature.py b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared_access_signature.py index a72da1e18361..506721dd69ab 100644 --- a/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared_access_signature.py +++ b/sdk/storage/azure-storage-file-datalake/azure/storage/filedatalake/_shared_access_signature.py @@ -201,11 +201,11 @@ def generate_file_system_sas( The resulting SAS URL must be used in conjunction with an Entra ID token that has been issued to the user specified in this value. :keyword Dict[str, str] request_headers: - If specified, both the correct request header(s) and corresponding values must be present, - or the request will fail. + Specifies a set of headers and their corresponding values that + must be present in the request when using this SAS. :keyword Dict[str, str] request_query_params: - If specified, both the correct query parameter(s) and corresponding values must be present, - or the request will fail. + Specifies a set of query parameters and their corresponding values that + must be present in the request when using this SAS. :keyword sts_hook: For debugging purposes only. If provided, the hook is called with the string to sign that was used to generate the SAS. @@ -221,6 +221,8 @@ def generate_file_system_sas( permission=cast(Optional[Union["ContainerSasPermissions", str]], permission), expiry=expiry, user_delegation_oid=user_delegation_oid, + request_headers=request_headers, + request_query_params=request_query_params, sts_hook=sts_hook, **kwargs ) @@ -329,11 +331,11 @@ def generate_directory_sas( The resulting SAS URL must be used in conjunction with an Entra ID token that has been issued to the user specified in this value. :keyword Dict[str, str] request_headers: - If specified, both the correct request header(s) and corresponding values must be present, - or the request will fail. + Specifies a set of headers and their corresponding values that + must be present in the request when using this SAS. :keyword Dict[str, str] request_query_params: - If specified, both the correct query parameter(s) and corresponding values must be present, - or the request will fail. + Specifies a set of query parameters and their corresponding values that + must be present in the request when using this SAS. :keyword sts_hook: For debugging purposes only. If provided, the hook is called with the string to sign that was used to generate the SAS. @@ -353,6 +355,8 @@ def generate_directory_sas( sdd=depth, is_directory=True, user_delegation_oid=user_delegation_oid, + request_headers=request_headers, + request_query_params=request_query_params, sts_hook=sts_hook, **kwargs ) @@ -464,11 +468,11 @@ def generate_file_sas( The resulting SAS URL must be used in conjunction with an Entra ID token that has been issued to the user specified in this value. :keyword Dict[str, str] request_headers: - If specified, both the correct request header(s) and corresponding values must be present, - or the request will fail. + Specifies a set of headers and their corresponding values that + must be present in the request when using this SAS. :keyword Dict[str, str] request_query_params: - If specified, both the correct query parameter(s) and corresponding values must be present, - or the request will fail. + Specifies a set of query parameters and their corresponding values that + must be present in the request when using this SAS. :keyword sts_hook: For debugging purposes only. If provided, the hook is called with the string to sign that was used to generate the SAS. @@ -488,8 +492,10 @@ def generate_file_sas( user_delegation_key=credential if not isinstance(credential, str) else None, permission=cast(Optional[Union["BlobSasPermissions", str]], permission), expiry=expiry, - sts_hook=sts_hook, user_delegation_oid=user_delegation_oid, + request_headers=request_headers, + request_query_params=request_query_params, + sts_hook=sts_hook, **kwargs ) diff --git a/sdk/storage/azure-storage-file-datalake/tests/test_file.py b/sdk/storage/azure-storage-file-datalake/tests/test_file.py index 85a63832f6c1..861bbc8f13fe 100644 --- a/sdk/storage/azure-storage-file-datalake/tests/test_file.py +++ b/sdk/storage/azure-storage-file-datalake/tests/test_file.py @@ -7,6 +7,7 @@ import unittest from datetime import datetime, timedelta from math import ceil +from urllib.parse import urlencode, urlparse, urlunparse import pytest from azure.core import MatchConditions @@ -1779,6 +1780,66 @@ def test_download_file_no_decompress_chunks(self, **kwargs): result = file_client.download_file(decompress=False).readall() assert result == compressed_data + @pytest.mark.live_test_only + @DataLakePreparer() + def test_dynamic_user_delegation_sas(self, **kwargs): + datalake_storage_account_name = kwargs.pop("datalake_storage_account_name") + + token_credential = self.get_credential(DataLakeServiceClient) + dsc = DataLakeServiceClient(self.account_url(datalake_storage_account_name, "dfs"), credential=token_credential) + fs_name, file_name = self.get_resource_name('oauthfs'), self.get_resource_name('oauthfile') + fs = dsc.create_file_system(fs_name) + file = fs.create_file(file_name) + file.upload_data(b"abc", overwrite=True) + + user_delegation_key = dsc.get_user_delegation_key( + key_start_time=datetime.utcnow(), + key_expiry_time=datetime.utcnow() + timedelta(hours=1) + ) + + request_headers = { + "foo$": "bar!", + "company": "msft", + "city": "redmond,atlanta,reston", + } + + request_query_params = { + "hello$": "world!", + "abra": "cadabra", + "firstName": "john,Tim", + } + + sas_token = generate_file_sas( + file.account_name, + file.file_system_name, + None, + file.path_name, + user_delegation_key, + permission=FileSasPermissions(read=True), + expiry=datetime.utcnow() + timedelta(hours=1), + request_headers=request_headers, + request_query_params=request_query_params + ) + + def callback(request): + request.http_request.headers["foo$"] = "world" + request.http_request.headers["company"] = "microsoft" + request.http_request.headers["city"] = "redmond,atlanta,reston" + + parsed_url = urlparse(request.http_request.url) + query = urlencode(request_query_params) + url = urlunparse(parsed_url._replace(query=query)) + request.http_request.url = url + + identity_file = DataLakeFileClient( + self.account_url(datalake_storage_account_name, 'dfs'), + file.file_system_name, + file.path_name, + credential=sas_token + ) + props = identity_file.get_file_properties(raw_request_hook=callback) + assert props is not None + # ------------------------------------------------------------------------------ if __name__ == '__main__': unittest.main() diff --git a/sdk/storage/azure-storage-file-datalake/tests/test_file_async.py b/sdk/storage/azure-storage-file-datalake/tests/test_file_async.py index 428383995abc..365c0f04609d 100644 --- a/sdk/storage/azure-storage-file-datalake/tests/test_file_async.py +++ b/sdk/storage/azure-storage-file-datalake/tests/test_file_async.py @@ -8,6 +8,7 @@ import unittest from datetime import datetime, timedelta from math import ceil +from urllib.parse import urlencode, urlparse, urlunparse import pytest from azure.core import MatchConditions @@ -1683,6 +1684,66 @@ async def test_download_file_no_decompress_chunks(self, **kwargs): result = await (await file_client.download_file(decompress=False)).readall() assert result == compressed_data + @pytest.mark.live_test_only + @DataLakePreparer() + async def test_dynamic_user_delegation_sas(self, **kwargs): + datalake_storage_account_name = kwargs.pop("datalake_storage_account_name") + + token_credential = self.get_credential(DataLakeServiceClient, is_async=True) + dsc = DataLakeServiceClient(self.account_url(datalake_storage_account_name, "dfs"), credential=token_credential) + fs_name, file_name = self.get_resource_name('oauthfs'), self.get_resource_name('oauthfile') + fs = await dsc.create_file_system(fs_name) + file = await fs.create_file(file_name) + await file.upload_data(b"abc", overwrite=True) + + user_delegation_key = await dsc.get_user_delegation_key( + key_start_time=datetime.utcnow(), + key_expiry_time=datetime.utcnow() + timedelta(hours=1) + ) + + request_headers = { + "foo$": "bar!", + "company": "msft", + "city": "redmond,atlanta,reston", + } + + request_query_params = { + "hello$": "world!", + "abra": "cadabra", + "firstName": "john,Tim", + } + + sas_token = generate_file_sas( + file.account_name, + file.file_system_name, + None, + file.path_name, + user_delegation_key, + permission=FileSasPermissions(read=True), + expiry=datetime.utcnow() + timedelta(hours=1), + request_headers=request_headers, + request_query_params=request_query_params + ) + + def callback(request): + request.http_request.headers["foo$"] = "world" + request.http_request.headers["company"] = "microsoft" + request.http_request.headers["city"] = "redmond,atlanta,reston" + + parsed_url = urlparse(request.http_request.url) + query = urlencode(request_query_params) + url = urlunparse(parsed_url._replace(query=query)) + request.http_request.url = url + + identity_file = DataLakeFileClient( + self.account_url(datalake_storage_account_name, 'dfs'), + file.file_system_name, + file.path_name, + credential=sas_token + ) + props = identity_file.get_file_properties(raw_request_hook=callback) + assert props is not None + # ------------------------------------------------------------------------------ if __name__ == '__main__': unittest.main() diff --git a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_shared/shared_access_signature.py b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_shared/shared_access_signature.py index aa16d249e070..4bfb3e77ea15 100644 --- a/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_shared/shared_access_signature.py +++ b/sdk/storage/azure-storage-file-share/azure/storage/fileshare/_shared/shared_access_signature.py @@ -42,6 +42,8 @@ class QueryStringConstants(object): SIGNED_KEY_SERVICE = "sks" SIGNED_KEY_VERSION = "skv" SIGNED_ENCRYPTION_SCOPE = "ses" + SIGNED_REQUEST_HEADERS = "srh" + SIGNED_REQUEST_QUERY_PARAMS = "srq" SIGNED_KEY_DELEGATED_USER_TID = "skdutid" SIGNED_DELEGATED_USER_OID = "sduoid" @@ -81,6 +83,8 @@ def to_list(): QueryStringConstants.SIGNED_KEY_SERVICE, QueryStringConstants.SIGNED_KEY_VERSION, QueryStringConstants.SIGNED_ENCRYPTION_SCOPE, + QueryStringConstants.SIGNED_REQUEST_HEADERS, + QueryStringConstants.SIGNED_REQUEST_QUERY_PARAMS, QueryStringConstants.SIGNED_KEY_DELEGATED_USER_TID, QueryStringConstants.SIGNED_DELEGATED_USER_OID, # for ADLS @@ -218,6 +222,18 @@ def add_override_response_headers( self._add_query(QueryStringConstants.SIGNED_CONTENT_LANGUAGE, content_language) self._add_query(QueryStringConstants.SIGNED_CONTENT_TYPE, content_type) + def add_request_headers(self, request_headers): + if not request_headers: + return + serialized = [f"{k}:{v}" for k, v in request_headers.items()] + self._add_query(QueryStringConstants.SIGNED_REQUEST_HEADERS, "\n".join(serialized) + "\n") + + def add_request_query_params(self, request_query_params): + if not request_query_params: + return + serialized = [f"{k}:{v}" for k, v in request_query_params.items()] + self._add_query(QueryStringConstants.SIGNED_REQUEST_QUERY_PARAMS, "\n" + "\n".join(serialized)) + def add_account_signature(self, account_name, account_key): def get_value_to_append(query): return_value = self.query_dict.get(query) or "" diff --git a/sdk/storage/azure-storage-queue/azure/storage/queue/_shared/shared_access_signature.py b/sdk/storage/azure-storage-queue/azure/storage/queue/_shared/shared_access_signature.py index 3bc715ea845b..a517d233082d 100644 --- a/sdk/storage/azure-storage-queue/azure/storage/queue/_shared/shared_access_signature.py +++ b/sdk/storage/azure-storage-queue/azure/storage/queue/_shared/shared_access_signature.py @@ -42,6 +42,8 @@ class QueryStringConstants(object): SIGNED_KEY_SERVICE = "sks" SIGNED_KEY_VERSION = "skv" SIGNED_ENCRYPTION_SCOPE = "ses" + SIGNED_REQUEST_HEADERS = "srh" + SIGNED_REQUEST_QUERY_PARAMS = "srq" SIGNED_KEY_DELEGATED_USER_TID = "skdutid" SIGNED_DELEGATED_USER_OID = "sduoid" @@ -81,6 +83,8 @@ def to_list(): QueryStringConstants.SIGNED_KEY_SERVICE, QueryStringConstants.SIGNED_KEY_VERSION, QueryStringConstants.SIGNED_ENCRYPTION_SCOPE, + QueryStringConstants.SIGNED_REQUEST_HEADERS, + QueryStringConstants.SIGNED_REQUEST_QUERY_PARAMS, QueryStringConstants.SIGNED_KEY_DELEGATED_USER_TID, QueryStringConstants.SIGNED_DELEGATED_USER_OID, # for ADLS @@ -226,6 +230,18 @@ def add_override_response_headers( self._add_query(QueryStringConstants.SIGNED_CONTENT_LANGUAGE, content_language) self._add_query(QueryStringConstants.SIGNED_CONTENT_TYPE, content_type) + def add_request_headers(self, request_headers): + if not request_headers: + return + serialized = [f"{k}:{v}" for k, v in request_headers.items()] + self._add_query(QueryStringConstants.SIGNED_REQUEST_HEADERS, "\n".join(serialized) + "\n") + + def add_request_query_params(self, request_query_params): + if not request_query_params: + return + serialized = [f"{k}:{v}" for k, v in request_query_params.items()] + self._add_query(QueryStringConstants.SIGNED_REQUEST_QUERY_PARAMS, "\n" + "\n".join(serialized)) + def add_account_signature(self, account_name, account_key): def get_value_to_append(query): return_value = self.query_dict.get(query) or ""