Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES/+unquoted-auth-challenge.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed pull-through signature lookups failing with a 500 when the upstream registry (e.g. gcr.io) sends unquoted values in its bearer auth challenge.
27 changes: 19 additions & 8 deletions pulp_container/app/downloaders.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,14 +115,7 @@ async def update_token(self, response_auth_header, used_token, repo_name):
):
return
log.info("Updating bearer token")
bearer_info_string = response_auth_header[len("Bearer ") :]
bearer_info_list = re.split(",(?=[^=,]+=)", bearer_info_string)

# The remaining string consists of comma seperated key=value pairs
auth_query_dict = {}
for key, value in (item.split("=") for item in bearer_info_list):
# The value is a string within a string, ex: '"value"'
auth_query_dict[key] = json.loads(value)
auth_query_dict = self.parse_auth_challenge(response_auth_header)
try:
token_base_url = auth_query_dict.pop("realm")
except KeyError:
Expand Down Expand Up @@ -156,6 +149,24 @@ async def update_token(self, response_auth_header, used_token, repo_name):
token = token_js.get("token") or token_js.get("access_token")
self.factory.registry_auth["bearer"] = token

@staticmethod
def parse_auth_challenge(response_auth_header):
"""
Parse the parameters of a Bearer auth challenge into a dictionary.

Values may be quoted strings or plain tokens (RFC 7235), e.g. gcr.io responds with
'Bearer realm="https://gcr.io/v2/token",service=gcr.io'.
"""
bearer_info_string = response_auth_header[len("Bearer ") :]
auth_query_dict = {}
for match in re.finditer(r'([^=,\s]+)=(?:"((?:[^"\\]|\\.)*)"|([^,]*))', bearer_info_string):
key, quoted, plain = match.groups()
if quoted is not None:
auth_query_dict[key] = re.sub(r"\\(.)", r"\1", quoted)
else:
auth_query_dict[key] = plain.strip()
return auth_query_dict

@staticmethod
def auth_header(token, basic_auth):
"""
Expand Down
33 changes: 33 additions & 0 deletions pulp_container/tests/unit/test_downloaders.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import pytest

from pulp_container.app.downloaders import RegistryAuthHttpDownloader


@pytest.mark.parametrize(
"header,expected",
[
(
'Bearer realm="https://auth.docker.io/token",service="registry.docker.io"',
{"realm": "https://auth.docker.io/token", "service": "registry.docker.io"},
),
(
'Bearer realm="https://gcr.io/v2/token",service=gcr.io',
{"realm": "https://gcr.io/v2/token", "service": "gcr.io"},
),
(
'Bearer realm="https://quay.io/v2/auth",service="quay.io",'
'scope="repository:foo/bar:pull,push"',
{
"realm": "https://quay.io/v2/auth",
"service": "quay.io",
"scope": "repository:foo/bar:pull,push",
},
),
(
'Bearer realm="https://example.com/token", service=example.com, error="a \\"b\\""',
{"realm": "https://example.com/token", "service": "example.com", "error": 'a "b"'},
),
],
)
def test_parse_auth_challenge(header, expected):
assert RegistryAuthHttpDownloader.parse_auth_challenge(header) == expected
Loading