From d0d345d03006138be67f92ffefcfe77ab51094f5 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Thu, 6 Feb 2025 15:01:53 -0800 Subject: [PATCH 1/9] Use authentication challenge for unauthenticated feed Fix NRE when getting scope Fix call to realm Fix scope Add content to the webrequest Remove extra debug Fix error message --- src/code/ContainerRegistryServerAPICalls.cs | 83 ++++++++++++++++++++- 1 file changed, 81 insertions(+), 2 deletions(-) diff --git a/src/code/ContainerRegistryServerAPICalls.cs b/src/code/ContainerRegistryServerAPICalls.cs index d9175c0b0..807e8af37 100644 --- a/src/code/ContainerRegistryServerAPICalls.cs +++ b/src/code/ContainerRegistryServerAPICalls.cs @@ -20,6 +20,7 @@ using System.Text; using System.Security.Cryptography; using System.Text.Json; +using ResourceType = Microsoft.PowerShell.PSResourceGet.UtilClasses.ResourceType; namespace Microsoft.PowerShell.PSResourceGet { @@ -46,6 +47,7 @@ internal class ContainerRegistryServerAPICalls : ServerApiCall const string containerRegistryFindImageVersionUrlTemplate = "https://{0}/v2/{1}/tags/list"; // 0 - registry, 1 - repo(modulename) const string containerRegistryStartUploadTemplate = "https://{0}/v2/{1}/blobs/uploads/"; // 0 - registry, 1 - packagename const string containerRegistryEndUploadTemplate = "https://{0}{1}&digest=sha256:{2}"; // 0 - registry, 1 - location, 2 - digest + const string defaultScope = "repository:*:*"; #endregion @@ -391,12 +393,18 @@ internal string GetContainerRegistryAccessToken(out ErrorRecord errRecord) } else { - bool isRepositoryUnauthenticated = IsContainerRegistryUnauthenticated(Repository.Uri.ToString(), out errRecord); + bool isRepositoryUnauthenticated = IsContainerRegistryUnauthenticated(Repository.Uri.ToString(), out errRecord, out accessToken); if (errRecord != null) { return null; } + if (!string.IsNullOrEmpty(accessToken)) + { + _cmdletPassedIn.WriteVerbose("Anonymous access token retrieved."); + return accessToken; + } + if (!isRepositoryUnauthenticated) { accessToken = Utils.GetAzAccessToken(); @@ -436,15 +444,86 @@ internal string GetContainerRegistryAccessToken(out ErrorRecord errRecord) /// /// Checks if container registry repository is unauthenticated. /// - internal bool IsContainerRegistryUnauthenticated(string containerRegistyUrl, out ErrorRecord errRecord) + internal bool IsContainerRegistryUnauthenticated(string containerRegistyUrl, out ErrorRecord errRecord, out string anonymousAccessToken) { _cmdletPassedIn.WriteDebug("In ContainerRegistryServerAPICalls::IsContainerRegistryUnauthenticated()"); errRecord = null; + anonymousAccessToken = string.Empty; string endpoint = $"{containerRegistyUrl}/v2/"; HttpResponseMessage response; try { response = _sessionClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, endpoint)).Result; + + if (response.StatusCode == HttpStatusCode.Unauthorized) + { + // check if there is a auth challenge header + if (response.Headers.WwwAuthenticate.Count() > 0) + { + var authHeader = response.Headers.WwwAuthenticate.First(); + if (authHeader.Scheme == "Bearer") + { + // check if there is a realm + if (authHeader.Parameter.Contains("realm")) + { + // get the realm + var realm = authHeader.Parameter.Split(',')?.Where(x => x.Contains("realm"))?.FirstOrDefault()?.Split('=')[1]?.Trim('"'); + // get the service + var service = authHeader.Parameter.Split(',')?.Where(x => x.Contains("service"))?.FirstOrDefault()?.Split('=')[1]?.Trim('"'); + + if (string.IsNullOrEmpty(realm) || string.IsNullOrEmpty(service)) + { + errRecord = new ErrorRecord( + new InvalidOperationException("Failed to get realm or service from the auth challenge header."), + "RegistryUnauthenticationCheckError", + ErrorCategory.InvalidResult, + this); + + return false; + } + + string content = "grant_type=access_token&service=" + service + "&scope=" + defaultScope; + var contentHeaders = new Collection> { new KeyValuePair("Content-Type", "application/x-www-form-urlencoded") }; + + // get the anonymous access token + var url = $"{realm}?service={service}&scope={defaultScope}"; + var results = GetHttpResponseJObjectUsingContentHeaders(url, HttpMethod.Get, content, contentHeaders, out errRecord); + + if (errRecord != null) + { + _cmdletPassedIn.WriteDebug($"Failed to get access token from the realm. Error: {errRecord}"); + return false; + } + + if (results == null) + { + _cmdletPassedIn.WriteDebug("Failed to get access token from the realm. results is null."); + return false; + } + + if (results["access_token"] == null) + { + _cmdletPassedIn.WriteDebug($"Failed to get access token from the realm. access_token is null. results: {results}"); + return false; + } + + anonymousAccessToken = results["access_token"].ToString(); + _cmdletPassedIn.WriteDebug("Anonymous access token retrieved"); + return true; + } + } + } + } + } + catch (HttpRequestException hre) + { + errRecord = new ErrorRecord( + hre, + "RegistryAnonymousAcquireError", + ErrorCategory.ConnectionError, + this); + + return false; } catch (Exception e) { From 35769982021dbda38e10a012a745961910c4bb95 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Wed, 19 Feb 2025 11:35:53 -0800 Subject: [PATCH 2/9] Add test to find from unauthenticated ACR --- ...ndPSResourceContainerRegistryServer.Tests.ps1 | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 index b7ffdfb8e..fb860e9e4 100644 --- a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 @@ -256,3 +256,19 @@ Describe 'Test Find-PSResource for MAR Repository' -tags 'CI' { $res.Dependencies[0].Name | Should -Be "Az.Accounts" } } + +Describe 'Test Find-PSResource for unauthenticated ACR repository' -tags 'CI' { + BeforeAll { + Register-PSResourceRepository -Name "Unauthenticated" -Uri "https://psresourcegetnoauth.azurecr.io/" -ApiVersion "ContainerRegistry" + } + + AfterAll { + Unregister-PSResourceRepository -Name "Unauthenticated" + } + + It "Should find resource given specific Name, Version null" { + $res = Find-PSResource -Name "hello-world" -Repository "Unauthenticated" + $res.Name | Should -Be "hello-world" + $res.Version | Should -Be "5.0.0" + } +} From 415c9f92724f94630f5cfc79af37c88500d242af Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Wed, 19 Feb 2025 11:39:01 -0800 Subject: [PATCH 3/9] Remove unused using --- src/code/ContainerRegistryServerAPICalls.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/code/ContainerRegistryServerAPICalls.cs b/src/code/ContainerRegistryServerAPICalls.cs index 807e8af37..81bd3c15f 100644 --- a/src/code/ContainerRegistryServerAPICalls.cs +++ b/src/code/ContainerRegistryServerAPICalls.cs @@ -20,7 +20,6 @@ using System.Text; using System.Security.Cryptography; using System.Text.Json; -using ResourceType = Microsoft.PowerShell.PSResourceGet.UtilClasses.ResourceType; namespace Microsoft.PowerShell.PSResourceGet { From 8e4ac8c059047d534b62318586a2af7798d8514e Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Wed, 19 Feb 2025 17:29:20 -0800 Subject: [PATCH 4/9] Bug fixes --- src/code/ContainerRegistryServerAPICalls.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/code/ContainerRegistryServerAPICalls.cs b/src/code/ContainerRegistryServerAPICalls.cs index c3dafb4fb..c310f149b 100644 --- a/src/code/ContainerRegistryServerAPICalls.cs +++ b/src/code/ContainerRegistryServerAPICalls.cs @@ -38,7 +38,7 @@ internal class ContainerRegistryServerAPICalls : ServerApiCall private static readonly FindResults emptyResponseResults = new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: containerRegistryFindResponseType); const string containerRegistryRefreshTokenTemplate = "grant_type=access_token&service={0}&tenant={1}&access_token={2}"; // 0 - registry, 1 - tenant, 2 - access token - const string containerRegistryAccessTokenTemplate = "grant_type=refresh_token&service={0}&scope=repository:*:*&refresh_token={1}"; // 0 - registry, 1 - refresh token + const string containerRegistryAccessTokenTemplate = "grant_type=refresh_token&service={0}&scope=repository:*:*&scope=registry:catalog:*&refresh_token={1}"; // 0 - registry, 1 - refresh token const string containerRegistryOAuthExchangeUrlTemplate = "https://{0}/oauth2/exchange"; // 0 - registry const string containerRegistryOAuthTokenUrlTemplate = "https://{0}/oauth2/token"; // 0 - registry const string containerRegistryManifestUrlTemplate = "https://{0}/v2/{1}/manifests/{2}"; // 0 - registry, 1 - repo(modulename), 2 - tag(version) @@ -487,13 +487,9 @@ internal bool IsContainerRegistryUnauthenticated(string containerRegistyUrl, out // get the anonymous access token var url = $"{realm}?service={service}&scope={defaultScope}"; - var results = GetHttpResponseJObjectUsingContentHeaders(url, HttpMethod.Get, content, contentHeaders, out errRecord); - if (errRecord != null) - { - _cmdletPassedIn.WriteDebug($"Failed to get access token from the realm. Error: {errRecord}"); - return false; - } + // we dont check the errorrecord here because we want to return false if we get a 401 and not throw an error + var results = GetHttpResponseJObjectUsingContentHeaders(url, HttpMethod.Get, content, contentHeaders, out _); if (results == null) { @@ -1834,7 +1830,7 @@ private FindResults FindPackages(string packageName, bool includePrerelease, out } // This remove the 'psresource/' prefix from the repository name for comparison with wildcard. - string moduleName = repositoryName.Substring(11); + string moduleName = repositoryName.StartsWith("psresource/") ? repositoryName.Substring(11) : repositoryName; WildcardPattern wildcardPattern = new WildcardPattern(packageName, WildcardOptions.IgnoreCase); From 4b3a109e60bfedf70868dbeab98c2b8fc669a7c5 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Wed, 19 Feb 2025 18:30:20 -0800 Subject: [PATCH 5/9] Add registry catalog scope for unauthenticated ACR --- src/code/ContainerRegistryServerAPICalls.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/code/ContainerRegistryServerAPICalls.cs b/src/code/ContainerRegistryServerAPICalls.cs index c310f149b..785c7aeae 100644 --- a/src/code/ContainerRegistryServerAPICalls.cs +++ b/src/code/ContainerRegistryServerAPICalls.cs @@ -46,7 +46,7 @@ internal class ContainerRegistryServerAPICalls : ServerApiCall const string containerRegistryFindImageVersionUrlTemplate = "https://{0}/v2/{1}/tags/list"; // 0 - registry, 1 - repo(modulename) const string containerRegistryStartUploadTemplate = "https://{0}/v2/{1}/blobs/uploads/"; // 0 - registry, 1 - packagename const string containerRegistryEndUploadTemplate = "https://{0}{1}&digest=sha256:{2}"; // 0 - registry, 1 - location, 2 - digest - const string defaultScope = "repository:*:*"; + const string defaultScope = "&scope=repository:*:*&scope=registry:catalog:*"; const string containerRegistryRepositoryListTemplate = "https://{0}/v2/_catalog"; // 0 - registry #endregion @@ -482,11 +482,11 @@ internal bool IsContainerRegistryUnauthenticated(string containerRegistyUrl, out return false; } - string content = "grant_type=access_token&service=" + service + "&scope=" + defaultScope; + string content = "grant_type=access_token&service=" + service + defaultScope; var contentHeaders = new Collection> { new KeyValuePair("Content-Type", "application/x-www-form-urlencoded") }; // get the anonymous access token - var url = $"{realm}?service={service}&scope={defaultScope}"; + var url = $"{realm}?service={service}{defaultScope}"; // we dont check the errorrecord here because we want to return false if we get a 401 and not throw an error var results = GetHttpResponseJObjectUsingContentHeaders(url, HttpMethod.Get, content, contentHeaders, out _); From 61b922f069f98ecb2690eee97aa2085c14d01ce7 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Wed, 19 Feb 2025 21:37:39 -0800 Subject: [PATCH 6/9] Fix test --- .../FindPSResourceContainerRegistryServer.Tests.ps1 | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 index 87c486ccf..ffbcdeb0e 100644 --- a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 @@ -151,12 +151,11 @@ Describe 'Test HTTP Find-PSResource for ACR Server Protocol' -tags 'CI' { $err[0].FullyQualifiedErrorId | Should -BeExactly "FindCommandOrDscResourceFailure,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource" } - It "Should not find all resources given Name '*'" { + It "Should find all resources given Name '*'" { # FindAll() $res = Find-PSResource -Name "*" -Repository $ACRRepoName -ErrorVariable err -ErrorAction SilentlyContinue - $res | Should -BeNullOrEmpty + $res | Should -Not -BeNullOrEmpty $err.Count | Should -BeGreaterThan 0 - $err[0].FullyQualifiedErrorId | Should -BeExactly "FindAllFailure,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource" } It "Should find script given Name" { From df02d9b1856d62b198890728c51ce459f65f6bae Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Wed, 19 Feb 2025 22:48:45 -0800 Subject: [PATCH 7/9] Fix expected value --- .../FindPSResourceContainerRegistryServer.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 index ffbcdeb0e..82843d68a 100644 --- a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 @@ -155,7 +155,7 @@ Describe 'Test HTTP Find-PSResource for ACR Server Protocol' -tags 'CI' { # FindAll() $res = Find-PSResource -Name "*" -Repository $ACRRepoName -ErrorVariable err -ErrorAction SilentlyContinue $res | Should -Not -BeNullOrEmpty - $err.Count | Should -BeGreaterThan 0 + $res.Count | Should -BeGreaterThan 0 } It "Should find script given Name" { From e32cd0ab1dc7cacca686ffbf477bc4e1b561147c Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Thu, 20 Feb 2025 15:41:35 -0800 Subject: [PATCH 8/9] Skip on WinPS --- ...dPSResourceContainerRegistryServer.Tests.ps1 | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 index 82843d68a..a1e51ddc3 100644 --- a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 @@ -268,16 +268,29 @@ Describe 'Test Find-PSResource for MAR Repository' -tags 'CI' { } } +# Skip this test fo Describe 'Test Find-PSResource for unauthenticated ACR repository' -tags 'CI' { BeforeAll { - Register-PSResourceRepository -Name "Unauthenticated" -Uri "https://psresourcegetnoauth.azurecr.io/" -ApiVersion "ContainerRegistry" + $skipOnWinPS = $PSVersionTable.PSVersion.Major -eq 5 + + if (-not $skipOnWinPS) { + Register-PSResourceRepository -Name "Unauthenticated" -Uri "https://psresourcegetnoauth.azurecr.io/" -ApiVersion "ContainerRegistry" + } } AfterAll { - Unregister-PSResourceRepository -Name "Unauthenticated" + if (-not $skipOnWinPS) { + Unregister-PSResourceRepository -Name "Unauthenticated" + } } It "Should find resource given specific Name, Version null" { + + if ($skipOnWinPS) { + Set-ItResult -Pending -Reason "Skipping test on Windows PowerShell" + return + } + $res = Find-PSResource -Name "hello-world" -Repository "Unauthenticated" $res.Name | Should -Be "hello-world" $res.Version | Should -Be "5.0.0" From 9722b2154bd54f47886906385a3baec4b9d19b96 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Thu, 20 Feb 2025 16:03:35 -0800 Subject: [PATCH 9/9] Fix parameter --- .../FindPSResourceContainerRegistryServer.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 index a1e51ddc3..8efa635a9 100644 --- a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 @@ -287,7 +287,7 @@ Describe 'Test Find-PSResource for unauthenticated ACR repository' -tags 'CI' { It "Should find resource given specific Name, Version null" { if ($skipOnWinPS) { - Set-ItResult -Pending -Reason "Skipping test on Windows PowerShell" + Set-ItResult -Pending -Because "Skipping test on Windows PowerShell" return }