Skip to content

Commit c6cb24b

Browse files
committed
Test DockerRegistry WWW-Authenticate token handling
1 parent 4b0bc84 commit c6cb24b

File tree

1 file changed

+102
-6
lines changed

1 file changed

+102
-6
lines changed

binderhub/tests/test_registry.py

Lines changed: 102 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
import base64
33
import json
44
import os
5+
from random import randint
56

7+
import pytest
8+
from tornado import httpclient
69
from tornado.web import Application, HTTPError, RequestHandler
710

811
from 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

65106
class 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

Comments
 (0)