Skip to content

Commit 7c4d042

Browse files
authored
Add AzureSasCredential (Azure#15946)
**In this PR:** - Add `AzureSasCredential` per Azure/azure-sdk#1954 - `AzureSasCredential` is the name that has been settled on the end of discussion. - Add `AzureSasCredentialPolicy` that appends SAS to query **Remarks:** - Some service (like storage in the Portal) present SAS with leading "?". This has to be stripped before appending - The validation if serviceUri already contain sas (mentioned [here](Azure/azure-sdk#1954 (comment))) will be responsibility of service clients: - the format varies between services (i.e. Event Grid SAS and Storage SAS are vastly different) - it would be good to fail fast (at client creation) rather than late (at request send). **References** - [.NET PR](Azure/azure-sdk-for-net#17636)
1 parent c04396e commit 7c4d042

File tree

6 files changed

+123
-7
lines changed

6 files changed

+123
-7
lines changed

sdk/core/azure-core/CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11

22
# Release History
33

4-
## 1.9.1 (Unreleased)
4+
## 1.10.0 (Unreleased)
5+
6+
### Features
7+
8+
- Added `AzureSasCredential` and its respective policy. #15946
59

610

711
## 1.9.0 (2020-11-09)

sdk/core/azure-core/azure/core/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@
99
# regenerated.
1010
# --------------------------------------------------------------------------
1111

12-
VERSION = "1.9.1"
12+
VERSION = "1.10.0"

sdk/core/azure-core/azure/core/credentials.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def get_token(self, *scopes, **kwargs):
3030

3131
AccessToken = namedtuple("AccessToken", ["token", "expires_on"])
3232

33-
__all__ = ["AzureKeyCredential", "AccessToken"]
33+
__all__ = ["AzureKeyCredential", "AzureSasCredential", "AccessToken"]
3434

3535

3636
class AzureKeyCredential(object):
@@ -71,3 +71,43 @@ def update(self, key):
7171
if not isinstance(key, six.string_types):
7272
raise TypeError("The key used for updating must be a string.")
7373
self._key = key
74+
75+
76+
class AzureSasCredential(object):
77+
"""Credential type used for authenticating to an Azure service.
78+
It provides the ability to update the shared access signature without creating a new client.
79+
80+
:param str signature: The shared access signature used to authenticate to an Azure service
81+
:raises: TypeError
82+
"""
83+
84+
def __init__(self, signature):
85+
# type: (str) -> None
86+
if not isinstance(signature, six.string_types):
87+
raise TypeError("signature must be a string.")
88+
self._signature = signature # type: str
89+
90+
@property
91+
def signature(self):
92+
# type () -> str
93+
"""The value of the configured shared access signature.
94+
95+
:rtype: str
96+
"""
97+
return self._signature
98+
99+
def update(self, signature):
100+
# type: (str) -> None
101+
"""Update the shared access signature.
102+
103+
This can be used when you've regenerated your shared access signature and want
104+
to update long-lived clients.
105+
106+
:param str signature: The shared access signature used to authenticate to an Azure service
107+
:raises: ValueError or TypeError
108+
"""
109+
if not signature:
110+
raise ValueError("The signature used for updating can not be None or empty")
111+
if not isinstance(signature, six.string_types):
112+
raise TypeError("The signature used for updating must be a string.")
113+
self._signature = signature

sdk/core/azure-core/azure/core/pipeline/policies/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
# --------------------------------------------------------------------------
2626

2727
from ._base import HTTPPolicy, SansIOHTTPPolicy, RequestHistory
28-
from ._authentication import BearerTokenCredentialPolicy, AzureKeyCredentialPolicy
28+
from ._authentication import BearerTokenCredentialPolicy, AzureKeyCredentialPolicy, AzureSasCredentialPolicy
2929
from ._custom_hook import CustomHookPolicy
3030
from ._redirect import RedirectPolicy
3131
from ._retry import RetryPolicy, RetryMode
@@ -45,6 +45,7 @@
4545
'SansIOHTTPPolicy',
4646
'BearerTokenCredentialPolicy',
4747
'AzureKeyCredentialPolicy',
48+
'AzureSasCredentialPolicy',
4849
'HeadersPolicy',
4950
'UserAgentPolicy',
5051
'NetworkTraceLoggingPolicy',

sdk/core/azure-core/azure/core/pipeline/policies/_authentication.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
if TYPE_CHECKING:
1818
# pylint:disable=unused-import
1919
from typing import Any, Dict, Optional
20-
from azure.core.credentials import AccessToken, TokenCredential, AzureKeyCredential
20+
from azure.core.credentials import AccessToken, TokenCredential, AzureKeyCredential, AzureSasCredential
2121
from azure.core.pipeline import PipelineRequest
2222

2323

@@ -114,3 +114,34 @@ def __init__(self, credential, name, **kwargs): # pylint: disable=unused-argume
114114

115115
def on_request(self, request):
116116
request.http_request.headers[self._name] = self._credential.key
117+
118+
119+
class AzureSasCredentialPolicy(SansIOHTTPPolicy):
120+
"""Adds a shared access signature to query for the provided credential.
121+
122+
:param credential: The credential used to authenticate requests.
123+
:type credential: ~azure.core.credentials.AzureSasCredential
124+
:raises: ValueError or TypeError
125+
"""
126+
def __init__(self, credential, **kwargs): # pylint: disable=unused-argument
127+
# type: (AzureSasCredential, **Any) -> None
128+
super(AzureSasCredentialPolicy, self).__init__()
129+
if not credential:
130+
raise ValueError("credential can not be None")
131+
self._credential = credential
132+
133+
def on_request(self, request):
134+
url = request.http_request.url
135+
query = request.http_request.query
136+
signature = self._credential.signature
137+
if signature.startswith("?"):
138+
signature = signature[1:]
139+
if query:
140+
if signature not in url:
141+
url = url + "&" + signature
142+
else:
143+
if url.endswith("?"):
144+
url = url + signature
145+
else:
146+
url = url + "?" + signature
147+
request.http_request.url = url

sdk/core/azure-core/tests/test_authentication.py

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
import time
77

88
import azure.core
9-
from azure.core.credentials import AccessToken, AzureKeyCredential
9+
from azure.core.credentials import AccessToken, AzureKeyCredential, AzureSasCredential
1010
from azure.core.exceptions import ServiceRequestError
1111
from azure.core.pipeline import Pipeline
12-
from azure.core.pipeline.policies import BearerTokenCredentialPolicy, SansIOHTTPPolicy, AzureKeyCredentialPolicy
12+
from azure.core.pipeline.policies import BearerTokenCredentialPolicy, SansIOHTTPPolicy, AzureKeyCredentialPolicy, AzureSasCredentialPolicy
1313
from azure.core.pipeline.transport import HttpRequest
1414

1515
import pytest
@@ -190,3 +190,43 @@ def test_azure_key_credential_updates():
190190
api_key = "new"
191191
credential.update(api_key)
192192
assert credential.key == api_key
193+
194+
@pytest.mark.parametrize("sas,url,expected_url", [
195+
("sig=test_signature", "https://test_sas_credential", "https://test_sas_credential?sig=test_signature"),
196+
("?sig=test_signature", "https://test_sas_credential", "https://test_sas_credential?sig=test_signature"),
197+
("sig=test_signature", "https://test_sas_credential?sig=test_signature", "https://test_sas_credential?sig=test_signature"),
198+
("?sig=test_signature", "https://test_sas_credential?sig=test_signature", "https://test_sas_credential?sig=test_signature"),
199+
("sig=test_signature", "https://test_sas_credential?", "https://test_sas_credential?sig=test_signature"),
200+
("?sig=test_signature", "https://test_sas_credential?", "https://test_sas_credential?sig=test_signature"),
201+
("sig=test_signature", "https://test_sas_credential?foo=bar", "https://test_sas_credential?foo=bar&sig=test_signature"),
202+
("?sig=test_signature", "https://test_sas_credential?foo=bar", "https://test_sas_credential?foo=bar&sig=test_signature"),
203+
])
204+
def test_azure_sas_credential_policy(sas, url, expected_url):
205+
"""Tests to see if we can create an AzureSasCredentialPolicy"""
206+
207+
def verify_authorization(request):
208+
assert request.url == expected_url
209+
210+
transport=Mock(send=verify_authorization)
211+
credential = AzureSasCredential(sas)
212+
credential_policy = AzureSasCredentialPolicy(credential=credential)
213+
pipeline = Pipeline(transport=transport, policies=[credential_policy])
214+
215+
pipeline.run(HttpRequest("GET", url))
216+
217+
def test_azure_sas_credential_updates():
218+
"""Tests AzureSasCredential updates"""
219+
sas = "original"
220+
221+
credential = AzureSasCredential(sas)
222+
assert credential.signature == sas
223+
224+
sas = "new"
225+
credential.update(sas)
226+
assert credential.signature == sas
227+
228+
def test_azure_sas_credential_policy_raises():
229+
"""Tests AzureSasCredential and AzureSasCredentialPolicy raises with non-string input parameters."""
230+
sas = 1234
231+
with pytest.raises(TypeError):
232+
credential = AzureSasCredential(sas)

0 commit comments

Comments
 (0)