From 81c4b04f7b1993602771946ef31f33a34a3e86e8 Mon Sep 17 00:00:00 2001 From: Matteo Mortari Date: Tue, 24 Sep 2024 16:41:31 +0200 Subject: [PATCH] core: improve anon/auth token logic (#148) * core: TokenAuth request_token fix missing auth the method is intended to request authenticated token, per pydocs, but was passing an headers which was always missing Authorization. * core: use token in auth in subsequent requests if a token was saved in auth, it shall be used in subsequent requests. This avoid a situation where: to upload a blob, first is done anonymously, then retry with token then upload a manifest, avoid the attempt to upload anonymously if a token was present in the previous flow * core: if 401 on 2nd attempt, avoid anon tokens in the first flow using auth backend for token: 1. try do_request with no auths at all 2. the attempt to gain an anon token is success, but then the request fails with 401 3. at this point, in the third attempt, give chance to the flow to request a token but avoid any anon tokens. Please note: this happens effectively only on the first run of the flow. Subsequent do_request flow invocations should just succeed now on the 1st request by re-using the token --simplified behaviour introduced with this proposal * guard as headers is Optional * implement review request * Revert "implement review request" This reverts commit 102381c5c4ae0fdf45c8a4dd26ae1765eae9b029. This reverts commit 1e891d2bfebe4b6520a1fe6902159198c8799d62. This reverts commit 6e226672c60184cd43b6532f5a910acbf9d064ea. this was taken care in https://github.com/oras-project/oras-py/pull/153 This reverts commit 10e010b365e56488963ca14b6e9e08b1ea7e4a7a. * implement review comment about anon/req token from: https://github.com/oras-project/oras-py/pull/148#discussion_r1677018164 > And if the basic auth is there, skip over asking for an anon token as it stands, in case the basic auth are present, these are exchanged for the request token. Signed-off-by: tarilabs --------- Signed-off-by: tarilabs --- .gitignore | 3 +++ oras/auth/token.py | 25 +++++++++++++------------ oras/auth/utils.py | 3 +++ oras/provider.py | 4 +++- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 55b5d328..a4755ad6 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ env __pycache__ .python-version .venv +.vscode +build +dist diff --git a/oras/auth/token.py b/oras/auth/token.py index 355848d6..2ab436cb 100644 --- a/oras/auth/token.py +++ b/oras/auth/token.py @@ -71,16 +71,16 @@ def authenticate_request( h = auth_utils.parse_auth_header(authHeaderRaw) - # First try to request an anonymous token - logger.debug("No Authorization, requesting anonymous token") - anon_token = self.request_anonymous_token(h) - if anon_token: - logger.debug("Successfully obtained anonymous token!") - self.token = anon_token - headers["Authorization"] = "Bearer %s" % self.token - return headers, True - - # Next try for logged in token + # if no basic auth, try by request an anonymous token + if not hasattr(self, "_basic_auth"): + anon_token = self.request_anonymous_token(h) + if anon_token: + logger.debug("Successfully obtained anonymous token!") + self.token = anon_token + headers["Authorization"] = "Bearer %s" % self.token + return headers, True + + # basic auth is available, try using auth token token = self.request_token(h) if token: self.token = token @@ -95,7 +95,7 @@ def authenticate_request( def request_token(self, h: auth_utils.authHeader) -> bool: """ - Request an authenticated token and save for later.s + Request an authenticated token and save for later. """ params = {} headers = {} @@ -124,6 +124,7 @@ def request_token(self, h: auth_utils.authHeader) -> bool: # Set Basic Auth to receive token headers["Authorization"] = "Basic %s" % self._basic_auth + logger.debug(f"Requesting auth token for: {h}") authResponse = self.session.get(h.realm, headers=headers, params=params) # type: ignore if authResponse.status_code != 200: @@ -150,7 +151,7 @@ def request_anonymous_token(self, h: auth_utils.authHeader) -> bool: if h.scope: params["scope"] = h.scope - logger.debug(f"Final params are {params}") + logger.debug(f"Requesting anon token with params: {params}") response = self.session.request("GET", h.realm, params=params) if response.status_code != 200: logger.debug(f"Response for anon token failed: {response.text}") diff --git a/oras/auth/utils.py b/oras/auth/utils.py index 6e3ba5d4..30b83266 100644 --- a/oras/auth/utils.py +++ b/oras/auth/utils.py @@ -65,6 +65,9 @@ def __init__(self, lookup: dict): if key in ["realm", "service", "scope"]: setattr(self, key, lookup[key]) + def __repr__(self): + return f"authHeader(lookup={{'service': {repr(self.service)}, 'realm': {repr(self.realm)}, 'scope': {repr(self.scope)}}})" + def parse_auth_header(authHeaderRaw: str) -> authHeader: """ diff --git a/oras/provider.py b/oras/provider.py index d1ef5640..ff2e950e 100644 --- a/oras/provider.py +++ b/oras/provider.py @@ -957,7 +957,9 @@ def do_request( :param stream: stream the responses :type stream: bool """ - # Make the request and return to calling function, unless requires auth + # Make the request and return to calling function, but attempt to use auth token if previously obtained + if headers is not None and isinstance(self.auth, oras.auth.TokenAuth): + headers.update(self.auth.get_auth_header()) response = self.session.request( method, url,