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

Enable anonymous access for ACR #20919

Merged
merged 8 commits into from
May 10, 2021
Merged
Show file tree
Hide file tree
Changes from 5 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Threading;
using System.Threading.Tasks;
using Azure.Core;

namespace Azure.Containers.ContainerRegistry
{
/// <summary>
/// Used internally to indiate the client is intended for use with anonymous access authentication.
/// </summary>
internal class ContainerRegistryAnonymousAccessCredential : TokenCredential
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved
{
public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
{
throw new InvalidOperationException();
}

public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
{
throw new InvalidOperationException();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ internal class ContainerRegistryChallengeAuthenticationPolicy : BearerTokenAuthe
private readonly IContainerRegistryAuthenticationClient _authenticationClient;
private readonly ContainerRegistryRefreshTokenCache _refreshTokenCache;
private readonly string[] _aadScopes;
private readonly bool _anonymousAccess;

public ContainerRegistryChallengeAuthenticationPolicy(TokenCredential credential, string aadScope, IContainerRegistryAuthenticationClient authenticationClient)
: this(credential, aadScope, authenticationClient, null, null)
Expand All @@ -51,6 +52,7 @@ internal ContainerRegistryChallengeAuthenticationPolicy(TokenCredential credenti
_authenticationClient = authenticationClient;
_refreshTokenCache = new ContainerRegistryRefreshTokenCache(credential, authenticationClient, tokenRefreshOffset, tokenRefreshRetryDelay);
_aadScopes = new[] { aadScope };
_anonymousAccess = credential is ContainerRegistryAnonymousAccessCredential;
}

// Since we'll not cache the AAD access token or set an auth header on the initial request,
Expand Down Expand Up @@ -84,11 +86,19 @@ private async ValueTask<bool> AuthorizeRequestOnChallengeAsyncInternal(HttpMessa
var service = AuthorizationChallengeParser.GetChallengeParameterFromResponse(message.Response, "Bearer", "service");
var scope = AuthorizationChallengeParser.GetChallengeParameterFromResponse(message.Response, "Bearer", "scope");

// Step 3: Exchange AAD Access Token for ACR Refresh Token, or get the cached value instead.
string acrRefreshToken = await _refreshTokenCache.GetAcrRefreshTokenAsync(message, context, service, async).ConfigureAwait(false);
string acrAccessToken;
if (_anonymousAccess)
{
acrAccessToken = await GetAnonymousAcrAccessTokenAsync(service, scope, async, message.CancellationToken).ConfigureAwait(false);
}
else
{
// Step 3: Exchange AAD Access Token for ACR Refresh Token, or get the cached value instead.
string acrRefreshToken = await _refreshTokenCache.GetAcrRefreshTokenAsync(message, context, service, async).ConfigureAwait(false);

// Step 4: Send in acrRefreshToken and get back acrAccessToken
string acrAccessToken = await ExchangeAcrRefreshTokenForAcrAccessTokenAsync(acrRefreshToken, service, scope, async, message.CancellationToken).ConfigureAwait(false);
// Step 4: Send in acrRefreshToken and get back acrAccessToken
acrAccessToken = await ExchangeAcrRefreshTokenForAcrAccessTokenAsync(acrRefreshToken, service, scope, async, message.CancellationToken).ConfigureAwait(false);
}

// Step 5 - Authorize Request. Note, we don't use SetAuthorizationHeader from the base class here, because it
// sets an AAD access token header, and at this point we're done with AAD and using an ACR access token.
Expand All @@ -102,11 +112,26 @@ private async Task<string> ExchangeAcrRefreshTokenForAcrAccessTokenAsync(string
Response<AcrAccessToken> acrAccessToken = null;
if (async)
{
acrAccessToken = await _authenticationClient.ExchangeAcrRefreshTokenForAcrAccessTokenAsync(service, scope, acrRefreshToken, cancellationToken).ConfigureAwait(false);
acrAccessToken = await _authenticationClient.ExchangeAcrRefreshTokenForAcrAccessTokenAsync(service, scope, acrRefreshToken, cancellationToken: cancellationToken).ConfigureAwait(false);
}
else
{
acrAccessToken = _authenticationClient.ExchangeAcrRefreshTokenForAcrAccessToken(service, scope, acrRefreshToken, cancellationToken: cancellationToken);
}

return acrAccessToken.Value.AccessToken;
}

private async Task<string> GetAnonymousAcrAccessTokenAsync(string service, string scope, bool async, CancellationToken cancellationToken)
{
Response<AcrAccessToken> acrAccessToken = null;
if (async)
{
acrAccessToken = await _authenticationClient.ExchangeAcrRefreshTokenForAcrAccessTokenAsync(service, scope, acrRefreshToken: string.Empty, TokenGrantType.Password, cancellationToken ).ConfigureAwait(false);
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved
}
else
{
acrAccessToken = _authenticationClient.ExchangeAcrRefreshTokenForAcrAccessToken(service, scope, acrRefreshToken, cancellationToken);
acrAccessToken = _authenticationClient.ExchangeAcrRefreshTokenForAcrAccessToken(service, scope, acrRefreshToken: string.Empty, TokenGrantType.Password, cancellationToken);
}

return acrAccessToken.Value.AccessToken;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ internal interface IContainerRegistryAuthenticationClient
Task<Response<AcrRefreshToken>> ExchangeAadAccessTokenForAcrRefreshTokenAsync(string service, string aadAccessToken, CancellationToken token = default);
Response<AcrRefreshToken> ExchangeAadAccessTokenForAcrRefreshToken(string service, string aadAccessToken, CancellationToken token = default);

Task<Response<AcrAccessToken>> ExchangeAcrRefreshTokenForAcrAccessTokenAsync(string service, string scope, string acrRefreshToken, CancellationToken token = default);
Response<AcrAccessToken> ExchangeAcrRefreshTokenForAcrAccessToken(string service, string scope, string acrRefreshToken, CancellationToken token = default);
Task<Response<AcrAccessToken>> ExchangeAcrRefreshTokenForAcrAccessTokenAsync(string service, string scope, string acrRefreshToken, TokenGrantType grantType = TokenGrantType.RefreshToken, CancellationToken cancellationToken = default);
Response<AcrAccessToken> ExchangeAcrRefreshTokenForAcrAccessToken(string service, string scope, string acrRefreshToken, TokenGrantType grantType = TokenGrantType.RefreshToken, CancellationToken cancellationToken = default);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,31 @@ public partial class ContainerRegistryClient
private readonly AuthenticationRestClient _acrAuthClient;
private readonly string AcrAadScope = "https://management.core.windows.net/.default";

/// <summary>
/// Initializes a new instance of the <see cref="ContainerRegistryClient"/> for managing container images and artifacts,
/// using anonymous access to the registry. Only operations that support anonymous access are enabled. Other service
/// methods will throw <see cref="RequestFailedException"/> if called.
/// </summary>
/// <param name="registryUri">The URI endpoint of the container registry. This is likely to be similar
/// to "https://{registry-name}.azurecr.io".</param>
/// <exception cref="ArgumentNullException"> Thrown when the <paramref name="registryUri"/> is null. </exception>
public ContainerRegistryClient(Uri registryUri) : this(registryUri, new ContainerRegistryAnonymousAccessCredential(), new ContainerRegistryClientOptions())
{
}

/// <summary>
/// Initializes a new instance of the ContainerRegistryClient for managing container images and artifacts,
/// using anonymous access to the registry. Only operations that support anonymous access are enabled. Other service
/// methods will throw <see cref="RequestFailedException"/> if called.
/// </summary>
/// <param name="registryUri">The URI endpoint of the container registry. This is likely to be similar
/// to "https://{registry-name}.azurecr.io".</param>
/// <param name="options">Client configuration options for connecting to Azure Container Registry.</param>
/// <exception cref="ArgumentNullException"> Thrown when the <paramref name="registryUri"/> is null. </exception>
public ContainerRegistryClient(Uri registryUri, ContainerRegistryClientOptions options) : this(registryUri, new ContainerRegistryAnonymousAccessCredential(), options)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="ContainerRegistryClient"/> for managing container images and artifacts.
/// </summary>
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading