22import base64
33import json
44import os
5+ from random import randint
56
7+ import pytest
8+ from tornado import httpclient
69from tornado .web import Application , HTTPError , RequestHandler
710
811from binderhub .registry import DockerRegistry
@@ -59,17 +62,62 @@ def test_registry_gcr_defaults(tmpdir):
5962 assert registry .password == "{...}"
6063
6164
65+ @pytest .mark .parametrize (
66+ "header,expected" ,
67+ [
68+ (
69+ 'Bearer realm="https://example.org/abc/token",service="example.org",scope=""' ,
70+ ("https://example.org/abc/token" , "example.org" , "" ),
71+ ),
72+ (
73+ 'BEARER scope="abc",service="example.org",realm="https://example.org/abc/token"' ,
74+ ("https://example.org/abc/token" , "example.org" , "abc" ),
75+ ),
76+ ],
77+ )
78+ def test_parse_www_authenticate_header (header , expected ):
79+ registry = DockerRegistry ()
80+ assert expected == registry ._parse_www_authenticate_header (header )
81+
82+
83+ @pytest .mark .parametrize (
84+ "header,expected" ,
85+ [
86+ (
87+ 'basic realm="https://example.org/abc/token"' ,
88+ "Only WWW-Authenticate Bearer type supported" ,
89+ ),
90+ (
91+ 'bearer realm="https://example.org/abc/token"' ,
92+ "Expected WWW-Authenticate to include realm service scope" ,
93+ ),
94+ ],
95+ )
96+ def test_parse_www_authenticate_header_invalid (header , expected ):
97+ registry = DockerRegistry ()
98+ with pytest .raises (ValueError ) as excinfo :
99+ registry ._parse_www_authenticate_header (header )
100+ assert excinfo .value .args [0 ].startswith (expected )
101+
102+
62103# Mock the registry API calls made by get_image_manifest
63104
64105
65106class MockTokenHandler (RequestHandler ):
66107 """Mock handler for the registry token handler"""
67108
68- def initialize (self , test_handle ):
109+ def initialize (self , test_handle , service = None , scope = None ):
69110 self .test_handle = test_handle
111+ self .service = service
112+ self .scope = scope
70113
71114 def get (self ):
72- self .get_argument ("scope" )
115+ scope = self .get_argument ("scope" )
116+ if self .scope :
117+ assert scope == self .scope
118+ service = self .get_argument ("service" )
119+ if self .service :
120+ assert service == self .service
73121 auth_header = self .request .headers .get ("Authorization" , "" )
74122 if not auth_header .startswith ("Basic " ):
75123 raise HTTPError (401 , "No basic auth" )
@@ -104,8 +152,52 @@ def get(self, image, tag):
104152 # get_image_manifest never looks at the contents here
105153 self .write (json .dumps ({"image" : image , "tag" : tag }))
106154
155+ def write_error (self , status_code , ** kwargs ):
156+ err_cls , err , traceback = kwargs ["exc_info" ]
157+ if status_code == 401 :
158+ r = self .request
159+ self .set_header (
160+ "WWW-Authenticate" ,
161+ f'Bearer realm="{ r .protocol } ://{ r .host } /token",service="service=1",scope="scope-2"' ,
162+ )
163+ super ().write_error (status_code , ** kwargs )
164+
165+
166+ async def test_get_token ():
167+ username = "user"
168+ password = "pass"
169+ test_handle = {"username" : username , "password" : password }
170+ app = Application (
171+ [
172+ (r"/token" , MockTokenHandler , {"test_handle" : test_handle }),
173+ ]
174+ )
175+ ip = "127.0.0.1"
176+ port = randint (10000 , 65535 )
177+ app .listen (port , ip )
178+
179+ registry = DockerRegistry (
180+ url = "https://example.org" , username = username , password = password
181+ )
182+
183+ assert registry .url == "https://example.org"
184+ assert registry .auth_config_url == "https://example.org"
185+ # token_url should be unset, since it should be determined by the caller from a
186+ # WWW-Authenticate header
187+ assert registry .token_url == ""
188+ assert registry .username == username
189+ assert registry .password == password
190+ token = await registry ._get_token (
191+ httpclient .AsyncHTTPClient (),
192+ f"http://{ ip } :{ port } /token" ,
193+ "service.1" ,
194+ "scope.2" ,
195+ )
196+ assert token == test_handle ["token" ]
197+
107198
108- async def test_get_image_manifest (tmpdir , request ):
199+ @pytest .mark .parametrize ("token_url_known" , [True , False ])
200+ async def test_get_image_manifest (tmpdir , token_url_known ):
109201 username = "asdf"
110202 password = "asdf;ouyag"
111203 test_handle = {"username" : username , "password" : password }
@@ -120,7 +212,7 @@ async def test_get_image_manifest(tmpdir, request):
120212 ]
121213 )
122214 ip = "127.0.0.1"
123- port = 10504
215+ port = randint ( 10000 , 65535 )
124216 url = f"http://{ ip } :{ port } "
125217 app .listen (port , ip )
126218 config_json = tmpdir .join ("dockerconfig.json" )
@@ -137,12 +229,16 @@ async def test_get_image_manifest(tmpdir, request):
137229 },
138230 f ,
139231 )
232+ if token_url_known :
233+ token_url = url + "/token"
234+ else :
235+ token_url = ""
140236 registry = DockerRegistry (
141- docker_config_path = str (config_json ), token_url = url + "/token" , url = url
237+ docker_config_path = str (config_json ), token_url = token_url , url = url
142238 )
143239 assert registry .url == url
144240 assert registry .auth_config_url == url
145- assert registry .token_url == url + "/token"
241+ assert registry .token_url == token_url
146242 assert registry .username == username
147243 assert registry .password == password
148244 manifest = await registry .get_image_manifest ("myimage" , "abc123" )
0 commit comments