@@ -38,14 +38,16 @@ 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:*" ;
50+ const string containerRegistryRepositoryListTemplate = "https://{0}/v2/_catalog" ; // 0 - registry
4951
5052 #endregion
5153
@@ -76,13 +78,13 @@ public ContainerRegistryServerAPICalls(PSRepositoryInfo repository, PSCmdlet cmd
7678 public override FindResults FindAll ( bool includePrerelease , ResourceType type , out ErrorRecord errRecord )
7779 {
7880 _cmdletPassedIn . WriteDebug ( "In ContainerRegistryServerAPICalls::FindAll()" ) ;
79- errRecord = new ErrorRecord (
80- new InvalidOperationException ( $ "Find all is not supported for the ContainerRegistry server protocol repository ' { Repository . Name } '" ) ,
81- "FindAllFailure" ,
82- ErrorCategory . InvalidOperation ,
83- this ) ;
81+ var findResult = FindPackages ( "*" , includePrerelease , out errRecord ) ;
82+ if ( errRecord != null )
83+ {
84+ return emptyResponseResults ;
85+ }
8486
85- return emptyResponseResults ;
87+ return findResult ;
8688 }
8789
8890 /// <summary>
@@ -161,13 +163,13 @@ public override FindResults FindNameWithTag(string packageName, string[] tags, b
161163 public override FindResults FindNameGlobbing ( string packageName , bool includePrerelease , ResourceType type , out ErrorRecord errRecord )
162164 {
163165 _cmdletPassedIn . WriteDebug ( "In ContainerRegistryServerAPICalls::FindNameGlobbing()" ) ;
164- errRecord = new ErrorRecord (
165- new InvalidOperationException ( $ "FindNameGlobbing all is not supported for the ContainerRegistry server protocol repository ' { Repository . Name } '" ) ,
166- "FindNameGlobbingFailure" ,
167- ErrorCategory . InvalidOperation ,
168- this ) ;
166+ var findResult = FindPackages ( packageName , includePrerelease , out errRecord ) ;
167+ if ( errRecord != null )
168+ {
169+ return emptyResponseResults ;
170+ }
169171
170- return emptyResponseResults ;
172+ return findResult ;
171173 }
172174
173175 /// <summary>
@@ -391,12 +393,18 @@ internal string GetContainerRegistryAccessToken(out ErrorRecord errRecord)
391393 }
392394 else
393395 {
394- bool isRepositoryUnauthenticated = IsContainerRegistryUnauthenticated ( Repository . Uri . ToString ( ) , out errRecord ) ;
396+ bool isRepositoryUnauthenticated = IsContainerRegistryUnauthenticated ( Repository . Uri . ToString ( ) , out errRecord , out accessToken ) ;
395397 if ( errRecord != null )
396398 {
397399 return null ;
398400 }
399401
402+ if ( ! string . IsNullOrEmpty ( accessToken ) )
403+ {
404+ _cmdletPassedIn . WriteVerbose ( "Anonymous access token retrieved." ) ;
405+ return accessToken ;
406+ }
407+
400408 if ( ! isRepositoryUnauthenticated )
401409 {
402410 accessToken = Utils . GetAzAccessToken ( ) ;
@@ -436,15 +444,82 @@ internal string GetContainerRegistryAccessToken(out ErrorRecord errRecord)
436444 /// <summary>
437445 /// Checks if container registry repository is unauthenticated.
438446 /// </summary>
439- internal bool IsContainerRegistryUnauthenticated ( string containerRegistyUrl , out ErrorRecord errRecord )
447+ internal bool IsContainerRegistryUnauthenticated ( string containerRegistyUrl , out ErrorRecord errRecord , out string anonymousAccessToken )
440448 {
441449 _cmdletPassedIn . WriteDebug ( "In ContainerRegistryServerAPICalls::IsContainerRegistryUnauthenticated()" ) ;
442450 errRecord = null ;
451+ anonymousAccessToken = string . Empty ;
443452 string endpoint = $ "{ containerRegistyUrl } /v2/";
444453 HttpResponseMessage response ;
445454 try
446455 {
447456 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 ;
448523 }
449524 catch ( Exception e )
450525 {
@@ -591,6 +666,20 @@ internal JObject FindContainerRegistryImageTags(string packageName, string versi
591666 return GetHttpResponseJObjectUsingDefaultHeaders ( findImageUrl , HttpMethod . Get , defaultHeaders , out errRecord ) ;
592667 }
593668
669+ /// <summary>
670+ /// Helper method to find all packages on container registry
671+ /// </summary>
672+ /// <param name="containerRegistryAccessToken"></param>
673+ /// <param name="errRecord"></param>
674+ /// <returns></returns>
675+ internal JObject FindAllRepositories ( string containerRegistryAccessToken , out ErrorRecord errRecord )
676+ {
677+ _cmdletPassedIn . WriteDebug ( "In ContainerRegistryServerAPICalls::FindAllRepositories()" ) ;
678+ string repositoryListUrl = string . Format ( containerRegistryRepositoryListTemplate , Registry ) ;
679+ var defaultHeaders = GetDefaultHeaders ( containerRegistryAccessToken ) ;
680+ return GetHttpResponseJObjectUsingDefaultHeaders ( repositoryListUrl , HttpMethod . Get , defaultHeaders , out errRecord ) ;
681+ }
682+
594683 /// <summary>
595684 /// Get metadata for a package version.
596685 /// </summary>
@@ -1705,12 +1794,63 @@ private string PrependMARPrefix(string packageName)
17051794
17061795 // If the repostitory is MAR and its not a wildcard search, we need to prefix the package name with MAR prefix.
17071796 string updatedPackageName = Repository . IsMARRepository ( ) && packageName . Trim ( ) != "*"
1708- ? string . Concat ( prefix , packageName )
1797+ ? packageName . StartsWith ( prefix ) ? packageName : string . Concat ( prefix , packageName )
17091798 : packageName ;
17101799
17111800 return updatedPackageName ;
17121801 }
17131802
1803+ private FindResults FindPackages ( string packageName , bool includePrerelease , out ErrorRecord errRecord )
1804+ {
1805+ _cmdletPassedIn . WriteDebug ( "In ContainerRegistryServerAPICalls::FindPackages()" ) ;
1806+ errRecord = null ;
1807+ string containerRegistryAccessToken = GetContainerRegistryAccessToken ( out errRecord ) ;
1808+ if ( errRecord != null )
1809+ {
1810+ return emptyResponseResults ;
1811+ }
1812+
1813+ var pkgResult = FindAllRepositories ( containerRegistryAccessToken , out errRecord ) ;
1814+ if ( errRecord != null )
1815+ {
1816+ return emptyResponseResults ;
1817+ }
1818+
1819+ List < Hashtable > repositoriesList = new List < Hashtable > ( ) ;
1820+ var isMAR = Repository . IsMARRepository ( ) ;
1821+
1822+ // Convert the list of repositories to a list of hashtables
1823+ foreach ( var repository in pkgResult [ "repositories" ] . ToList ( ) )
1824+ {
1825+ string repositoryName = repository . ToString ( ) ;
1826+
1827+ if ( isMAR && ! repositoryName . StartsWith ( PSRepositoryInfo . MARPrefix ) )
1828+ {
1829+ continue ;
1830+ }
1831+
1832+ // This remove the 'psresource/' prefix from the repository name for comparison with wildcard.
1833+ string moduleName = repositoryName . StartsWith ( "psresource/" ) ? repositoryName . Substring ( 11 ) : repositoryName ;
1834+
1835+ WildcardPattern wildcardPattern = new WildcardPattern ( packageName , WildcardOptions . IgnoreCase ) ;
1836+
1837+ if ( ! wildcardPattern . IsMatch ( moduleName ) )
1838+ {
1839+ continue ;
1840+ }
1841+
1842+ _cmdletPassedIn . WriteDebug ( $ "Found repository: { repositoryName } ") ;
1843+
1844+ repositoriesList . AddRange ( FindPackagesWithVersionHelper ( repositoryName , VersionType . VersionRange , versionRange : VersionRange . All , requiredVersion : null , includePrerelease , getOnlyLatest : true , out errRecord ) ) ;
1845+ if ( errRecord != null )
1846+ {
1847+ return emptyResponseResults ;
1848+ }
1849+ }
1850+
1851+ return new FindResults ( stringResponse : new string [ ] { } , hashtableResponse : repositoriesList . ToArray ( ) , responseType : containerRegistryFindResponseType ) ;
1852+ }
1853+
17141854 #endregion
17151855 }
17161856}
0 commit comments