Skip to content

Commit

Permalink
[Container Registry] Anonymous Access Client (#18550)
Browse files Browse the repository at this point in the history
* changing ContainerRepositoryClient to ContainerRepository

* renaming files

* re-recording, commenting out tests that are not necessary

* working sync registry artifact class

* async registry artifact

* issue with recording infra that is removing an acr specific oauth path

* pylint issues

* recording and processors

* removing commented out code

* undoing changes to cache

* more lint fixes

* help with logging output

* change to list_repository_names

* renaming for consistency

* all changes made, plus recordings

* fixing up more tests again!

* formatting

* fixing up merge issues

* undoing changes to gen code

* pylint issues

* consistent naming

* changes

* anon test

* small changes to generated, eventually will be reflected in the swagger

* adding basics for anon

* adding test infra

* adding async tests

* adding more tests for anon container repo and reg artifact

* added async anon client

* asserting credential is false

* fixing scrubber

* new swagger

* merge conflicts reflected in tests

* lint

* updating tests and resource for anonymous access

* updating generated code

* undoing generated code changes

* shouldnt have done that oops

* undoing unnecessary changes to recordings

* changelog

* anna and mccoys comments

* lint fixes
  • Loading branch information
seankane-msft authored May 11, 2021
1 parent 2420e32 commit a4545c1
Show file tree
Hide file tree
Showing 34 changed files with 2,369 additions and 105 deletions.
1 change: 1 addition & 0 deletions sdk/containerregistry/azure-containerregistry/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* Rename `TagProperties` to `ArtifactTagProperties`
* Rename `ContentPermissions` to `ContentProperties`
* Rename `content_permissions` attributes on `TagProperties`, `RepositoryProperties`, and `RegistryArtifactProperties` to `writeable_properties`.
* Adds anonymous access capabilities to client by passing in `None` to credential.

## 1.0.0b1 (2021-04-06)
* First release of the Azure Container Registry library for Python
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# coding=utf-8
# ------------------------------------
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# ------------------------------------
from typing import TYPE_CHECKING, Dict, Any

from ._exchange_client import ExchangeClientAuthenticationPolicy
from ._generated import ContainerRegistry
from ._generated.models._container_registry_enums import TokenGrantType
from ._helpers import _parse_challenge
from ._user_agent import USER_AGENT

if TYPE_CHECKING:
from azure.core.credentials import TokenCredential


class AnonymousACRExchangeClient(object):
"""Class for handling oauth authentication requests
:param endpoint: Azure Container Registry endpoint
:type endpoint: str
:param credential: Credential which provides tokens to authenticate requests
:type credential: :class:`~azure.core.credentials.TokenCredential`
"""

def __init__(self, endpoint, **kwargs): # pylint: disable=missing-client-constructor-parameter-credential
# type: (str, Dict[str, Any]) -> None
if not endpoint.startswith("https://") and not endpoint.startswith("http://"):
endpoint = "https://" + endpoint
self._endpoint = endpoint
self.credential_scope = "https://management.core.windows.net/.default"
self._client = ContainerRegistry(
credential=None,
url=endpoint,
sdk_moniker=USER_AGENT,
authentication_policy=ExchangeClientAuthenticationPolicy(),
credential_scopes=kwargs.pop("credential_scopes", self.credential_scope),
**kwargs
)

def get_acr_access_token(self, challenge, **kwargs):
# type: (str, Dict[str, Any]) -> str
parsed_challenge = _parse_challenge(challenge)
parsed_challenge["grant_type"] = TokenGrantType.PASSWORD
return self.exchange_refresh_token_for_access_token(
None,
service=parsed_challenge["service"],
scope=parsed_challenge["scope"],
grant_type=TokenGrantType.PASSWORD,
**kwargs
)

def exchange_refresh_token_for_access_token(
self, refresh_token=None, service=None, scope=None, grant_type=TokenGrantType.PASSWORD, **kwargs
):
# type: (str, str, str, str, Dict[str, Any]) -> str
access_token = self._client.authentication.exchange_acr_refresh_token_for_acr_access_token(
service=service, scope=scope, refresh_token=refresh_token, grant_type=grant_type, **kwargs
)
return access_token.access_token

def __enter__(self):
self._client.__enter__()
return self

def __exit__(self, *args):
self._client.__exit__(*args)

def close(self):
# type: () -> None
"""Close sockets opened by the client.
Calling this method is unnecessary when using the client as a context manager.
"""
self._client.close()
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from azure.core.pipeline.policies import HTTPPolicy

from ._anonymous_exchange_client import AnonymousACRExchangeClient
from ._exchange_client import ACRExchangeClient
from ._helpers import _enforce_https

Expand All @@ -24,7 +25,10 @@ def __init__(self, credential, endpoint):
# type: (TokenCredential, str) -> None
super(ContainerRegistryChallengePolicy, self).__init__()
self._credential = credential
self._exchange_client = ACRExchangeClient(endpoint, self._credential)
if self._credential is None:
self._exchange_client = AnonymousACRExchangeClient(endpoint)
else:
self._exchange_client = ACRExchangeClient(endpoint, self._credential)

def on_request(self, request):
# type: (PipelineRequest) -> None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# Licensed under the MIT License.
# ------------------------------------
from enum import Enum
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Dict, Any

from azure.core.pipeline.transport import HttpTransport

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# ------------------------------------
import re
import time
from typing import TYPE_CHECKING

Expand Down Expand Up @@ -40,9 +39,6 @@ class ACRExchangeClient(object):
:type credential: :class:`~azure.core.credentials.TokenCredential`
"""

BEARER = "Bearer"
AUTHENTICATION_CHALLENGE_PARAMS_PATTERN = re.compile('(?:(\\w+)="([^""]*)")+')

def __init__(self, endpoint, credential, **kwargs):
# type: (str, TokenCredential, Dict[str, Any]) -> None
if not endpoint.startswith("https://") and not endpoint.startswith("http://"):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ async def exchange_acr_refresh_token_for_acr_access_token(
service: str,
scope: str,
refresh_token: str,
grant_type: Union[str, "_models.Enum2"] = "refresh_token",
grant_type: Union[str, "_models.TokenGrantType"] = "refresh_token",
**kwargs
) -> "_models.AcrAccessToken":
"""Exchange ACR Refresh token for an ACR Access Token.
Expand All @@ -121,7 +121,7 @@ async def exchange_acr_refresh_token_for_acr_access_token(
:param refresh_token: Must be a valid ACR refresh token.
:type refresh_token: str
:param grant_type: Grant type is expected to be refresh_token.
:type grant_type: str or ~container_registry.models.Enum2
:type grant_type: str or ~container_registry.models.TokenGrantType
:keyword callable cls: A custom type or function that will be passed the direct response
:return: AcrAccessToken, or the result of cls(response)
:rtype: ~container_registry.models.AcrAccessToken
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,9 @@
from ._container_registry_enums import (
ArtifactArchitecture,
ArtifactOperatingSystem,
Enum2,
ManifestOrderBy,
TagOrderBy,
TokenGrantType,
)

__all__ = [
Expand Down Expand Up @@ -129,7 +129,7 @@
'V2Manifest',
'ArtifactArchitecture',
'ArtifactOperatingSystem',
'Enum2',
'ManifestOrderBy',
'TagOrderBy',
'TokenGrantType',
]
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,6 @@ class ArtifactOperatingSystem(with_metaclass(_CaseInsensitiveEnumMeta, str, Enum
SOLARIS = "solaris"
WINDOWS = "windows"

class Enum2(with_metaclass(_CaseInsensitiveEnumMeta, str, Enum)):
"""Grant type is expected to be refresh_token
"""

REFRESH_TOKEN = "refresh_token"
PASSWORD = "password"

class ManifestOrderBy(with_metaclass(_CaseInsensitiveEnumMeta, str, Enum)):
"""Sort options for ordering manifests in a collection.
"""
Expand All @@ -83,3 +76,10 @@ class TagOrderBy(with_metaclass(_CaseInsensitiveEnumMeta, str, Enum)):
LAST_UPDATED_ON_DESCENDING = "timedesc"
#: Order tags by LastUpdatedOn field, from least recently updated to most recently updated.
LAST_UPDATED_ON_ASCENDING = "timeasc"

class TokenGrantType(with_metaclass(_CaseInsensitiveEnumMeta, str, Enum)):
"""Grant type is expected to be refresh_token
"""

REFRESH_TOKEN = "refresh_token"
PASSWORD = "password"
Original file line number Diff line number Diff line change
Expand Up @@ -953,7 +953,7 @@ class PathsV3R3RxOauth2TokenPostRequestbodyContentApplicationXWwwFormUrlencodedS
:param grant_type: Required. Grant type is expected to be refresh_token. Possible values
include: "refresh_token", "password".
:type grant_type: str or ~container_registry.models.Enum2
:type grant_type: str or ~container_registry.models.TokenGrantType
:param service: Required. Indicates the name of your Azure container registry.
:type service: str
:param scope: Required. Which is expected to be a valid scope, and can be specified more than
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1060,7 +1060,7 @@ class PathsV3R3RxOauth2TokenPostRequestbodyContentApplicationXWwwFormUrlencodedS
:param grant_type: Required. Grant type is expected to be refresh_token. Possible values
include: "refresh_token", "password".
:type grant_type: str or ~container_registry.models.Enum2
:type grant_type: str or ~container_registry.models.TokenGrantType
:param service: Required. Indicates the name of your Azure container registry.
:type service: str
:param scope: Required. Which is expected to be a valid scope, and can be specified more than
Expand Down Expand Up @@ -1088,7 +1088,7 @@ class PathsV3R3RxOauth2TokenPostRequestbodyContentApplicationXWwwFormUrlencodedS
def __init__(
self,
*,
grant_type: Union[str, "Enum2"] = "refresh_token",
grant_type: Union[str, "TokenGrantType"] = "refresh_token",
service: str,
scope: str,
acr_refresh_token: str,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def exchange_acr_refresh_token_for_acr_access_token(
service, # type: str
scope, # type: str
refresh_token, # type: str
grant_type="refresh_token", # type: Union[str, "_models.Enum2"]
grant_type="refresh_token", # type: Union[str, "_models.TokenGrantType"]
**kwargs # type: Any
):
# type: (...) -> "_models.AcrAccessToken"
Expand All @@ -127,7 +127,7 @@ def exchange_acr_refresh_token_for_acr_access_token(
:param refresh_token: Must be a valid ACR refresh token.
:type refresh_token: str
:param grant_type: Grant type is expected to be refresh_token.
:type grant_type: str or ~container_registry.models.Enum2
:type grant_type: str or ~container_registry.models.TokenGrantType
:keyword callable cls: A custom type or function that will be passed the direct response
:return: AcrAccessToken, or the result of cls(response)
:rtype: ~container_registry.models.AcrAccessToken
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# coding=utf-8
# ------------------------------------
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# ------------------------------------
from typing import TYPE_CHECKING, Dict, List, Any

from ._async_exchange_client import ExchangeClientAuthenticationPolicy
from .._generated.aio import ContainerRegistry
from .._generated.models._container_registry_enums import TokenGrantType
from .._helpers import _parse_challenge
from .._user_agent import USER_AGENT

if TYPE_CHECKING:
from azure.core.credentials_async import AsyncTokenCredential


class AnonymousACRExchangeClient(object):
"""Class for handling oauth authentication requests
:param endpoint: Azure Container Registry endpoint
:type endpoint: str
"""

def __init__(self, endpoint: str, **kwargs: Dict[str, Any]) -> None: # pylint: disable=missing-client-constructor-parameter-credential
if not endpoint.startswith("https://") and not endpoint.startswith("http://"):
endpoint = "https://" + endpoint
self._endpoint = endpoint
self._credential_scope = "https://management.core.windows.net/.default"
self._client = ContainerRegistry(
credential=None,
url=endpoint,
sdk_moniker=USER_AGENT,
authentication_policy=ExchangeClientAuthenticationPolicy(),
credential_scopes=kwargs.pop("credential_scopes", self._credential_scope),
**kwargs
)

async def get_acr_access_token(self, challenge: str, **kwargs: Dict[str, Any]) -> str:
parsed_challenge = _parse_challenge(challenge)
parsed_challenge["grant_type"] = TokenGrantType.PASSWORD
return await self.exchange_refresh_token_for_access_token(
None,
service=parsed_challenge["service"],
scope=parsed_challenge["scope"],
grant_type=TokenGrantType.PASSWORD,
**kwargs
)

async def exchange_refresh_token_for_access_token(
self,
refresh_token: str = None,
service: str = None,
scope: str = None,
grant_type: str = TokenGrantType.PASSWORD,
**kwargs: Any
) -> str:
access_token = await self._client.authentication.exchange_acr_refresh_token_for_acr_access_token(
service=service, scope=scope, refresh_token=refresh_token, grant_type=grant_type, **kwargs
)
return access_token.access_token

async def __aenter__(self):
self._client.__aenter__()
return self

async def __aexit__(self, *args):
self._client.__aexit__(*args)

async def close(self) -> None:
"""Close sockets opened by the client.
Calling this method is unnecessary when using the client as a context manager.
"""
await self._client.close()
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from azure.core.pipeline.policies import AsyncHTTPPolicy

from ._async_anonymous_exchange_client import AnonymousACRExchangeClient
from ._async_exchange_client import ACRExchangeClient
from .._helpers import _enforce_https

Expand All @@ -21,7 +22,10 @@ class ContainerRegistryChallengePolicy(AsyncHTTPPolicy):
def __init__(self, credential: "AsyncTokenCredential", endpoint: str) -> None:
super().__init__()
self._credential = credential
self._exchange_client = ACRExchangeClient(endpoint, self._credential)
if self._credential is None:
self._exchange_client = AnonymousACRExchangeClient(endpoint)
else:
self._exchange_client = ACRExchangeClient(endpoint, self._credential)

async def on_request(self, request):
# type: (PipelineRequest) -> None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# ------------------------------------
import re
import time
from typing import TYPE_CHECKING, Dict, List, Any

Expand Down Expand Up @@ -37,9 +36,6 @@ class ACRExchangeClient(object):
:type credential: :class:`azure.core.credentials.TokenCredential`
"""

BEARER = "Bearer"
AUTHENTICATION_CHALLENGE_PARAMS_PATTERN = re.compile('(?:(\\w+)="([^""]*)")+')

def __init__(self, endpoint: str, credential: "AsyncTokencredential", **kwargs: Dict[str, Any]) -> None:
if not endpoint.startswith("https://") and not endpoint.startswith("http://"):
endpoint = "https://" + endpoint
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,8 @@ async def get_tag_properties(self, tag: str, **kwargs: Dict[str, Any]) -> Artifa
tag_properties = await client.get_tag_properties(tag.name)
"""
return ArtifactTagProperties._from_generated( # pylint: disable=protected-access
await self._client.container_registry.get_tag_properties(self.repository, tag, **kwargs)
await self._client.container_registry.get_tag_properties(self.repository, tag, **kwargs),
repository=self.repository,
)

@distributed_trace
Expand Down
Loading

0 comments on commit a4545c1

Please sign in to comment.