Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add AzureNamedKeyCredential #17548

Merged
merged 19 commits into from
Apr 25, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sdk/core/azure-core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Features

- Added `azure.core.credentials.AzureNamedKeyCredential` credential #17548.
rakshith91 marked this conversation as resolved.
Show resolved Hide resolved
- Supported adding custom policies #16519

### Bug fixes
Expand Down
48 changes: 47 additions & 1 deletion sdk/core/azure-core/azure/core/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def get_token(self, *scopes, **kwargs):

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

__all__ = ["AzureKeyCredential", "AzureSasCredential", "AccessToken"]
__all__ = ["AzureKeyCredential", "AzureSasCredential", "AccessToken", "AzureNamedKeyCredential"]


class AzureKeyCredential(object):
Expand Down Expand Up @@ -111,3 +111,49 @@ def update(self, signature):
if not isinstance(signature, six.string_types):
raise TypeError("The signature used for updating must be a string.")
self._signature = signature


class AzureNamedKeyCredential(object):
"""Credential type used for working with any service needing a named key that follows patterns
established by the other credential types.

:param str name: The name of the credential used to authenticate to an Azure service.
:param str key: The key used to authenticate to an Azure service.
:raises: TypeError
"""
def __init__(self, name, key):
# type: (str, str) -> None
if not isinstance(name, six.string_types) or not isinstance(key, six.string_types):
raise TypeError("Both name and key must be Strings.")
self._name = name
self._key = key

@property
def name(self):
# type () -> str
"""The value of the configured name.

:rtype: str
"""
return self._name

@property
def key(self):
# type () -> str
"""The value of the configured key.

:rtype: str
"""
return self._key

def update(self, name, key):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have scenarios where you want to update/change the name of the key?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think one scenario could be that users want to use a new share access policy which has new name and key.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can user update key only?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

technically - they are allowed to provide the same name - but they must provide a name

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the most common scenario is the name is fixed after creation, but user may modify the key value?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yunhaoling any thoughts on this?

If we know that users may normally not modify the name, I'll incline towards making name kwarg only

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually on second thought, i think we should let this be - as much as i like kwargs, it doesn't seem like they'll grow later in this case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, if the user thinks they wont need to change name for their use case, AzureKeyCredential serves their purpose

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...unless they need to initially specify the name?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, right - still doesn't change tht kwargs would not grow in this case - so will stick to it

# type: (str, str) -> None
"""Update the named key credential.

Both name and key must be provided in order to update the named key credential.
Individual attributes cannot be updated.
"""
if not isinstance(name, six.string_types) or not isinstance(key, six.string_types):
raise TypeError("Both name and key must be Strings.")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why Strings (with capital S)?

self._name = name
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason for a single update method rather than two read/write properties is (per my understanding) to make the assignment atomic. This is not an atomic assignment. Is there a specific reason we have an non-atomic update method?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How common user creates an AzureNamedKeyCredential then modify the name?

It seems to me maybe

 def update(self, key, **kwarg):

makes more sense?

@johanste

Copy link
Contributor Author

@rakshith91 rakshith91 Mar 29, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the update method is to

  1. be consistent with other creds in python (aka AzureKeyCredential)
  2. be consistent with other languages when it comes to AzureNamedKeyCredential

should we have two read/write properties instead of an update method?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. The reason for the update method is to be atomic. You cannot do that unless you expect callers to lock the instance when reading values.

self._key = key
6 changes: 5 additions & 1 deletion sdk/core/azure-core/azure/core/pipeline/policies/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@
# --------------------------------------------------------------------------

