From fc44c0113ae701e5e1941bb4fa8d94b535f2448b Mon Sep 17 00:00:00 2001 From: tarilabs Date: Sat, 6 Jul 2024 22:08:41 +0200 Subject: [PATCH 01/13] 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. Signed-off-by: tarilabs --- oras/auth/token.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/oras/auth/token.py b/oras/auth/token.py index 355848d..0b47d7f 100644 --- a/oras/auth/token.py +++ b/oras/auth/token.py @@ -99,6 +99,8 @@ def request_token(self, h: auth_utils.authHeader) -> bool: """ params = {} headers = {} + if self._basic_auth: # we exchange the basic auth for the token + headers["Authorization"] = "Basic %s" % self._basic_auth # Prepare request to retry if h.service: From 3d434a0566ae5b0b9559872229c1c57dbb715595 Mon Sep 17 00:00:00 2001 From: tarilabs Date: Sat, 6 Jul 2024 22:11:28 +0200 Subject: [PATCH 02/13] 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 Signed-off-by: tarilabs --- oras/provider.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/oras/provider.py b/oras/provider.py index d1ef564..6f3db0e 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 isinstance(self.auth, oras.auth.TokenAuth): + headers.update(self.auth.get_auth_header()) response = self.session.request( method, url, From f8409f9e41fa887b165e5ec78f0557573704cd9c Mon Sep 17 00:00:00 2001 From: tarilabs Date: Sat, 6 Jul 2024 22:14:51 +0200 Subject: [PATCH 03/13] 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 Signed-off-by: tarilabs --- oras/auth/token.py | 19 +++++++++++-------- oras/provider.py | 14 ++++++++++++++ 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/oras/auth/token.py b/oras/auth/token.py index 0b47d7f..f8c346e 100644 --- a/oras/auth/token.py +++ b/oras/auth/token.py @@ -44,7 +44,7 @@ def reset_basic_auth(self): self.set_header("Authorization", "Basic %s" % self._basic_auth) def authenticate_request( - self, original: requests.Response, headers: dict, refresh=False + self, original: requests.Response, headers: dict, refresh=False, skipAnonToken=False ): """ Authenticate Request @@ -72,17 +72,20 @@ 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 + if not skipAnonToken: + 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 + logger.debug("requesting auth token") token = self.request_token(h) if token: + logger.debug("Successfully obtained auth token!") self.token = token headers["Authorization"] = "Bearer %s" % self.token return headers, True diff --git a/oras/provider.py b/oras/provider.py index 6f3db0e..881012b 100644 --- a/oras/provider.py +++ b/oras/provider.py @@ -1002,5 +1002,19 @@ def do_request( stream=stream, verify=self._tls_verify, ) + # ...or attempt exchange anon token for auth token if 401 + if response.status_code == 401: + headers, changed = self.auth.authenticate_request( + response, headers, refresh=True, skipAnonToken=True + ) + response = self.session.request( + method, + url, + data=data, + json=json, + headers=headers, + stream=stream, + verify=self._tls_verify, + ) return response From 425d035b66e7a045bb84459ef3a79331e0e36a43 Mon Sep 17 00:00:00 2001 From: tarilabs Date: Sat, 6 Jul 2024 22:28:45 +0200 Subject: [PATCH 04/13] linting Signed-off-by: tarilabs --- oras/auth/token.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/oras/auth/token.py b/oras/auth/token.py index f8c346e..49563e9 100644 --- a/oras/auth/token.py +++ b/oras/auth/token.py @@ -44,7 +44,11 @@ def reset_basic_auth(self): self.set_header("Authorization", "Basic %s" % self._basic_auth) def authenticate_request( - self, original: requests.Response, headers: dict, refresh=False, skipAnonToken=False + self, + original: requests.Response, + headers: dict, + refresh=False, + skipAnonToken=False, ): """ Authenticate Request @@ -102,7 +106,7 @@ def request_token(self, h: auth_utils.authHeader) -> bool: """ params = {} headers = {} - if self._basic_auth: # we exchange the basic auth for the token + if self._basic_auth: # we exchange the basic auth for the token headers["Authorization"] = "Basic %s" % self._basic_auth # Prepare request to retry From 708027f042de829e1ac4e36c988558c885d6f3e2 Mon Sep 17 00:00:00 2001 From: tarilabs Date: Sat, 6 Jul 2024 22:35:57 +0200 Subject: [PATCH 05/13] guard as headers is Optional Signed-off-by: tarilabs --- oras/provider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oras/provider.py b/oras/provider.py index 881012b..129b75d 100644 --- a/oras/provider.py +++ b/oras/provider.py @@ -958,7 +958,7 @@ def do_request( :type stream: bool """ # Make the request and return to calling function, but attempt to use auth token if previously obtained - if isinstance(self.auth, oras.auth.TokenAuth): + if headers is not None and isinstance(self.auth, oras.auth.TokenAuth): headers.update(self.auth.get_auth_header()) response = self.session.request( method, From bd07f6ca17b49b5075f23a5d11335056b6dbd923 Mon Sep 17 00:00:00 2001 From: tarilabs Date: Sat, 13 Jul 2024 16:33:27 +0200 Subject: [PATCH 06/13] implement review request Signed-off-by: tarilabs --- oras/auth/token.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oras/auth/token.py b/oras/auth/token.py index 49563e9..7259b3c 100644 --- a/oras/auth/token.py +++ b/oras/auth/token.py @@ -48,7 +48,7 @@ def authenticate_request( original: requests.Response, headers: dict, refresh=False, - skipAnonToken=False, + skip_anon_token=False, ): """ Authenticate Request @@ -76,7 +76,7 @@ def authenticate_request( h = auth_utils.parse_auth_header(authHeaderRaw) # First try to request an anonymous token - if not skipAnonToken: + if not skip_anon_token: logger.debug("No Authorization, requesting anonymous token") anon_token = self.request_anonymous_token(h) if anon_token: From 5df68eb2234c14967731527ecf157a91a0e1c577 Mon Sep 17 00:00:00 2001 From: tarilabs Date: Sun, 22 Sep 2024 18:24:04 +0200 Subject: [PATCH 07/13] Revert "implement review request" This reverts commit 102381c5c4ae0fdf45c8a4dd26ae1765eae9b029. Signed-off-by: tarilabs --- oras/auth/token.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oras/auth/token.py b/oras/auth/token.py index 7259b3c..49563e9 100644 --- a/oras/auth/token.py +++ b/oras/auth/token.py @@ -48,7 +48,7 @@ def authenticate_request( original: requests.Response, headers: dict, refresh=False, - skip_anon_token=False, + skipAnonToken=False, ): """ Authenticate Request @@ -76,7 +76,7 @@ def authenticate_request( h = auth_utils.parse_auth_header(authHeaderRaw) # First try to request an anonymous token - if not skip_anon_token: + if not skipAnonToken: logger.debug("No Authorization, requesting anonymous token") anon_token = self.request_anonymous_token(h) if anon_token: From 27ee4c4ff3c796b20d3d620d4618099aa1e559ae Mon Sep 17 00:00:00 2001 From: tarilabs Date: Sun, 22 Sep 2024 18:24:21 +0200 Subject: [PATCH 08/13] Revert "linting" This reverts commit 1e891d2bfebe4b6520a1fe6902159198c8799d62. Signed-off-by: tarilabs --- oras/auth/token.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/oras/auth/token.py b/oras/auth/token.py index 49563e9..f8c346e 100644 --- a/oras/auth/token.py +++ b/oras/auth/token.py @@ -44,11 +44,7 @@ def reset_basic_auth(self): self.set_header("Authorization", "Basic %s" % self._basic_auth) def authenticate_request( - self, - original: requests.Response, - headers: dict, - refresh=False, - skipAnonToken=False, + self, original: requests.Response, headers: dict, refresh=False, skipAnonToken=False ): """ Authenticate Request @@ -106,7 +102,7 @@ def request_token(self, h: auth_utils.authHeader) -> bool: """ params = {} headers = {} - if self._basic_auth: # we exchange the basic auth for the token + if self._basic_auth: # we exchange the basic auth for the token headers["Authorization"] = "Basic %s" % self._basic_auth # Prepare request to retry From 06f885bc032d1b1b68f1e0618a2aa6eff29e161f Mon Sep 17 00:00:00 2001 From: tarilabs Date: Sun, 22 Sep 2024 18:24:31 +0200 Subject: [PATCH 09/13] Revert "core: if 401 on 2nd attempt, avoid anon tokens" This reverts commit 6e226672c60184cd43b6532f5a910acbf9d064ea. Signed-off-by: tarilabs --- oras/auth/token.py | 19 ++++++++----------- oras/provider.py | 14 -------------- 2 files changed, 8 insertions(+), 25 deletions(-) diff --git a/oras/auth/token.py b/oras/auth/token.py index f8c346e..0b47d7f 100644 --- a/oras/auth/token.py +++ b/oras/auth/token.py @@ -44,7 +44,7 @@ def reset_basic_auth(self): self.set_header("Authorization", "Basic %s" % self._basic_auth) def authenticate_request( - self, original: requests.Response, headers: dict, refresh=False, skipAnonToken=False + self, original: requests.Response, headers: dict, refresh=False ): """ Authenticate Request @@ -72,20 +72,17 @@ def authenticate_request( h = auth_utils.parse_auth_header(authHeaderRaw) # First try to request an anonymous token - if not skipAnonToken: - 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 + 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 - logger.debug("requesting auth token") token = self.request_token(h) if token: - logger.debug("Successfully obtained auth token!") self.token = token headers["Authorization"] = "Bearer %s" % self.token return headers, True diff --git a/oras/provider.py b/oras/provider.py index 129b75d..ff2e950 100644 --- a/oras/provider.py +++ b/oras/provider.py @@ -1002,19 +1002,5 @@ def do_request( stream=stream, verify=self._tls_verify, ) - # ...or attempt exchange anon token for auth token if 401 - if response.status_code == 401: - headers, changed = self.auth.authenticate_request( - response, headers, refresh=True, skipAnonToken=True - ) - response = self.session.request( - method, - url, - data=data, - json=json, - headers=headers, - stream=stream, - verify=self._tls_verify, - ) return response From 05b1e57f4192b0ece2d74c9752cfe90de605dd0e Mon Sep 17 00:00:00 2001 From: tarilabs Date: Sun, 22 Sep 2024 18:24:58 +0200 Subject: [PATCH 10/13] Revert "core: TokenAuth request_token fix missing auth" b/c #153 this was taken care in https://github.com/oras-project/oras-py/pull/153 This reverts commit 10e010b365e56488963ca14b6e9e08b1ea7e4a7a. Signed-off-by: tarilabs --- oras/auth/token.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/oras/auth/token.py b/oras/auth/token.py index 0b47d7f..355848d 100644 --- a/oras/auth/token.py +++ b/oras/auth/token.py @@ -99,8 +99,6 @@ def request_token(self, h: auth_utils.authHeader) -> bool: """ params = {} headers = {} - if self._basic_auth: # we exchange the basic auth for the token - headers["Authorization"] = "Basic %s" % self._basic_auth # Prepare request to retry if h.service: From bb4c2ec85f8614dc9a21e3c9ea7d9feb388ff91c Mon Sep 17 00:00:00 2001 From: tarilabs Date: Mon, 23 Sep 2024 12:52:57 +0200 Subject: [PATCH 11/13] 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 --- .gitignore | 3 +++ oras/auth/token.py | 22 ++++++++++++---------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 55b5d32..a4755ad 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 355848d..6390e55 100644 --- a/oras/auth/token.py +++ b/oras/auth/token.py @@ -71,16 +71,18 @@ 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'): + logger.debug("No Basic Auth found, 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 + + # try using auth token + logger.debug("requesting Auth Token") token = self.request_token(h) if token: self.token = token From 9c3f0030a580a8de3881df9cbd8d144d45950231 Mon Sep 17 00:00:00 2001 From: tarilabs Date: Mon, 23 Sep 2024 13:00:13 +0200 Subject: [PATCH 12/13] linting Signed-off-by: tarilabs --- oras/auth/token.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oras/auth/token.py b/oras/auth/token.py index 6390e55..7001739 100644 --- a/oras/auth/token.py +++ b/oras/auth/token.py @@ -72,7 +72,7 @@ def authenticate_request( h = auth_utils.parse_auth_header(authHeaderRaw) # if no basic auth, try by request an anonymous token - if not hasattr(self, '_basic_auth'): + if not hasattr(self, "_basic_auth"): logger.debug("No Basic Auth found, requesting anonymous token") anon_token = self.request_anonymous_token(h) if anon_token: From 7d891e96e24f16f690be4c3953a53cbd819dfe6f Mon Sep 17 00:00:00 2001 From: tarilabs Date: Mon, 23 Sep 2024 17:54:56 +0200 Subject: [PATCH 13/13] review feedback: logging Signed-off-by: tarilabs --- oras/auth/token.py | 9 ++++----- oras/auth/utils.py | 3 +++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/oras/auth/token.py b/oras/auth/token.py index 7001739..2ab436c 100644 --- a/oras/auth/token.py +++ b/oras/auth/token.py @@ -73,7 +73,6 @@ def authenticate_request( # if no basic auth, try by request an anonymous token if not hasattr(self, "_basic_auth"): - logger.debug("No Basic Auth found, requesting anonymous token") anon_token = self.request_anonymous_token(h) if anon_token: logger.debug("Successfully obtained anonymous token!") @@ -81,8 +80,7 @@ def authenticate_request( headers["Authorization"] = "Bearer %s" % self.token return headers, True - # try using auth token - logger.debug("requesting Auth Token") + # basic auth is available, try using auth token token = self.request_token(h) if token: self.token = token @@ -97,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 = {} @@ -126,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: @@ -152,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 6e3ba5d..30b8326 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: """