diff options
Diffstat (limited to 'keystonemiddleware/auth_token/__init__.py')
-rw-r--r-- | keystonemiddleware/auth_token/__init__.py | 223 |
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): |