"Fossies" - the Fresh Open Source Software Archive

Member "keystone-17.0.0/keystone/token/provider.py" (13 May 2020, 12256 Bytes) of package /linux/misc/openstack/keystone-17.0.0.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "provider.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 16.0.1_vs_17.0.0.

    1 # Copyright 2012 OpenStack Foundation
    2 #
    3 # Licensed under the Apache License, Version 2.0 (the "License"); you may
    4 # not use this file except in compliance with the License. You may obtain
    5 # a copy of the License at
    6 #
    7 #      http://www.apache.org/licenses/LICENSE-2.0
    8 #
    9 # Unless required by applicable law or agreed to in writing, software
   10 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
   11 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
   12 # License for the specific language governing permissions and limitations
   13 # under the License.
   14 
   15 """Token provider interface."""
   16 
   17 import base64
   18 import datetime
   19 import uuid
   20 
   21 from oslo_log import log
   22 from oslo_utils import timeutils
   23 
   24 from keystone.common import cache
   25 from keystone.common import manager
   26 from keystone.common import provider_api
   27 from keystone.common import utils
   28 import keystone.conf
   29 from keystone import exception
   30 from keystone.federation import constants
   31 from keystone.i18n import _
   32 from keystone.models import token_model
   33 from keystone import notifications
   34 
   35 
   36 CONF = keystone.conf.CONF
   37 LOG = log.getLogger(__name__)
   38 PROVIDERS = provider_api.ProviderAPIs
   39 
   40 TOKENS_REGION = cache.create_region(name='tokens')
   41 MEMOIZE_TOKENS = cache.get_memoization_decorator(
   42     group='token',
   43     region=TOKENS_REGION)
   44 
   45 # NOTE(morganfainberg): This is for compatibility in case someone was relying
   46 # on the old location of the UnsupportedTokenVersionException for their code.
   47 UnsupportedTokenVersionException = exception.UnsupportedTokenVersionException
   48 
   49 # supported token versions
   50 V3 = token_model.V3
   51 VERSIONS = token_model.VERSIONS
   52 
   53 # minimum access rules support
   54 ACCESS_RULES_MIN_VERSION = token_model.ACCESS_RULES_MIN_VERSION
   55 
   56 
   57 def default_expire_time():
   58     """Determine when a fresh token should expire.
   59 
   60     Expiration time varies based on configuration (see ``[token] expiration``).
   61 
   62     :returns: a naive UTC datetime.datetime object
   63 
   64     """
   65     expire_delta = datetime.timedelta(seconds=CONF.token.expiration)
   66     expires_at = timeutils.utcnow() + expire_delta
   67     return expires_at.replace(microsecond=0)
   68 
   69 
   70 def random_urlsafe_str():
   71     """Generate a random URL-safe string.
   72 
   73     :rtype: str
   74     """
   75     # chop the padding (==) off the end of the encoding to save space
   76     return base64.urlsafe_b64encode(uuid.uuid4().bytes)[:-2].decode('utf-8')
   77 
   78 
   79 class Manager(manager.Manager):
   80     """Default pivot point for the token provider backend.
   81 
   82     See :mod:`keystone.common.manager.Manager` for more details on how this
   83     dynamically calls the backend.
   84 
   85     """
   86 
   87     driver_namespace = 'keystone.token.provider'
   88     _provides_api = 'token_provider_api'
   89 
   90     V3 = V3
   91     VERSIONS = VERSIONS
   92 
   93     def __init__(self):
   94         super(Manager, self).__init__(CONF.token.provider)
   95         self._register_callback_listeners()
   96 
   97     def _register_callback_listeners(self):
   98         # This is used by the @dependency.provider decorator to register the
   99         # provider (token_provider_api) manager to listen for trust deletions.
  100         callbacks = {
  101             notifications.ACTIONS.deleted: [
  102                 ['OS-TRUST:trust', self._drop_token_cache],
  103                 ['user', self._drop_token_cache],
  104                 ['domain', self._drop_token_cache],
  105             ],
  106             notifications.ACTIONS.disabled: [
  107                 ['user', self._drop_token_cache],
  108                 ['domain', self._drop_token_cache],
  109                 ['project', self._drop_token_cache],
  110             ],
  111             notifications.ACTIONS.internal: [
  112                 [notifications.INVALIDATE_TOKEN_CACHE,
  113                     self._drop_token_cache],
  114             ]
  115         }
  116 
  117         for event, cb_info in callbacks.items():
  118             for resource_type, callback_fns in cb_info:
  119                 notifications.register_event_callback(event, resource_type,
  120                                                       callback_fns)
  121 
  122     def _drop_token_cache(self, service, resource_type, operation, payload):
  123         """Invalidate the entire token cache.
  124 
  125         This is a handy private utility method that should be used when
  126         consuming notifications that signal invalidating the token cache.
  127 
  128         """
  129         if CONF.token.cache_on_issue or CONF.token.caching:
  130             TOKENS_REGION.invalidate()
  131 
  132     def check_revocation_v3(self, token):
  133         token_values = self.revoke_api.model.build_token_values(token)
  134         PROVIDERS.revoke_api.check_token(token_values)
  135 
  136     def check_revocation(self, token):
  137         return self.check_revocation_v3(token)
  138 
  139     def validate_token(self, token_id, window_seconds=0,
  140                        access_rules_support=None):
  141         if not token_id:
  142             raise exception.TokenNotFound(_('No token in the request'))
  143 
  144         try:
  145             token = self._validate_token(token_id)
  146             self._is_valid_token(token, window_seconds=window_seconds)
  147             self._validate_token_access_rules(token, access_rules_support)
  148             return token
  149         except exception.Unauthorized as e:
  150             LOG.debug('Unable to validate token: %s', e)
  151             raise exception.TokenNotFound(token_id=token_id)
  152 
  153     @MEMOIZE_TOKENS
  154     def _validate_token(self, token_id):
  155         (user_id, methods, audit_ids, system, domain_id,
  156             project_id, trust_id, federated_group_ids, identity_provider_id,
  157             protocol_id, access_token_id, app_cred_id, issued_at,
  158             expires_at) = self.driver.validate_token(token_id)
  159 
  160         token = token_model.TokenModel()
  161         token.user_id = user_id
  162         token.methods = methods
  163         if len(audit_ids) > 1:
  164             token.parent_audit_id = audit_ids.pop()
  165         token.audit_id = audit_ids.pop()
  166         token.system = system
  167         token.domain_id = domain_id
  168         token.project_id = project_id
  169         token.trust_id = trust_id
  170         token.access_token_id = access_token_id
  171         token.application_credential_id = app_cred_id
  172         token.expires_at = expires_at
  173         if federated_group_ids is not None:
  174             token.is_federated = True
  175             token.identity_provider_id = identity_provider_id
  176             token.protocol_id = protocol_id
  177             token.federated_groups = federated_group_ids
  178 
  179         token.mint(token_id, issued_at)
  180         return token
  181 
  182     def _is_valid_token(self, token, window_seconds=0):
  183         """Verify the token is valid format and has not expired."""
  184         current_time = timeutils.normalize_time(timeutils.utcnow())
  185 
  186         try:
  187             expiry = timeutils.parse_isotime(token.expires_at)
  188             expiry = timeutils.normalize_time(expiry)
  189 
  190             # add a window in which you can fetch a token beyond expiry
  191             expiry += datetime.timedelta(seconds=window_seconds)
  192 
  193         except Exception:
  194             LOG.exception('Unexpected error or malformed token '
  195                           'determining token expiry: %s', token)
  196             raise exception.TokenNotFound(_('Failed to validate token'))
  197 
  198         if current_time < expiry:
  199             self.check_revocation(token)
  200             # Token has not expired and has not been revoked.
  201             return None
  202         else:
  203             raise exception.TokenNotFound(_('Failed to validate token'))
  204 
  205     def _validate_token_access_rules(self, token, access_rules_support=None):
  206         if token.application_credential_id:
  207             app_cred_api = PROVIDERS.application_credential_api
  208             app_cred = app_cred_api.get_application_credential(
  209                 token.application_credential_id)
  210             if (app_cred.get('access_rules') is not None and
  211                 (not access_rules_support or
  212                  (float(access_rules_support) < ACCESS_RULES_MIN_VERSION))):
  213                 LOG.exception('Attempted to use application credential'
  214                               ' access rules with a middleware that does not'
  215                               ' understand them. You must upgrade'
  216                               ' keystonemiddleware on all services that'
  217                               ' accept application credentials as an'
  218                               ' authentication method.')
  219                 raise exception.TokenNotFound(_('Failed to validate token'))
  220 
  221     def issue_token(self, user_id, method_names, expires_at=None,
  222                     system=None, project_id=None, domain_id=None,
  223                     auth_context=None, trust_id=None, app_cred_id=None,
  224                     parent_audit_id=None):
  225 
  226         # NOTE(lbragstad): Grab a blank token object and use composition to
  227         # build the token according to the authentication and authorization
  228         # context. This cuts down on the amount of logic we have to stuff into
  229         # the TokenModel's __init__() method.
  230         token = token_model.TokenModel()
  231         token.methods = method_names
  232         token.system = system
  233         token.domain_id = domain_id
  234         token.project_id = project_id
  235         token.trust_id = trust_id
  236         token.application_credential_id = app_cred_id
  237         token.audit_id = random_urlsafe_str()
  238         token.parent_audit_id = parent_audit_id
  239 
  240         if auth_context:
  241             if constants.IDENTITY_PROVIDER in auth_context:
  242                 token.is_federated = True
  243                 token.protocol_id = auth_context[constants.PROTOCOL]
  244                 idp_id = auth_context[constants.IDENTITY_PROVIDER]
  245                 if isinstance(idp_id, bytes):
  246                     idp_id = idp_id.decode('utf-8')
  247                 token.identity_provider_id = idp_id
  248                 token.user_id = auth_context['user_id']
  249                 token.federated_groups = [
  250                     {'id': group} for group in auth_context['group_ids']
  251                 ]
  252 
  253             if 'access_token_id' in auth_context:
  254                 token.access_token_id = auth_context['access_token_id']
  255 
  256         if not token.user_id:
  257             token.user_id = user_id
  258 
  259         token.user_domain_id = token.user['domain_id']
  260 
  261         if isinstance(expires_at, datetime.datetime):
  262             token.expires_at = utils.isotime(expires_at, subsecond=True)
  263         if isinstance(expires_at, str):
  264             token.expires_at = expires_at
  265         elif not expires_at:
  266             token.expires_at = utils.isotime(
  267                 default_expire_time(), subsecond=True
  268             )
  269 
  270         token_id, issued_at = self.driver.generate_id_and_issued_at(token)
  271         token.mint(token_id, issued_at)
  272 
  273         # cache the token object and with ID
  274         if CONF.token.cache_on_issue or CONF.token.caching:
  275             # NOTE(amakarov): here and above TOKENS_REGION is to be passed
  276             # to serve as required positional "self" argument. It's ignored,
  277             # so I've put it here for convenience - any placeholder is fine.
  278             self._validate_token.set(token, self, token.id)
  279 
  280         return token
  281 
  282     def invalidate_individual_token_cache(self, token_id):
  283         # NOTE(morganfainberg): invalidate takes the exact same arguments as
  284         # the normal method, this means we need to pass "self" in (which gets
  285         # stripped off).
  286 
  287         # FIXME(morganfainberg): Does this cache actually need to be
  288         # invalidated? We maintain a cached revocation list, which should be
  289         # consulted before accepting a token as valid.  For now we will
  290         # do the explicit individual token invalidation.
  291 
  292         self._validate_token.invalidate(self, token_id)
  293 
  294     def revoke_token(self, token_id, revoke_chain=False):
  295         token = self.validate_token(token_id)
  296 
  297         project_id = token.project_id if token.project_scoped else None
  298         domain_id = token.domain_id if token.domain_scoped else None
  299 
  300         if revoke_chain:
  301             PROVIDERS.revoke_api.revoke_by_audit_chain_id(
  302                 token.parent_audit_id, project_id=project_id,
  303                 domain_id=domain_id
  304             )
  305         else:
  306             PROVIDERS.revoke_api.revoke_by_audit_id(token.audit_id)
  307 
  308         # FIXME(morganfainberg): Does this cache actually need to be
  309         # invalidated? We maintain a cached revocation list, which should be
  310         # consulted before accepting a token as valid.  For now we will
  311         # do the explicit individual token invalidation.
  312         self.invalidate_individual_token_cache(token_id)