summaryrefslogtreecommitdiff
path: root/keystonemiddleware/auth_token/__init__.py
diff options
context:
space:
mode:
Diffstat (limited to 'keystonemiddleware/auth_token/__init__.py')
-rw-r--r--keystonemiddleware/auth_token/__init__.py223
1 files changed, 80 insertions, 143 deletions
diff --git a/keystonemiddleware/auth_token/__init__.py b/keystonemiddleware/auth_token/__init__.py
index 6041e9e..0feed6f 100644
--- a/keystonemiddleware/auth_token/__init__.py
+++ b/keystonemiddleware/auth_token/__init__.py
@@ -217,8 +217,8 @@ object is stored.
"""
-import binascii
import copy
+import re
from keystoneauth1 import access
from keystoneauth1 import adapter
@@ -226,8 +226,6 @@ from keystoneauth1 import discover
from keystoneauth1 import exceptions as ksa_exceptions
from keystoneauth1 import loading
from keystoneauth1.loading import session as session_loading
-from keystoneclient.common import cms
-from keystoneclient import exceptions as ksc_exceptions
import oslo_cache
from oslo_config import cfg
from oslo_log import log as logging
@@ -242,7 +240,6 @@ from keystonemiddleware.auth_token import _exceptions as ksm_exceptions
from keystonemiddleware.auth_token import _identity
from keystonemiddleware.auth_token import _opts
from keystonemiddleware.auth_token import _request
-from keystonemiddleware.auth_token import _signing_dir
from keystonemiddleware.auth_token import _user_plugin
from keystonemiddleware.i18n import _
@@ -281,6 +278,26 @@ def list_opts():
return [(g, copy.deepcopy(o)) for g, o in AUTH_TOKEN_OPTS]
+def _path_matches(request_path, path_pattern):
+ # The fnmatch module doesn't provide the ability to match * versus **,
+ # so convert to regex.
+ token_regex = (r'(?P<tag>{[^}]*})|' # {tag} # nosec
+ r'(?P<wild>\*(?=$|[^\*]))|' # *
+ r'(?P<rec_wild>\*\*)|' # **
+ r'(?P<literal>[^{}\*])') # anything else
+ path_regex = ''
+ for match in re.finditer(token_regex, path_pattern):
+ token = match.groupdict()
+ if token['tag'] or token['wild']:
+ path_regex += r'[^\/]+'
+ if token['rec_wild']:
+ path_regex += '.*'
+ if token['literal']:
+ path_regex += token['literal']
+ path_regex = r'^%s$' % path_regex
+ return re.match(path_regex, request_path)
+
+
class _BIND_MODE(object):
DISABLED = 'disabled'
PERMISSIVE = 'permissive'
@@ -289,16 +306,6 @@ class _BIND_MODE(object):
KERBEROS = 'kerberos'
-def _uncompress_pkiz(token):
- # TypeError If the signed_text is not zlib compressed binascii.Error if
- # signed_text has incorrect base64 padding (py34)
-
- try:
- return cms.pkiz_uncompress(token)
- except (TypeError, binascii.Error):
- raise ksm_exceptions.InvalidToken(token)
-
-
class BaseAuthProtocol(object):
"""A base class for AuthProtocol token checking implementations.
@@ -315,13 +322,15 @@ class BaseAuthProtocol(object):
log=_LOG,
enforce_token_bind=_BIND_MODE.PERMISSIVE,
service_token_roles=None,
- service_token_roles_required=False):
+ service_token_roles_required=False,
+ service_type=None):
self.log = log
self._app = app
self._enforce_token_bind = enforce_token_bind
self._service_token_roles = set(service_token_roles or [])
self._service_token_roles_required = service_token_roles_required
self._service_token_warning_emitted = False
+ self._service_type = service_type
@webob.dec.wsgify(RequestClass=_request._AuthTokenRequest)
def __call__(self, req):
@@ -402,6 +411,8 @@ class BaseAuthProtocol(object):
allow_expired=allow_expired)
self._validate_token(user_auth_ref,
allow_expired=allow_expired)
+ if user_auth_ref.version != 'v2.0':
+ self.validate_allowed_request(request, data['token'])
if not request.service_token:
self._confirm_token_bind(user_auth_ref, request)
except ksm_exceptions.InvalidToken:
@@ -530,13 +541,57 @@ class BaseAuthProtocol(object):
{'bind_type': bind_type, 'identifier': identifier})
self._invalid_user_token()
+ def validate_allowed_request(self, request, token):
+ self.log.debug("Validating token access rules against request")
+ app_cred = token.get('application_credential')
+ if not app_cred:
+ return
+ access_rules = app_cred.get('access_rules')
+ if access_rules is None:
+ return
+ if hasattr(self, '_conf'):
+ my_service_type = self._conf.get('service_type')
+ else:
+ my_service_type = self._service_type
+ if not my_service_type:
+ self.log.warning('Cannot validate request with restricted'
+ ' access rules. Set service_type in'
+ ' [keystone_authtoken] to allow access rule'
+ ' validation.')
+ raise ksm_exceptions.InvalidToken(_('Token authorization failed'))
+ # token can always be validated regardless of access rules
+ if (my_service_type == 'identity' and
+ request.method == 'GET' and
+ request.path.endswith('/v3/auth/tokens')):
+ return
+ catalog = token['catalog']
+ # validate service type is in catalog
+ catalog_svcs = [s for s in catalog if s['type'] == my_service_type]
+ if len(catalog_svcs) == 0:
+ self.log.warning('Cannot validate request with restricted'
+ ' access rules. service_type in'
+ ' [keystone_authtoken] is not a valid service'
+ ' type in the catalog.')
+ raise ksm_exceptions.InvalidToken(_('Token authorization failed'))
+ if request.service_token:
+ # The request may not match an allowed request, but the presence
+ # of the service token indicates this is a chain of requests and
+ # hence this request was not user-facing
+ return
+ for access_rule in access_rules:
+ method = access_rule['method']
+ path = access_rule['path']
+ service = access_rule['service']
+ if request.method == method and \
+ service == my_service_type and \
+ _path_matches(request.path, path):
+ return
+ raise ksm_exceptions.InvalidToken(_('Token authorization failed'))
+
class AuthProtocol(BaseAuthProtocol):
"""Middleware that handles authenticating client calls."""
- _SIGNING_CERT_FILE_NAME = 'signing_cert.pem'
- _SIGNING_CA_FILE_NAME = 'cacert.pem'
-
def __init__(self, app, conf):
log = logging.getLogger(conf.get('log_name', __name__))
log.info('Starting Keystone auth_token middleware')
@@ -568,9 +623,7 @@ class AuthProtocol(BaseAuthProtocol):
self._delay_auth_decision = self._conf.get('delay_auth_decision')
self._include_service_catalog = self._conf.get(
'include_service_catalog')
- self._hash_algorithms = self._conf.get('hash_algorithms')
self._interface = self._conf.get('interface')
-
self._auth = self._create_auth_plugin()
self._session = self._create_session()
self._identity_server = self._create_identity_server()
@@ -590,9 +643,6 @@ class AuthProtocol(BaseAuthProtocol):
self._www_authenticate_uri = \
self._identity_server.www_authenticate_uri
- self._signing_directory = _signing_dir.SigningDirectory(
- directory_name=self._conf.get('signing_dir'), log=self.log)
-
self._token_cache = self._token_cache_factory()
def process_request(self, request):
@@ -674,37 +724,6 @@ class AuthProtocol(BaseAuthProtocol):
header_val = 'Keystone uri="%s"' % self._www_authenticate_uri
return [('WWW-Authenticate', header_val)]
- def _token_hashes(self, token):
- """Generate a list of hashes that the current token may be cached as.
-
- The first element of this list is the preferred algorithm and is what
- new cache values should be saved as.
-
- :param str token: The token being presented by a user.
-
- :returns: list of str token hashes.
- """
- if cms.is_asn1_token(token) or cms.is_pkiz(token):
- return list(cms.cms_hash_token(token, mode=algo)
- for algo in self._hash_algorithms)
- else:
- return [token]
-
- def _cache_get_hashes(self, token_hashes):
- """Check if the token is cached already.
-
- Functions takes a list of hashes that might be in the cache and matches
- the first one that is present. If nothing is found in the cache it
- returns None.
-
- :returns: token data if found else None.
- """
- for token in token_hashes:
- cached = self._token_cache.get(token)
-
- if cached:
- return cached
-
def fetch_token(self, token, allow_expired=False):
"""Retrieve a token from either a PKI bundle or the identity server.
@@ -713,11 +732,8 @@ class AuthProtocol(BaseAuthProtocol):
:raises exc.InvalidToken: if token is rejected
"""
data = None
- token_hashes = None
-
try:
- token_hashes = self._token_hashes(token)
- cached = self._cache_get_hashes(token_hashes)
+ cached = self._token_cache.get(token)
if cached:
if cached == _CACHE_INVALID_INDICATOR:
@@ -733,13 +749,11 @@ class AuthProtocol(BaseAuthProtocol):
data = cached
else:
- data = self._validate_offline(token, token_hashes)
- if not data:
- data = self._identity_server.verify_token(
- token,
- allow_expired=allow_expired)
+ data = self._identity_server.verify_token(
+ token,
+ allow_expired=allow_expired)
- self._token_cache.set(token_hashes[0], data)
+ self._token_cache.set(token, data)
except (ksa_exceptions.ConnectFailure,
ksa_exceptions.DiscoveryFailure,
@@ -755,9 +769,7 @@ class AuthProtocol(BaseAuthProtocol):
'The Keystone service is temporarily unavailable.')
except ksm_exceptions.InvalidToken:
self.log.debug('Token validation failure.', exc_info=True)
- if token_hashes:
- self._token_cache.set(token_hashes[0],
- _CACHE_INVALID_INDICATOR)
+ self._token_cache.set(token, _CACHE_INVALID_INDICATOR)
self.log.warning('Authorization failed for token')
raise
except ksa_exceptions.EndpointNotFound:
@@ -767,34 +779,6 @@ class AuthProtocol(BaseAuthProtocol):
return data
- def _validate_offline(self, token, token_hashes):
- if cms.is_pkiz(token):
- token_data = _uncompress_pkiz(token)
- inform = cms.PKIZ_CMS_FORM
- elif cms.is_asn1_token(token):
- token_data = cms.token_to_cms(token)
- inform = cms.PKI_ASN1_FORM
- else:
- # Can't do offline validation for this type of token.
- return
-
- try:
- verified = self._cms_verify(token_data, inform)
- except ksc_exceptions.CertificateConfigError:
- self.log.warning('Fetch certificate config failed, '
- 'fallback to online validation.')
- else:
- self.log.warning('auth_token middleware received a PKI/Z token. '
- 'This form of token is deprecated and has been '
- 'removed from keystone server and will be '
- 'removed from auth_token middleware in the Rocky '
- 'release. Please contact your administrator '
- 'about upgrading keystone and the token format.')
-
- data = jsonutils.loads(verified)
-
- return data
-
def _validate_token(self, auth_ref, **kwargs):
super(AuthProtocol, self)._validate_token(auth_ref, **kwargs)
@@ -802,53 +786,6 @@ class AuthProtocol(BaseAuthProtocol):
msg = _('Unable to determine service tenancy.')
raise ksm_exceptions.InvalidToken(msg)
- def _cms_verify(self, data, inform=cms.PKI_ASN1_FORM):
- """Verify the signature of the provided data's IAW CMS syntax.
-
- If either of the certificate files might be missing, fetch them and
- retry.
- """
- def verify():
- try:
- signing_cert_path = self._signing_directory.calc_path(
- self._SIGNING_CERT_FILE_NAME)
- signing_ca_path = self._signing_directory.calc_path(
- self._SIGNING_CA_FILE_NAME)
- return cms.cms_verify(data, signing_cert_path,
- signing_ca_path,
- inform=inform).decode('utf-8')
- except (ksc_exceptions.CMSError,
- cms.subprocess.CalledProcessError) as err:
- self.log.warning('Verify error: %s', err)
- msg = _('Token authorization failed')
- raise ksm_exceptions.InvalidToken(msg)
-
- try:
- return verify()
- except ksc_exceptions.CertificateConfigError:
- # the certs might be missing; unconditionally fetch to avoid racing
- self._fetch_signing_cert()
- self._fetch_ca_cert()
-
- try:
- # retry with certs in place
- return verify()
- except ksc_exceptions.CertificateConfigError as err:
- # if this is still occurring, something else is wrong and we
- # need err.output to identify the problem
- self.log.error('CMS Verify output: %s', err.output)
- raise
-
- def _fetch_signing_cert(self):
- self._signing_directory.write_file(
- self._SIGNING_CERT_FILE_NAME,
- self._identity_server.fetch_signing_cert())
-
- def _fetch_ca_cert(self):
- self._signing_directory.write_file(
- self._SIGNING_CA_FILE_NAME,
- self._identity_server.fetch_ca_cert())
-
def _create_auth_plugin(self):
# NOTE(jamielennox): Ideally this would use load_from_conf_options
# however that is not possible because we have to support the override
@@ -885,7 +822,7 @@ class AuthProtocol(BaseAuthProtocol):
plugin_opts = loading.get_auth_plugin_conf_options(plugin_loader)
self._conf.oslo_conf_obj.register_opts(plugin_opts, group=group)
- getter = lambda opt: self._conf.get(opt.dest, group=group)
+ getter = lambda opt: self._conf.get(opt.dest, group=group) # noqa
return plugin_loader.load_from_options_getter(getter)
def _create_session(self, **kwargs):