diff --git a/src/code/ContainerRegistryServerAPICalls.cs b/src/code/ContainerRegistryServerAPICalls.cs index 785c7aeae..35ff9a4be 100644 --- a/src/code/ContainerRegistryServerAPICalls.cs +++ b/src/code/ContainerRegistryServerAPICalls.cs @@ -47,6 +47,10 @@ internal class ContainerRegistryServerAPICalls : ServerApiCall 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 = "&scope=repository:*:*&scope=registry:catalog:*"; + const string catalogScope = "&scope=registry:catalog:*"; + const string grantTypeTemplate = "grant_type=access_token&service={0}{1}"; // 0 - registry, 1 - scope + const string authUrlTemplate = "{0}?service={1}{2}"; // 0 - realm, 1 - service, 2 - scope + const string containerRegistryRepositoryListTemplate = "https://{0}/v2/_catalog"; // 0 - registry #endregion @@ -323,7 +327,7 @@ private Stream InstallVersion( return null; } - string containerRegistryAccessToken = GetContainerRegistryAccessToken(out errRecord); + string containerRegistryAccessToken = GetContainerRegistryAccessToken(needCatalogAccess: false, out errRecord); if (errRecord != null) { return null; @@ -371,7 +375,7 @@ private Stream InstallVersion( /// If no credential provided at registration then, check if the ACR endpoint can be accessed without a token. If not, try using Azure.Identity to get the az access token, then ACR refresh token and then ACR access token. /// Note: Access token can be empty if the repository is unauthenticated /// - internal string GetContainerRegistryAccessToken(out ErrorRecord errRecord) + internal string GetContainerRegistryAccessToken(bool needCatalogAccess, out ErrorRecord errRecord) { _cmdletPassedIn.WriteDebug("In ContainerRegistryServerAPICalls::GetContainerRegistryAccessToken()"); string accessToken = string.Empty; @@ -393,7 +397,7 @@ internal string GetContainerRegistryAccessToken(out ErrorRecord errRecord) } else { - bool isRepositoryUnauthenticated = IsContainerRegistryUnauthenticated(Repository.Uri.ToString(), out errRecord, out accessToken); + bool isRepositoryUnauthenticated = IsContainerRegistryUnauthenticated(Repository.Uri.ToString(), needCatalogAccess, out errRecord, out accessToken); if (errRecord != null) { return null; @@ -444,7 +448,7 @@ internal string GetContainerRegistryAccessToken(out ErrorRecord errRecord) /// /// Checks if container registry repository is unauthenticated. /// - internal bool IsContainerRegistryUnauthenticated(string containerRegistyUrl, out ErrorRecord errRecord, out string anonymousAccessToken) + internal bool IsContainerRegistryUnauthenticated(string containerRegistyUrl, bool needCatalogAccess, out ErrorRecord errRecord, out string anonymousAccessToken) { _cmdletPassedIn.WriteDebug("In ContainerRegistryServerAPICalls::IsContainerRegistryUnauthenticated()"); errRecord = null; @@ -482,18 +486,22 @@ internal bool IsContainerRegistryUnauthenticated(string containerRegistyUrl, out return false; } - string content = "grant_type=access_token&service=" + service + defaultScope; + string content = needCatalogAccess ? String.Format(grantTypeTemplate, service, catalogScope) : String.Format(grantTypeTemplate, 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}{defaultScope}"; + string url = needCatalogAccess ? String.Format(authUrlTemplate, realm, service, catalogScope) : String.Format(authUrlTemplate, realm, 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 _); + _cmdletPassedIn.WriteDebug($"Getting anonymous access token from the realm: {url}"); + ErrorRecord errRecordTemp = null; + + var results = GetHttpResponseJObjectUsingContentHeaders(url, HttpMethod.Get, content, contentHeaders, out errRecordTemp); if (results == null) { _cmdletPassedIn.WriteDebug("Failed to get access token from the realm. results is null."); + _cmdletPassedIn.WriteDebug($"ErrorRecord: {errRecordTemp}"); return false; } @@ -504,6 +512,7 @@ internal bool IsContainerRegistryUnauthenticated(string containerRegistyUrl, out } anonymousAccessToken = results["access_token"].ToString(); + _cmdletPassedIn.WriteDebug("Anonymous access token retrieved"); return true; } @@ -984,24 +993,29 @@ internal JObject GetHttpResponseJObjectUsingContentHeaders(string url, HttpMetho { HttpRequestMessage request = new HttpRequestMessage(method, url); - if (string.IsNullOrEmpty(content)) + // HTTP GET does not expect a body / content. + if (method != HttpMethod.Get) { - errRecord = new ErrorRecord( - exception: new ArgumentNullException($"Content is null or empty and cannot be used to make a request as its content headers."), - "RequestContentHeadersNullOrEmpty", - ErrorCategory.InvalidData, - _cmdletPassedIn); - return null; - } + if (string.IsNullOrEmpty(content)) + { + errRecord = new ErrorRecord( + exception: new ArgumentNullException($"Content is null or empty and cannot be used to make a request as its content headers."), + "RequestContentHeadersNullOrEmpty", + ErrorCategory.InvalidData, + _cmdletPassedIn); - request.Content = new StringContent(content); - request.Content.Headers.Clear(); - if (contentHeaders != null) - { - foreach (var header in contentHeaders) + return null; + } + + request.Content = new StringContent(content); + request.Content.Headers.Clear(); + if (contentHeaders != null) { - request.Content.Headers.Add(header.Key, header.Value); + foreach (var header in contentHeaders) + { + request.Content.Headers.Add(header.Key, header.Value); + } } } @@ -1230,7 +1244,7 @@ internal bool PushNupkgContainerRegistry( // Get access token (includes refresh tokens) _cmdletPassedIn.WriteVerbose($"Get access token for container registry server."); - var containerRegistryAccessToken = GetContainerRegistryAccessToken(out errRecord); + var containerRegistryAccessToken = GetContainerRegistryAccessToken(needCatalogAccess: false, out errRecord); if (errRecord != null) { return false; @@ -1695,7 +1709,7 @@ private Hashtable[] FindPackagesWithVersionHelper(string packageName, VersionTyp string packageNameLowercase = packageName.ToLower(); string packageNameForFind = PrependMARPrefix(packageNameLowercase); - string containerRegistryAccessToken = GetContainerRegistryAccessToken(out errRecord); + string containerRegistryAccessToken = GetContainerRegistryAccessToken(needCatalogAccess: false, out errRecord); if (errRecord != null) { return emptyHashResponses; @@ -1711,8 +1725,9 @@ private Hashtable[] FindPackagesWithVersionHelper(string packageName, VersionTyp List allVersionsList = foundTags["tags"].ToList(); SortedDictionary sortedQualifyingPkgs = GetPackagesWithRequiredVersion(allVersionsList, versionType, versionRange, requiredVersion, packageNameForFind, includePrerelease, out errRecord); - if (errRecord != null) + if (errRecord != null && sortedQualifyingPkgs?.Count == 0) { + _cmdletPassedIn.WriteDebug("No qualifying packages found for the specified criteria."); return emptyHashResponses; } @@ -1761,7 +1776,9 @@ private Hashtable[] FindPackagesWithVersionHelper(string packageName, VersionTyp ErrorCategory.InvalidArgument, this); - return null; + _cmdletPassedIn.WriteError(errRecord); + _cmdletPassedIn.WriteDebug($"Skipping package '{packageName}' with version '{pkgVersionString}' as it is not a valid NuGet version."); + continue; // skip this version and continue with the next one } _cmdletPassedIn.WriteDebug($"'{packageName}' version parsed as '{pkgVersion}'"); @@ -1804,7 +1821,7 @@ private FindResults FindPackages(string packageName, bool includePrerelease, out { _cmdletPassedIn.WriteDebug("In ContainerRegistryServerAPICalls::FindPackages()"); errRecord = null; - string containerRegistryAccessToken = GetContainerRegistryAccessToken(out errRecord); + string containerRegistryAccessToken = GetContainerRegistryAccessToken(needCatalogAccess: true, out errRecord); if (errRecord != null) { return emptyResponseResults;