"""Token utility module.""" import logging import requests from keystoneclient.v2_0 import client as v2_client from keystoneclient.v3 import client as v3_client from orm_common.utils import dictator _verify = False OK_CODE = 200 _KEYSTONES = {} logger = logging.getLogger(__name__) class KeystoneNotFoundError(Exception): """Indicates that the Keystone EP of a certain LCP was not found.""" pass class TokenConf(object): """The Token Validator configuration class.""" def __init__(self, mech_id, password, rms_url, tenant_name, version): """Initialize the Token Validator configuration. :param mech_id: Username for Keystone :param password: Password for Keystone :param rms_url: The entire RMS URL, e.g. 'http://1.3.3.7:8080' :param tenant_name: The ORM tenant name :param version: Keystone version to use (a string: '3' or '2.0') """ self.mech_id = mech_id self.password = password self.rms_url = rms_url self.tenant_name = tenant_name self.version = version class TokenUser(object): """Class with details about the token user.""" def __init__(self, token): """Initialize the Token User. :param token: The token object (returned by tokens.validate) """ self.token = token.token self.user = token.user self.tenant = getattr(token, 'tenant', None) self.domain = getattr(token, 'domain', None) def get_token_user(token, conf, lcp_id=None, keystone_ep=None): """Get a token user. :param token: The token to validate :param conf: A TokenConf object :param lcp_id: The ID of the LCP associated with the Keystone instance with which the token was created. Ignored if keystone_ep is not None :param keystone_ep: The Keystone endpoint, in case we already have it :return: False if one of the tokens received (or more) is invalid, True otherwise. """ # Not using logger.error/exception because in some cases, these flows # can be completely valid if keystone_ep is None: if lcp_id is None: message = 'Received None for both keystone_ep and lcp_id!' logger.debug(message) raise ValueError(message) keystone_ep = _find_keystone_ep(conf.rms_url, lcp_id) if keystone_ep is None: message = 'Keystone EP of LCP %s not found in RMS' % (lcp_id,) logger.debug(message) logger.critical( 'CRITICAL|CON{}KEYSTONE002|X-Auth-Region: {} is not ' 'reachable (not found in RMS)'.format( dictator.get('service_name', 'ORM'), lcp_id)) raise KeystoneNotFoundError(message) if conf.version == '3': client = v3_client elif conf.version == '2.0': client = v2_client else: message = 'Invalid Keystone version: %s' % (conf.version,) logger.debug(message) raise ValueError(message) keystone = _get_keystone_client(client, conf, keystone_ep, lcp_id) try: token_info = keystone.tokens.validate(token) logger.debug('User token found in Keystone') return TokenUser(token_info) # Other exceptions raised by validate() are critical errors, # so instead of returning False, we'll just let them propagate except client.exceptions.NotFound: logger.debug('User token not found in Keystone! Make sure that it is ' 'correct and that it has not expired yet') return None def _find_keystone_ep(rms_url, lcp_name): """Get the Keystone EP from RMS. :param rms_url: RMS server URL :param lcp_name: The LCP name :return: Keystone EP (string), None if it was not found """ if not rms_url: message = 'Invalid RMS URL: %s' % (rms_url,) logger.debug(message) raise ValueError(message) logger.debug( 'Looking for Keystone EP of LCP {} using RMS URL {}'.format( lcp_name, rms_url)) response = requests.get('%s/v2/orm/regions?regionname=%s' % ( rms_url, lcp_name, ), verify=_verify) if response.status_code != OK_CODE: # The LCP was not found in RMS logger.debug('Received bad response code from RMS: {}'.format( response.status_code)) return None lcp = response.json() try: for endpoint in lcp['regions'][0]['endpoints']: if endpoint['type'] == 'identity': return endpoint['publicURL'] except KeyError: logger.debug('Response from RMS came in an unsupported format. ' 'Make sure that you are using RMS 3.5') return None # Keystone EP not found in the response logger.debug('No identity endpoint was found in the response from RMS') return None def _does_user_have_role(keystone, version, user, role, location): """Check whether a user has a role. :param keystone: The Keystone client to use :param version: Keystone version :param user: A dict that represents the user in question :param role: The role to check whether the user has :param location: Keystone role location :return: True if the user has the requested role, False otherwise. :raise: client.exceptions.NotFound when the requested role does not exist, ValueError when the version is 2.0 but the location is not 'tenant' """ location = dict(location) if version == '3': role = keystone.roles.find(name=role) try: return keystone.roles.check(role, user=user['user']['id'], **location) except v3_client.exceptions.NotFound: return False except KeyError: # Shouldn't be raised when using Keystone's v3/v2.0 API, but let's # play on the safe side logger.debug('The user parameter came in a wrong format!') return False elif version == '2.0': # v2.0 supports tenants only if location.keys()[0] != 'tenant': raise ValueError( 'Using Keystone v2.0, expected "tenant", received: "%s"' % ( location.keys()[0],)) tenant = keystone.tenants.find(name=location['tenant']) # v2.0 does not enable us to check for a specific role (unlike v3) role_list = keystone.roles.roles_for_user(user.user['id'], tenant=tenant) return any([user_role.name == role for user_role in role_list]) def _get_keystone_client(client, conf, keystone_ep, lcp_id): """Get the Keystone client. :param client: keystoneclient package to use :param conf: Token conf :param keystone_ep: The Keystone endpoint that RMS returned :param lcp_id: The region ID :return: The instance of Keystone client to use """ global _KEYSTONES try: if keystone_ep not in _KEYSTONES: # Instantiate the Keystone client according to the configuration _KEYSTONES[keystone_ep] = client.Client( username=conf.mech_id, password=conf.password, tenant_name=conf.tenant_name, auth_url=keystone_ep + '/v' + conf.version) return _KEYSTONES[keystone_ep] except Exception: logger.critical( 'CRITICAL|CON{}KEYSTONE001|Cannot reach Keystone EP: {} of ' 'region {}. Please contact Keystone team.'.format( dictator.get('service_name', 'ORM'), keystone_ep, lcp_id)) raise def is_token_valid(token_to_validate, lcp_id, conf, required_role=None, role_location=None): """Validate a token. :param token_to_validate: The token to validate :param lcp_id: The ID of the LCP associated with the Keystone instance with which the token was created :param conf: A TokenConf object :param required_role: The required role for privileged actions, e.g. 'admin' (optional). :param role_location: The Keystone role location (a dict whose single key is either 'domain' or 'tenant', whose value is the location name) :return: False if one of the tokens received (or more) is invalid, True otherwise. :raise: KeystoneNotFoundError when the Keystone EP for the required LCP was not found in RMS output, client.exceptions.AuthorizationFailure when the connection with the Keystone EP could not be established, client.exceptions.EndpointNotFound when _our_ authentication (as an admin) with Keystone failed, ValueError when an invalid Keystone version was specified, ValueError when a role or a tenant was not found, ValueError when a role is required but role_location is None. """ keystone_ep = _find_keystone_ep(conf.rms_url, lcp_id) if keystone_ep is None: raise KeystoneNotFoundError('Keystone EP of LCP %s not found in RMS' % (lcp_id,)) if conf.version == '3': client = v3_client elif conf.version == '2.0': client = v2_client else: raise ValueError('Invalid Keystone version: %s' % (conf.version,)) keystone = _get_keystone_client(client, conf, keystone_ep, lcp_id) try: user = keystone.tokens.validate(token_to_validate) logger.debug('User token found in Keystone') # Other exceptions raised by validate() are critical errors, # so instead of returning False, we'll just let them propagate except client.exceptions.NotFound: logger.debug('User token not found in Keystone! Make sure that it is' 'correct and that it has not expired yet') return False if required_role is not None: if role_location is None: raise ValueError( 'A role is required but no role location was specified!') try: logger.debug('Checking role...') return _does_user_have_role(keystone, conf.version, user, required_role, role_location) except client.exceptions.NotFound: raise ValueError('Role %s or tenant %s not found!' % ( required_role, role_location,)) else: # We know that the token is valid and there's no need to enforce a # policy on this operation, so we can let the user pass logger.debug('No role to check, authentication finished successfully') return True