Skip to content

Commit d02dc4a

Browse files
committed
Merge branch 'master' of https://github.com/PowerShell/PSResourceGet into bugfix-mar-az-deps
2 parents b7d4a0b + 9c741e4 commit d02dc4a

File tree

2 files changed

+110
-8
lines changed

2 files changed

+110
-8
lines changed

src/code/ContainerRegistryServerAPICalls.cs

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,15 @@ internal class ContainerRegistryServerAPICalls : ServerApiCall
3838
private static readonly FindResults emptyResponseResults = new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: containerRegistryFindResponseType);
3939

4040
const string containerRegistryRefreshTokenTemplate = "grant_type=access_token&service={0}&tenant={1}&access_token={2}"; // 0 - registry, 1 - tenant, 2 - access token
41-
const string containerRegistryAccessTokenTemplate = "grant_type=refresh_token&service={0}&scope=repository:*:*&refresh_token={1}"; // 0 - registry, 1 - refresh token
41+
const string containerRegistryAccessTokenTemplate = "grant_type=refresh_token&service={0}&scope=repository:*:*&scope=registry:catalog:*&refresh_token={1}"; // 0 - registry, 1 - refresh token
4242
const string containerRegistryOAuthExchangeUrlTemplate = "https://{0}/oauth2/exchange"; // 0 - registry
4343
const string containerRegistryOAuthTokenUrlTemplate = "https://{0}/oauth2/token"; // 0 - registry
4444
const string containerRegistryManifestUrlTemplate = "https://{0}/v2/{1}/manifests/{2}"; // 0 - registry, 1 - repo(modulename), 2 - tag(version)
4545
const string containerRegistryBlobDownloadUrlTemplate = "https://{0}/v2/{1}/blobs/{2}"; // 0 - registry, 1 - repo(modulename), 2 - layer digest
4646
const string containerRegistryFindImageVersionUrlTemplate = "https://{0}/v2/{1}/tags/list"; // 0 - registry, 1 - repo(modulename)
4747
const string containerRegistryStartUploadTemplate = "https://{0}/v2/{1}/blobs/uploads/"; // 0 - registry, 1 - packagename
4848
const string containerRegistryEndUploadTemplate = "https://{0}{1}&digest=sha256:{2}"; // 0 - registry, 1 - location, 2 - digest
49+
const string defaultScope = "&scope=repository:*:*&scope=registry:catalog:*";
4950
const string containerRegistryRepositoryListTemplate = "https://{0}/v2/_catalog"; // 0 - registry
5051