from ._base import HTTPPolicy, SansIOHTTPPolicy, RequestHistory
from ._authentication import BearerTokenCredentialPolicy, AzureKeyCredentialPolicy, AzureSasCredentialPolicy
from ._authentication import (
BearerTokenCredentialPolicy, AzureKeyCredentialPolicy, AzureSasCredentialPolicy,
AzureNamedKeyCredentialPolicy
)
rakshith91 marked this conversation as resolved.
Show resolved Hide resolved
from ._custom_hook import CustomHookPolicy
from ._redirect import RedirectPolicy
from ._retry import RetryPolicy, RetryMode
Expand All @@ -45,6 +48,7 @@
'SansIOHTTPPolicy',
'BearerTokenCredentialPolicy',
'AzureKeyCredentialPolicy',
'AzureNamedKeyCredentialPolicy',
rakshith91 marked this conversation as resolved.
Show resolved Hide resolved
'AzureSasCredentialPolicy',
'HeadersPolicy',
'UserAgentPolicy',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@
if TYPE_CHECKING:
# pylint:disable=unused-import
from typing import Any, Dict, Optional
from azure.core.credentials import AccessToken, TokenCredential, AzureKeyCredential, AzureSasCredential
from azure.core.credentials import (
AccessToken,
TokenCredential,
AzureKeyCredential,
AzureSasCredential,
AzureNamedKeyCredential
)
from azure.core.pipeline import PipelineRequest


Expand Down Expand Up @@ -145,3 +151,20 @@ def on_request(self, request):
else:
url = url + "?" + signature
request.http_request.url = url


class AzureNamedKeyCredentialPolicy(SansIOHTTPPolicy):
"""Adds a key header for the provided credential.

:param credential: The credential used to authenticate requests.
:type credential: ~azure.core.credentials.AzureNamedKeyCredential
:raises: ValueError or TypeError
"""
def __init__(self, credential, **kwargs): # pylint: disable=unused-argument
# type: (AzureNamedKeyCredential, **Any) -> None
super(AzureNamedKeyCredentialPolicy, self).__init__()
self._key = credential.key
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens when a client is created with a given NamedKeyCredential and later call update on the NamedKeyCredential? The whole reason for the update method is to be able to roll the key.

self._name = credential.name

def on_request(self, request):
request.http_request.headers[self._name] = self._key
45 changes: 43 additions & 2 deletions sdk/core/azure-core/tests/test_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
import time

import azure.core
from azure.core.credentials import AccessToken, AzureKeyCredential, AzureSasCredential
from azure.core.credentials import AccessToken, AzureKeyCredential, AzureSasCredential, AzureNamedKeyCredential
from azure.core.exceptions import ServiceRequestError
from azure.core.pipeline import Pipeline
from azure.core.pipeline.policies import BearerTokenCredentialPolicy, SansIOHTTPPolicy, AzureKeyCredentialPolicy, AzureSasCredentialPolicy
from azure.core.pipeline.policies import (
BearerTokenCredentialPolicy, SansIOHTTPPolicy, AzureKeyCredentialPolicy,
AzureSasCredentialPolicy, AzureNamedKeyCredentialPolicy
)
from azure.core.pipeline.transport import HttpRequest

import pytest
Expand Down Expand Up @@ -230,3 +233,41 @@ def test_azure_sas_credential_policy_raises():
sas = 1234
with pytest.raises(TypeError):
credential = AzureSasCredential(sas)

def test_azure_named_key_credential():
cred = AzureNamedKeyCredential("sample_name", "samplekey")

assert cred.name == "sample_name"
assert cred.key == "samplekey"

cred.update("newname", "newkey")
assert cred.name == "newname"
assert cred.key == "newkey"


def test_azure_named_key_credential_raises():
with pytest.raises(TypeError, match="Both name and key must be Strings."):
cred = AzureNamedKeyCredential("sample_name", 123345)

cred = AzureNamedKeyCredential("sample_name", "samplekey")
assert cred.name == "sample_name"
assert cred.key == "samplekey"

with pytest.raises(TypeError, match="Both name and key must be Strings."):
cred.update(1234, "newkey")

def test_azure_named_key_credential_policy():
"""Tests to see if we can create an AzureKeyCredentialPolicy"""

key_name = "api_key"
api_key = "test_key"

def verify_authorization_header(request):
assert request.headers[key_name] == api_key

transport=Mock(send=verify_authorization_header)
credential = AzureNamedKeyCredential(key_name, api_key)
credential_policy = AzureNamedKeyCredentialPolicy(credential=credential)
pipeline = Pipeline(transport=transport, policies=[credential_policy])

pipeline.run(HttpRequest("GET", "https://test_key_credential"))