Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
d7bcfe6
changing ContainerRepositoryClient to ContainerRepository
seankane-msft Apr 27, 2021
7a0b598
renaming files
seankane-msft Apr 27, 2021
15bbaaf
re-recording, commenting out tests that are not necessary
seankane-msft Apr 27, 2021
b91302c
working sync registry artifact class
seankane-msft Apr 28, 2021
6f7c55c
async registry artifact
seankane-msft Apr 28, 2021
22ecb18
issue with recording infra that is removing an acr specific oauth path
seankane-msft Apr 28, 2021
d5986dd
pylint issues
seankane-msft Apr 28, 2021
1287136
recording and processors
seankane-msft Apr 28, 2021
145949b
removing commented out code
seankane-msft Apr 28, 2021
09f3636
undoing changes to cache
seankane-msft Apr 28, 2021
6e38bf1
more lint fixes
seankane-msft Apr 28, 2021
d9659c0
help with logging output
seankane-msft Apr 28, 2021
941473a
change to list_repository_names
seankane-msft Apr 28, 2021
44ceace
renaming for consistency
seankane-msft Apr 28, 2021
4e5dbc8
all changes made, plus recordings
seankane-msft Apr 28, 2021
f7caebd
fixing up more tests again!
seankane-msft Apr 28, 2021
6ced986
formatting
seankane-msft Apr 28, 2021
5417f1d
merge conflicts
seankane-msft Apr 29, 2021
83c0d90
fixing up merge issues
seankane-msft May 3, 2021
c0ed9ff
merge conflicts
seankane-msft May 3, 2021
ed3f937
more conflicts
seankane-msft May 3, 2021
ccec23b
undoing changes to gen code
seankane-msft May 3, 2021
afa0c1b
pylint issues
seankane-msft May 4, 2021
0e2e335
consistent naming
seankane-msft May 4, 2021
24cb3a0
changes
seankane-msft May 4, 2021
0dcd6cd
anon test
seankane-msft May 4, 2021
62964e8
small changes to generated, eventually will be reflected in the swagger
seankane-msft May 5, 2021
64937b8
adding basics for anon
seankane-msft May 5, 2021
315c9f8
adding test infra
seankane-msft May 5, 2021
abd4e4e
adding async tests
seankane-msft May 5, 2021
119575e
adding more tests for anon container repo and reg artifact
seankane-msft May 5, 2021
7c31693
added async anon client
seankane-msft May 6, 2021
0c9e77a
asserting credential is false
seankane-msft May 6, 2021
7f57d03
fixing scrubber
seankane-msft May 6, 2021
d260ff9
new swagger
seankane-msft May 6, 2021
8fbf308
merge conflicts
seankane-msft May 6, 2021
dc033e9
merge conflicts reflected in tests
seankane-msft May 6, 2021
9a93a9c
lint
seankane-msft May 6, 2021
c06fecd
updating tests and resource for anonymous access
seankane-msft May 6, 2021
2b78a40
updating generated code
seankane-msft May 7, 2021
77aed4b
merge conflicts
seankane-msft May 9, 2021
67887d6
undoing generated code changes
seankane-msft May 9, 2021
4e7c9b6
shouldnt have done that oops
seankane-msft May 9, 2021
86f86aa
undoing unnecessary changes to recordings
seankane-msft May 9, 2021
87b1928
changelog
seankane-msft May 9, 2021
3255a2d
merge conflicts
seankane-msft May 11, 2021
68f7bfd
anna and mccoys comments
seankane-msft May 11, 2021
8a254ab
lint fixes
seankane-msft May 11, 2021
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/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://"):
Copy link
Member

Choose a reason for hiding this comment

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

This feels like it could be veering into client-side validation territory, but if there are other clients that do this then it's probably okay for now

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I believe we do this in all four tier-1 languages, and the biggest reason being the value given in the portal when you create an account does not prefix the endpoint with https:// or 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:
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't this check move before line 28? Otherwise we are building the ACRExchangeClient for nothing?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes I can move it, I originally didn't like the if/else flow but I don't see a way around it with this design.

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
Copy link
Member

@mccoyp mccoyp May 11, 2021

Choose a reason for hiding this comment

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

nit: it looks like Dict and Any can just be imported if type checking


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://"):
Copy link
Member

Choose a reason for hiding this comment

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

Same comment about parameter validation, but I realize that this may be kind of ACR-specific

endpoint = "https://" + endpoint
self._endpoint = endpoint
self._credential_scope = "https://management.core.windows.net/.default"
Copy link
Member

Choose a reason for hiding this comment

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

This could probably be a constant somewhere to be shared between the ExchangeClients

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The credential scope needs to be fixed in the next release to handle foreign clouds as well. I'm going to address this in our next beta.

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