@@ -38,14 +38,16 @@ internal class ContainerRegistryServerAPICalls : ServerApiCall
38
38
private static readonly FindResults emptyResponseResults = new FindResults ( stringResponse : Utils . EmptyStrArray , hashtableResponse : emptyHashResponses , responseType : containerRegistryFindResponseType ) ;
39
39
40
40
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
42
42
const string containerRegistryOAuthExchangeUrlTemplate = "https://{0}/oauth2/exchange" ; // 0 - registry
43
43
const string containerRegistryOAuthTokenUrlTemplate = "https://{0}/oauth2/token" ; // 0 - registry
44
44
const string containerRegistryManifestUrlTemplate = "https://{0}/v2/{1}/manifests/{2}" ; // 0 - registry, 1 - repo(modulename), 2 - tag(version)
45
45
const string containerRegistryBlobDownloadUrlTemplate = "https://{0}/v2/{1}/blobs/{2}" ; // 0 - registry, 1 - repo(modulename), 2 - layer digest
46
46
const string containerRegistryFindImageVersionUrlTemplate = "https://{0}/v2/{1}/tags/list" ; // 0 - registry, 1 - repo(modulename)
47
47
const string containerRegistryStartUploadTemplate = "https://{0}/v2/{1}/blobs/uploads/" ; // 0 - registry, 1 - packagename
48
48
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
49
51
50
52
#endregion
51
53
@@ -76,13 +78,13 @@ public ContainerRegistryServerAPICalls(PSRepositoryInfo repository, PSCmdlet cmd
76
78
public override FindResults FindAll ( bool includePrerelease , ResourceType type , out ErrorRecord errRecord )
77
79
{
78
80
_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
+ }
84
86
85
- return emptyResponseResults ;
87
+ return findResult ;
86
88
}
87
89
88
90
/// <summary>
@@ -161,13 +163,13 @@ public override FindResults FindNameWithTag(string packageName, string[] tags, b
161
163
public override FindResults FindNameGlobbing ( string packageName , bool includePrerelease , ResourceType type , out ErrorRecord errRecord )
162
164
{
163
165
_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
+ }
169
171
170
- return emptyResponseResults ;
172
+ return findResult ;
171
173
}
172
174
173
175
/// <summary>
@@ -391,12 +393,18 @@ internal string GetContainerRegistryAccessToken(out ErrorRecord errRecord)
391
393
}
392
394
else
393
395
{
394
- bool isRepositoryUnauthenticated = IsContainerRegistryUnauthenticated ( Repository . Uri . ToString ( ) , out errRecord ) ;
396
+ bool isRepositoryUnauthenticated = IsContainerRegistryUnauthenticated ( Repository . Uri . ToString ( ) , out errRecord , out accessToken ) ;
395
397
if ( errRecord != null )
396
398
{
397
399
return null ;
398
400
}
399
401
402
+ if ( ! string . IsNullOrEmpty ( accessToken ) )
403
+ {
404
+ _cmdletPassedIn . WriteVerbose ( "Anonymous access token retrieved." ) ;
405
+ return accessToken ;
406
+ }
407
+
400
408
if ( ! isRepositoryUnauthenticated )
401
409
{
402
410
accessToken = Utils . GetAzAccessToken ( ) ;
@@ -436,15 +444,82 @@ internal string GetContainerRegistryAccessToken(out ErrorRecord errRecord)
436
444
/// <summary>
437
445
/// Checks if container registry repository is unauthenticated.
438
446
/// </summary>
439
- internal bool IsContainerRegistryUnauthenticated ( string containerRegistyUrl , out ErrorRecord errRecord )
447
+ internal bool IsContainerRegistryUnauthenticated ( string containerRegistyUrl , out ErrorRecord errRecord , out string anonymousAccessToken )
440
448
{
441
449
_cmdletPassedIn . WriteDebug ( "In ContainerRegistryServerAPICalls::IsContainerRegistryUnauthenticated()" ) ;
442
450
errRecord = null ;
451
+ anonymousAccessToken = string . Empty ;
443
452
string endpoint = $ "{ containerRegistyUrl } /v2/";
444
453
HttpResponseMessage response ;
445
454
try
446
455
{
447
456
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 ;
448
523
}
449
524
catch ( Exception e )
450
525
{
@@ -591,6 +666,20 @@ internal JObject FindContainerRegistryImageTags(string packageName, string versi
591
666
return GetHttpResponseJObjectUsingDefaultHeaders ( findImageUrl , HttpMethod . Get , defaultHeaders , out errRecord ) ;
592
667
}
593
668
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
+
594
683
/// <summary>
595
684
/// Get metadata for a package version.
596
685
/// </summary>
@@ -1705,12 +1794,63 @@ private string PrependMARPrefix(string packageName)
1705
1794
1706
1795
// If the repostitory is MAR and its not a wildcard search, we need to prefix the package name with MAR prefix.
1707
1796
string updatedPackageName = Repository . IsMARRepository ( ) && packageName . Trim ( ) != "*"
1708
- ? string . Concat ( prefix , packageName )
1797
+ ? packageName . StartsWith ( prefix ) ? packageName : string . Concat ( prefix , packageName )
1709
1798
: packageName ;
1710
1799
1711
1800
return updatedPackageName ;
1712
1801
}
1713
1802
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
+
1714
1854
#endregion
1715
1855
}
1716
1856
}
0 commit comments