5152
#endregion
@@ -392,12 +393,18 @@ internal string GetContainerRegistryAccessToken(out ErrorRecord errRecord)
392393
}
393394
else
394395
{
395-
bool isRepositoryUnauthenticated = IsContainerRegistryUnauthenticated(Repository.Uri.ToString(), out errRecord);
396+
bool isRepositoryUnauthenticated = IsContainerRegistryUnauthenticated(Repository.Uri.ToString(), out errRecord, out accessToken);
396397
if (errRecord != null)
397398
{
398399
return null;
399400
}
400401

402+
if (!string.IsNullOrEmpty(accessToken))
403+
{
404+
_cmdletPassedIn.WriteVerbose("Anonymous access token retrieved.");
405+
return accessToken;
406+
}
407+
401408
if (!isRepositoryUnauthenticated)
402409
{
403410
accessToken = Utils.GetAzAccessToken();
@@ -437,15 +444,82 @@ internal string GetContainerRegistryAccessToken(out ErrorRecord errRecord)
437444
/// <summary>
438445
/// Checks if container registry repository is unauthenticated.
439446
/// </summary>
440-
internal bool IsContainerRegistryUnauthenticated(string containerRegistyUrl, out ErrorRecord errRecord)
447+
internal bool IsContainerRegistryUnauthenticated(string containerRegistyUrl, out ErrorRecord errRecord, out string anonymousAccessToken)
441448
{
442449
_cmdletPassedIn.WriteDebug("In ContainerRegistryServerAPICalls::IsContainerRegistryUnauthenticated()");
443450
errRecord = null;
451+
anonymousAccessToken = string.Empty;
444452
string endpoint = $"{containerRegistyUrl}/v2/";
445453
HttpResponseMessage response;
446454
try
447455
{
448456
response = _sessionClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, endpoint)).Result;
457+
458+
if (response.StatusCode == HttpStatusCode.Unauthorized)
459+
{
460+
// check if there is a auth challenge header
461+
if (response.Headers.WwwAuthenticate.Count() > 0)
462+
{
463+
var authHeader = response.Headers.WwwAuthenticate.First();
464+
if (authHeader.Scheme == "Bearer")
465+
{
466+
// check if there is a realm
467+
if (authHeader.Parameter.Contains("realm"))
468+
{
469+
// get the realm
470+
var realm = authHeader.Parameter.Split(',')?.Where(x => x.Contains("realm"))?.FirstOrDefault()?.Split('=')[1]?.Trim('"');
471+
// get the service
472+
var service = authHeader.Parameter.Split(',')?.Where(x => x.Contains("service"))?.FirstOrDefault()?.Split('=')[1]?.Trim('"');
473+
474+
if (string.IsNullOrEmpty(realm) || string.IsNullOrEmpty(service))
475+
{
476+
errRecord = new ErrorRecord(
477+
new InvalidOperationException("Failed to get realm or service from the auth challenge header."),
478+
"RegistryUnauthenticationCheckError",
479+
ErrorCategory.InvalidResult,
480+
this);
481+
482+
return false;
483+
}
484+
485+
string content = "grant_type=access_token&service=" + service + defaultScope;
486+
var contentHeaders = new Collection<KeyValuePair<string, string>> { new KeyValuePair<string, string>("Content-Type", "application/x-www-form-urlencoded") };
487+
488+
// get the anonymous access token
489+
var url = $"{realm}?service={service}{defaultScope}";
490+
491+
// we dont check the errorrecord here because we want to return false if we get a 401 and not throw an error
492+
var results = GetHttpResponseJObjectUsingContentHeaders(url, HttpMethod.Get, content, contentHeaders, out _);
493+
494+
if (results == null)
495+
{
496+
_cmdletPassedIn.WriteDebug("Failed to get access token from the realm. results is null.");
497+
return false;
498+
}
499+
500+
if (results["access_token"] == null)
501+
{
502+
_cmdletPassedIn.WriteDebug($"Failed to get access token from the realm. access_token is null. results: {results}");
503+
return false;
504+
}
505+
506+
anonymousAccessToken = results["access_token"].ToString();
507+
_cmdletPassedIn.WriteDebug("Anonymous access token retrieved");
508+
return true;
509+
}
510+
}
511+
}
512+
}
513+
}
514+
catch (HttpRequestException hre)
515+
{
516+
errRecord = new ErrorRecord(
517+
hre,
518+
"RegistryAnonymousAcquireError",
519+
ErrorCategory.ConnectionError,
520+
this);
521+
522+
return false;
449523
}
450524
catch (Exception e)
451525
{
@@ -1756,7 +1830,7 @@ private FindResults FindPackages(string packageName, bool includePrerelease, out
17561830
}
17571831

17581832
// This remove the 'psresource/' prefix from the repository name for comparison with wildcard.
1759-
string moduleName = repositoryName.Substring(11);
1833+
string moduleName = repositoryName.StartsWith("psresource/") ? repositoryName.Substring(11) : repositoryName;
17601834

17611835
WildcardPattern wildcardPattern = new WildcardPattern(packageName, WildcardOptions.IgnoreCase);
17621836

test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,12 +151,11 @@ Describe 'Test HTTP Find-PSResource for ACR Server Protocol' -tags 'CI' {
151151
$err[0].FullyQualifiedErrorId | Should -BeExactly "FindCommandOrDscResourceFailure,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource"
152152
}
153153

154-
It "Should not find all resources given Name '*'" {
154+
It "Should find all resources given Name '*'" {
155155
# FindAll()
156156
$res = Find-PSResource -Name "*" -Repository $ACRRepoName -ErrorVariable err -ErrorAction SilentlyContinue
157-
$res | Should -BeNullOrEmpty
158-
$err.Count | Should -BeGreaterThan 0
159-
$err[0].FullyQualifiedErrorId | Should -BeExactly "FindAllFailure,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource"
157+
$res | Should -Not -BeNullOrEmpty
158+
$res.Count | Should -BeGreaterThan 0
160159
}
161160

162161
It "Should find script given Name" {
@@ -273,3 +272,32 @@ Describe 'Test Find-PSResource for MAR Repository' -tags 'CI' {
273272
$res.Count | Should -BeGreaterThan 1
274273
}
275274
}
275+
276+
# Skip this test fo
277+
Describe 'Test Find-PSResource for unauthenticated ACR repository' -tags 'CI' {
278+
BeforeAll {
279+
$skipOnWinPS = $PSVersionTable.PSVersion.Major -eq 5
280+
281+
if (-not $skipOnWinPS) {
282+
Register-PSResourceRepository -Name "Unauthenticated" -Uri "https://psresourcegetnoauth.azurecr.io/" -ApiVersion "ContainerRegistry"
283+
}
284+
}
285+
286+
AfterAll {
287+
if (-not $skipOnWinPS) {
288+
Unregister-PSResourceRepository -Name "Unauthenticated"
289+
}
290+
}
291+
292+
It "Should find resource given specific Name, Version null" {
293+
294+
if ($skipOnWinPS) {
295+
Set-ItResult -Pending -Because "Skipping test on Windows PowerShell"
296+
return
297+
}
298+
299+
$res = Find-PSResource -Name "hello-world" -Repository "Unauthenticated"
300+
$res.Name | Should -Be "hello-world"
301+
$res.Version | Should -Be "5.0.0"
302+
}
303+
}

0 commit comments

Comments
 (0)