Improve bearer auth handling

We can share tokens across threads for scope so if we are fetching
multiple layers of the same container, let's reuse the token rather than
duplicating the token request. Additionally we can verify if a token
needs to be refreshed based on the expiration time.

Change-Id: I4a3149b08013f493e13b592f064e3ff2ed4074f7
(cherry picked from commit f52b1e1a46)
This commit is contained in:
Alex Schultz 2020-09-03 13:29:31 -06:00 committed by Emilien Macchi
parent fc46fba50d
commit 4fa8c87a96
4 changed files with 105 additions and 23 deletions

View File

@ -32,6 +32,9 @@ import tempfile
import tenacity
import yaml
from datetime import datetime
from dateutil.parser import parse as dt_parse
from dateutil.tz import tzlocal
from oslo_concurrency import processutils
from oslo_log import log as logging
from tripleo_common.actions import ansible
@ -300,6 +303,69 @@ class RegistrySessionHelper(object):
request=request_response)
return request_response
@staticmethod
def get_cached_bearer_token(lock=None, scope=None):
if not lock:
return None
with lock.get_lock():
data = lock.sessions().get(scope)
if data and data.get('issued_at'):
token_time = dt_parse(data.get('issued_at'))
now = datetime.now(tzlocal())
if (now - token_time).seconds < data.get('expires_in'):
return data['token']
return None
@staticmethod
def get_bearer_token(session, lock=None, username=None, password=None,
realm=None, service=None, scope=None):
cached_token = RegistrySessionHelper.get_cached_bearer_token(lock,
scope)
if cached_token:
return cached_token
auth = None
token_param = {}
if service:
token_param['service'] = service
if scope:
token_param['scope'] = scope
if username:
auth = requests.auth.HTTPBasicAuth(username, password)
auth_req = session.get(realm, params=token_param, auth=auth,
timeout=30)
auth_req.raise_for_status()
resp = auth_req.json()
if lock and 'token' in resp:
with lock.get_lock():
lock.sessions().update({scope: resp})
elif lock and 'token' not in resp:
raise Exception('Invalid auth response, no token provide')
hash_request_id = hashlib.sha1(str(auth_req.url).encode())
LOG.debug(
'Session authenticated: id {}'.format(
hash_request_id.hexdigest()
)
)
return resp['token']
@staticmethod
def parse_www_authenticate(header):
auth_type = None
auth_type_match = re.search('^([A-Za-z]*) ', header)
if auth_type_match:
auth_type = auth_type_match.group(1)
if not auth_type:
return (None, None, None)
realm = None
service = None
if 'realm=' in header:
realm = re.search('realm="(.*?)"', header).group(1)
if 'service=' in header:
service = re.search('service="(.*?)"', header).group(1)
return (auth_type, realm, service)
@staticmethod
@tenacity.retry( # Retry up to 5 times with longer time for rate limit
reraise=True,
@ -669,6 +735,8 @@ class BaseImageUploader(object):
session=None):
netloc = image_url.netloc
image, tag = self._image_tag_from_url(image_url)
scope = 'repository:%s:pull' % image[1:]
self.is_insecure_registry(registry_host=netloc)
url = self._build_url(image_url, path='/')
verify = (netloc not in self.no_verify_registries)
@ -678,6 +746,14 @@ class BaseImageUploader(object):
session.headers.pop('Authorization', None)
session.verify = verify
cached_token = None
if getattr(self, 'lock', None):
cached_token = RegistrySessionHelper.\
get_cached_bearer_token(self.lock, scope)
if cached_token:
session.headers['Authorization'] = 'Bearer %s' % cached_token
r = session.get(url, timeout=30)
LOG.debug('%s status code %s' % (url, r.status_code))
if r.status_code == 200:
@ -692,22 +768,22 @@ class BaseImageUploader(object):
www_auth = r.headers['www-authenticate']
token_param = {}
if www_auth.startswith('Bearer '):
LOG.debug('Using bearer token auth')
realm = re.search('realm="(.*?)"', www_auth).group(1)
if 'service=' in www_auth:
token_param['service'] = re.search(
'service="(.*?)"', www_auth).group(1)
token_param['scope'] = 'repository:%s:pull' % image[1:]
(auth_type, realm, service) = \
RegistrySessionHelper.parse_www_authenticate(www_auth)
if username:
auth = requests_auth.HTTPBasicAuth(username, password)
LOG.debug('Token parameters: params {}'.format(token_param))
rauth = session.get(realm, params=token_param, auth=auth,
timeout=30)
rauth.raise_for_status()
auth_header = 'Bearer %s' % rauth.json()['token']
elif www_auth.startswith('Basic '):
if auth_type and auth_type.lower() == 'bearer':
LOG.debug('Using bearer token auth')
if getattr(self, 'lock', None):
lock = self.lock
else:
lock = None
token = RegistrySessionHelper.get_bearer_token(session, lock=lock,
username=username,
password=password,
realm=realm,
service=service,
scope=scope)
elif auth_type and auth_type.lower() == 'basic':
LOG.debug('Using basic auth')
if not username or not password:
raise Exception('Authentication credentials required for '
@ -715,19 +791,20 @@ class BaseImageUploader(object):
auth = requests_auth.HTTPBasicAuth(username, password)
rauth = session.get(url, params=token_param, auth=auth, timeout=30)
rauth.raise_for_status()
auth_header = (
'Basic %s' % base64.b64encode(
token = (
base64.b64encode(
bytes(username + ':' + password, 'utf-8')).decode('ascii')
)
hash_request_id = hashlib.sha1(str(rauth.url).encode())
LOG.debug(
'Session authenticated: id {}'.format(
hash_request_id.hexdigest()
)
)
else:
raise ImageUploaderException(
'Unknown www-authenticate value: %s' % www_auth)
hash_request_id = hashlib.sha1(str(rauth.url).encode())
LOG.debug(
'Session authenticated: id {}'.format(
hash_request_id.hexdigest()
)
)
auth_header = '%s %s' % (auth_type, token)
session.headers['Authorization'] = auth_header
setattr(session, 'reauthenticate', self.authenticate)

View File

@ -19,3 +19,6 @@ class BaseLock(object):
def objects(self):
return self._objects
def sessions(self):
return self._sessions

View File

@ -28,3 +28,4 @@ class ProcessLock(base.BaseLock):
def __init__(self):
self._lock = self._mgr.Lock()
self._objects = self._mgr.list()
self._sessions = self._mgr.dict()

View File

@ -20,3 +20,4 @@ class ThreadingLock(base.BaseLock):
def __init__(self):
self._lock = threading.Lock()
self._objects = []
self._sessions = {}