def __get_token_userpass(self): """ Sends a request to get an auth token from the server and stores it as a class attribute. Uses username/password. :returns: True if the token was successfully received. False otherwise. """ headers = {'X-Rucio-Username': self.creds['username'], 'X-Rucio-Password': self.creds['password']} url = build_url(self.auth_host, path='auth/userpass') result = self._send_request(url, headers=headers, get_token=True) if not result: # result is either None or not OK. if isinstance(result, Response): if 'ExceptionClass' in result.headers and result.headers['ExceptionClass']: if 'ExceptionMessage' in result.headers and result.headers['ExceptionMessage']: raise CannotAuthenticate('%s: %s' % (result.headers['ExceptionClass'], result.headers['ExceptionMessage'])) else: raise CannotAuthenticate(result.headers["ExceptionClass"]) elif result.text: raise CannotAuthenticate(result.text) self.logger.error('Cannot retrieve authentication token!') return False if result.status_code != codes.ok: # pylint: disable-msg=E1101 exc_cls, exc_msg = self._get_exception(headers=result.headers, status_code=result.status_code, data=result.content) raise exc_cls(exc_msg) self.auth_token = result.headers['x-rucio-auth-token'] return True
def __get_token(self): """ Calls the corresponding method to receive an auth token depending on the auth type. To be used if a 401 - Unauthorized error is received. """ retry = 0 LOG.debug('get a new token') while retry <= self.AUTH_RETRIES: if self.auth_type == 'userpass': if not self.__get_token_userpass(): raise CannotAuthenticate('userpass authentication failed') elif self.auth_type == 'x509' or self.auth_type == 'x509_proxy': if not self.__get_token_x509(): raise CannotAuthenticate('x509 authentication failed') elif self.auth_type == 'gss': if not self.__get_token_gss(): raise CannotAuthenticate('kerberos authentication failed') else: raise CannotAuthenticate('auth type \'%s\' not supported' % self.auth_type) if self.auth_token is not None: self.__write_token() self.headers['X-Rucio-Auth-Token'] = self.auth_token break retry += 1 if self.auth_token is None: raise CannotAuthenticate('cannot get an auth token from server')
def __authenticate(self): """ Main method for authentication. It first tries to read a locally saved token. If not available it requests a new one. """ if self.auth_type == 'userpass': if self.creds['username'] is None or self.creds['password'] is None: raise NoAuthInformation('No username or password passed') elif self.auth_type == 'x509': if self.creds['client_cert'] is None: raise NoAuthInformation( 'The path to the client certificate is required') elif self.auth_type == 'x509_proxy': if self.creds['client_proxy'] is None: raise NoAuthInformation('The client proxy has to be defined') elif self.auth_type == 'ssh': if self.creds['ssh_private_key'] is None: raise NoAuthInformation( 'The SSH private key has to be defined') elif self.auth_type == 'gss': pass else: raise CannotAuthenticate('auth type \'%s\' not supported' % self.auth_type) if not self.__read_token(): self.__get_token()
def redirect_auth_oidc(auth_code, fetchtoken=False, session=None): """ Finds the Authentication URL in the Rucio DB oauth_requests table and redirects user's browser to this URL. :param auth_code: Rucio assigned code to redirect authorization securely to IdP via Rucio Auth server through a browser. :param fetchtoken: If True, valid token temporarily saved in the oauth_requests table will be returned. If False, redirection URL is returned. :param session: The database session in use. :returns: result of the query (authorization URL or a token if a user asks with the correct code) or None. Exception thrown in case of an unexpected crash. """ try: redirect_result = session.query(models.OAuthRequest.redirect_msg).filter_by(access_msg=auth_code).first() if isinstance(redirect_result, tuple): if 'http' not in redirect_result[0] and fetchtoken: # in this case the function check if the value is a valid token vdict = validate_auth_token(redirect_result[0], session=session) if vdict: return redirect_result[0] return None elif 'http' in redirect_result[0] and not fetchtoken: # return redirection URL return redirect_result[0] return None return None except: raise CannotAuthenticate(traceback.format_exc())
def __get_token_x509(self): """ Sends a request to get an auth token from the server and stores it as a class attribute. Uses x509 authentication. :returns: True if the token was successfully received. False otherwise. """ headers = {'X-Rucio-Account': self.account} client_cert = None client_key = None if self.auth_type == 'x509': url = build_url(self.auth_host, path='auth/x509') client_cert = self.creds['client_cert'] if 'client_key' in self.creds: client_key = self.creds['client_key'] elif self.auth_type == 'x509_proxy': url = build_url(self.auth_host, path='auth/x509_proxy') client_cert = self.creds['client_proxy'] if not path.exists(client_cert): LOG.error('given client cert (%s) doesn\'t exist' % client_cert) return False if client_key is not None and not path.exists(client_key): LOG.error('given client key (%s) doesn\'t exist' % client_key) if client_key is None: cert = client_cert else: cert = (client_cert, client_key) result = None for retry in range(self.AUTH_RETRIES + 1): try: result = self.session.get(url, headers=headers, cert=cert, verify=self.ca_cert) break except ConnectionError as error: if 'alert certificate expired' in str(error): raise CannotAuthenticate(str(error)) LOG.warning('ConnectionError: ' + str(error)) self.ca_cert = False if retry > self.request_retries: raise # Note a response object for a failed request evaluates to false, so we cannot # use "not result" here if result is None: LOG.error('Internal error: Request for authentication token returned no result!') return False if result.status_code != codes.ok: # pylint: disable-msg=E1101 exc_cls, exc_msg = self._get_exception(headers=result.headers, status_code=result.status_code, data=result.content) raise exc_cls(exc_msg) self.auth_token = result.headers['x-rucio-auth-token'] LOG.debug('got new token \'%s\'' % self.auth_token) return True
def __get_token(self): """ Calls the corresponding method to receive an auth token depending on the auth type. To be used if a 401 - Unauthorized error is received. """ LOG.debug('get a new token') for retry in range(self.AUTH_RETRIES + 1): if self.auth_type == 'userpass': if not self.__get_token_userpass(): raise CannotAuthenticate('userpass authentication failed for account=%s with identity=%s' % (self.account, self.creds['username'])) elif self.auth_type == 'x509' or self.auth_type == 'x509_proxy': if not self.__get_token_x509(): raise CannotAuthenticate('x509 authentication failed for account=%s with identity=%s' % (self.account, self.creds)) elif self.auth_type == 'gss': if not self.__get_token_gss(): raise CannotAuthenticate('kerberos authentication failed for account=%s with identity=%s' % (self.account, self.creds)) elif self.auth_type == 'ssh': if not self.__get_token_ssh(): raise CannotAuthenticate('ssh authentication failed for account=%s with identity=%s' % (self.account, self.creds)) else: raise CannotAuthenticate('auth type \'%s\' not supported' % self.auth_type) if self.auth_token is not None: self.__write_token() self.headers['X-Rucio-Auth-Token'] = self.auth_token break if self.auth_token is None: raise CannotAuthenticate('cannot get an auth token from server')
def __authenticate(self): """ Main method for authentication. It first tries to read a locally saved token. If not available it requests a new one. """ if self.auth_type == 'userpass': if self.creds['username'] is None or self.creds['password'] is None: raise NoAuthInformation('No username or password passed') elif self.auth_type == 'oidc': if self.creds['oidc_auto'] and ( self.creds['oidc_username'] is None or self.creds['oidc_password'] is None): raise NoAuthInformation( 'For automatic OIDC log-in with your Identity Provider username and password are required.' ) if not self.creds['oidc_scope']: raise NoAuthInformation( 'For OIDC log-in you need to provide a scope parameter. The minimal expected by Rucio server is usually "openid profile"' ) elif self.auth_type == 'x509': if self.creds['client_cert'] is None: raise NoAuthInformation( 'The path to the client certificate is required') elif self.auth_type == 'x509_proxy': if self.creds['client_proxy'] is None: raise NoAuthInformation('The client proxy has to be defined') elif self.auth_type == 'ssh': if self.creds['ssh_private_key'] is None: raise NoAuthInformation( 'The SSH private key has to be defined') elif self.auth_type == 'gss': pass elif self.auth_type == 'saml': if self.creds['username'] is None or self.creds['password'] is None: raise NoAuthInformation('No SAML username or password passed') else: raise CannotAuthenticate('auth type \'%s\' not supported' % self.auth_type) if not self.__read_token(): self.__get_token()
def __get_token_ssh(self): """ Sends a request to get an auth token from the server and stores it as a class attribute. Uses SSH key exchange authentication. :returns: True if the token was successfully received. False otherwise. """ headers = {'X-Rucio-Account': self.account} private_key_path = self.creds['ssh_private_key'] if not path.exists(private_key_path): LOG.error('given private key (%s) doesn\'t exist' % private_key_path) return False if private_key_path is not None and not path.exists(private_key_path): LOG.error('given private key (%s) doesn\'t exist' % private_key_path) return False url = build_url(self.auth_host, path='auth/ssh_challenge_token') result = None for retry in range(self.AUTH_RETRIES + 1): try: result = self.session.get(url, headers=headers, verify=self.ca_cert) break except ConnectionError as error: if 'alert certificate expired' in str(error): raise CannotAuthenticate(str(error)) LOG.warning('ConnectionError: ' + str(error)) self.ca_cert = False if retry > self.request_retries: raise if not result: LOG.error('cannot get ssh_challenge_token') return False if result.status_code != codes.ok: # pylint: disable-msg=E1101 exc_cls, exc_msg = self._get_exception( headers=result.headers, status_code=result.status_code, data=result.content) raise exc_cls(exc_msg) self.ssh_challenge_token = result.headers[ 'x-rucio-ssh-challenge-token'] LOG.debug('got new ssh challenge token \'%s\'' % self.ssh_challenge_token) # sign the challenge token with the private key with open(private_key_path, 'r') as fd_private_key_path: private_key = fd_private_key_path.read() signature = ssh_sign(private_key, self.ssh_challenge_token) headers['X-Rucio-SSH-Signature'] = signature url = build_url(self.auth_host, path='auth/ssh') result = None for retry in range(self.AUTH_RETRIES + 1): try: result = self.session.get(url, headers=headers, verify=self.ca_cert) break except ConnectionError as error: if 'alert certificate expired' in str(error): raise CannotAuthenticate(str(error)) LOG.warning('ConnectionError: ' + str(error)) self.ca_cert = False if retry > self.request_retries: raise if not result: LOG.error('cannot get auth_token') return False if result.status_code != codes.ok: # pylint: disable-msg=E1101 exc_cls, exc_msg = self._get_exception( headers=result.headers, status_code=result.status_code, data=result.content) raise exc_cls(exc_msg) self.auth_token = result.headers['x-rucio-auth-token'] LOG.debug('got new token') return True
def __init__(self, rucio_host=None, auth_host=None, account=None, ca_cert=None, auth_type=None, creds=None, timeout=600, user_agent='rucio-clients', vo=None, logger=None): """ Constructor of the BaseClient. :param rucio_host: The address of the rucio server, if None it is read from the config file. :param rucio_port: The port of the rucio server, if None it is read from the config file. :param auth_host: The address of the rucio authentication server, if None it is read from the config file. :param auth_port: The port of the rucio authentication server, if None it is read from the config file. :param account: The account to authenticate to rucio. :param use_ssl: Enable or disable ssl for commucation. Default is enabled. :param ca_cert: The path to the rucio server certificate. :param auth_type: The type of authentication (e.g.: 'userpass', 'kerberos' ...) :param creds: Dictionary with credentials needed for authentication. :param user_agent: Indicates the client. :param vo: The VO to authenticate into. :param logger: Logger object to use. If None, use the default LOG created by the module """ self.host = rucio_host self.list_hosts = [] self.auth_host = auth_host self.logger = logger or LOG self.session = Session() self.user_agent = "%s/%s" % (user_agent, version.version_string()) # e.g. "rucio-clients/0.2.13" sys.argv[0] = sys.argv[0].split('/')[-1] self.script_id = '::'.join(sys.argv[0:2]) if self.script_id == '': # Python interpreter used self.script_id = 'python' try: if self.host is None: self.host = config_get('client', 'rucio_host') if self.auth_host is None: self.auth_host = config_get('client', 'auth_host') except (NoOptionError, NoSectionError) as error: raise MissingClientParameter('Section client and Option \'%s\' cannot be found in config file' % error.args[0]) try: self.trace_host = config_get('trace', 'trace_host') except (NoOptionError, NoSectionError): self.trace_host = self.host self.logger.debug('No trace_host passed. Using rucio_host instead') self.account = account self.vo = vo self.ca_cert = ca_cert self.auth_type = auth_type self.creds = creds self.auth_token = None self.auth_token_file_path = config_get('client', 'auth_token_file_path', False, None) self.headers = {} self.timeout = timeout self.request_retries = self.REQUEST_RETRIES self.token_exp_epoch = None self.token_exp_epoch_file = None self.auth_oidc_refresh_active = config_get_bool('client', 'auth_oidc_refresh_active', False, False) # defining how many minutes before token expires, oidc refresh (if active) should start self.auth_oidc_refresh_before_exp = config_get_int('client', 'auth_oidc_refresh_before_exp', False, 20) if auth_type is None: self.logger.debug('No auth_type passed. Trying to get it from the environment variable RUCIO_AUTH_TYPE and config file.') if 'RUCIO_AUTH_TYPE' in environ: if environ['RUCIO_AUTH_TYPE'] not in ['userpass', 'x509', 'x509_proxy', 'gss', 'ssh', 'saml', 'oidc']: raise MissingClientParameter('Possible RUCIO_AUTH_TYPE values: userpass, x509, x509_proxy, gss, ssh, saml, oidc, vs. ' + environ['RUCIO_AUTH_TYPE']) self.auth_type = environ['RUCIO_AUTH_TYPE'] else: try: self.auth_type = config_get('client', 'auth_type') except (NoOptionError, NoSectionError) as error: raise MissingClientParameter('Option \'%s\' cannot be found in config file' % error.args[0]) if self.auth_type == 'oidc': if not self.creds: self.creds = {} # if there are defautl values, check if rucio.cfg does not specify them, otherwise put default if 'oidc_refresh_lifetime' not in self.creds or self.creds['oidc_refresh_lifetime'] is None: self.creds['oidc_refresh_lifetime'] = config_get('client', 'oidc_refresh_lifetime', False, None) if 'oidc_issuer' not in self.creds or self.creds['oidc_issuer'] is None: self.creds['oidc_issuer'] = config_get('client', 'oidc_issuer', False, None) if 'oidc_audience' not in self.creds or self.creds['oidc_audience'] is None: self.creds['oidc_audience'] = config_get('client', 'oidc_audience', False, None) if 'oidc_auto' not in self.creds or self.creds['oidc_auto'] is False: self.creds['oidc_auto'] = config_get_bool('client', 'oidc_auto', False, False) if self.creds['oidc_auto']: if 'oidc_username' not in self.creds or self.creds['oidc_username'] is None: self.creds['oidc_username'] = config_get('client', 'oidc_username', False, None) if 'oidc_password' not in self.creds or self.creds['oidc_password'] is None: self.creds['oidc_password'] = config_get('client', 'oidc_password', False, None) if 'oidc_scope' not in self.creds or self.creds['oidc_scope'] == 'openid profile': self.creds['oidc_scope'] = config_get('client', 'oidc_scope', False, 'openid profile') if 'oidc_polling' not in self.creds or self.creds['oidc_polling'] is False: self.creds['oidc_polling'] = config_get_bool('client', 'oidc_polling', False, False) if not self.creds: self.logger.debug('No creds passed. Trying to get it from the config file.') self.creds = {} try: if self.auth_type in ['userpass', 'saml']: self.creds['username'] = config_get('client', 'username') self.creds['password'] = config_get('client', 'password') elif self.auth_type == 'x509': self.creds['client_cert'] = path.abspath(path.expanduser(path.expandvars(config_get('client', 'client_cert')))) if not path.exists(self.creds['client_cert']): raise MissingClientParameter('X.509 client certificate not found: %s' % self.creds['client_cert']) self.creds['client_key'] = path.abspath(path.expanduser(path.expandvars(config_get('client', 'client_key')))) if not path.exists(self.creds['client_key']): raise MissingClientParameter('X.509 client key not found: %s' % self.creds['client_key']) else: perms = oct(os.stat(self.creds['client_key']).st_mode)[-3:] if perms not in ['400', '600']: raise CannotAuthenticate('X.509 authentication selected, but private key (%s) permissions are liberal (required: 400 or 600, found: %s)' % (self.creds['client_key'], perms)) elif self.auth_type == 'x509_proxy': try: self.creds['client_proxy'] = path.abspath(path.expanduser(path.expandvars(config_get('client', 'client_x509_proxy')))) except NoOptionError: # Recreate the classic GSI logic for locating the proxy: # - $X509_USER_PROXY, if it is set. # - /tmp/x509up_u`id -u` otherwise. # If neither exists (at this point, we don't care if it exists but is invalid), then rethrow if 'X509_USER_PROXY' in environ: self.creds['client_proxy'] = environ['X509_USER_PROXY'] else: fname = '/tmp/x509up_u%d' % geteuid() if path.exists(fname): self.creds['client_proxy'] = fname else: raise MissingClientParameter('Cannot find a valid X509 proxy; not in %s, $X509_USER_PROXY not set, and ' '\'x509_proxy\' not set in the configuration file.' % fname) elif self.auth_type == 'ssh': self.creds['ssh_private_key'] = path.abspath(path.expanduser(path.expandvars(config_get('client', 'ssh_private_key')))) except (NoOptionError, NoSectionError) as error: if error.args[0] != 'client_key': raise MissingClientParameter('Option \'%s\' cannot be found in config file' % error.args[0]) rucio_scheme = urlparse(self.host).scheme auth_scheme = urlparse(self.auth_host).scheme if rucio_scheme != 'http' and rucio_scheme != 'https': raise ClientProtocolNotSupported('\'%s\' not supported' % rucio_scheme) if auth_scheme != 'http' and auth_scheme != 'https': raise ClientProtocolNotSupported('\'%s\' not supported' % auth_scheme) if (rucio_scheme == 'https' or auth_scheme == 'https') and ca_cert is None: self.logger.debug('HTTPS is required, but no ca_cert was passed. Trying to get it from X509_CERT_DIR.') self.ca_cert = os.environ.get('X509_CERT_DIR', None) if self.ca_cert is None: self.logger.debug('HTTPS is required, but no ca_cert was passed and X509_CERT_DIR is not defined. Trying to get it from the config file.') try: self.ca_cert = path.expandvars(config_get('client', 'ca_cert')) except (NoOptionError, NoSectionError): self.logger.debug('No ca_cert found in configuration. Falling back to Mozilla default CA bundle (certifi).') self.ca_cert = True self.list_hosts = [self.host] if account is None: self.logger.debug('No account passed. Trying to get it from the RUCIO_ACCOUNT environment variable or the config file.') try: self.account = environ['RUCIO_ACCOUNT'] except KeyError: try: self.account = config_get('client', 'account') except (NoOptionError, NoSectionError): pass if vo is None: self.logger.debug('No VO passed. Trying to get it from environment variable RUCIO_VO.') try: self.vo = environ['RUCIO_VO'] except KeyError: self.logger.debug('No VO found. Trying to get it from the config file.') try: self.vo = config_get('client', 'vo') except (NoOptionError, NoSectionError): self.logger.debug('No VO found. Using default VO.') self.vo = 'def' token_filename_suffix = "for_default_account" if self.account is None else "for_account_" + self.account # if token file path is defined in the rucio.cfg file, use that file. Currently this prevents authenticating as another user or VO. if self.auth_token_file_path: self.token_file = self.auth_token_file_path self.token_path = '/'.join(self.token_file.split('/')[:-1]) else: self.token_path = self.TOKEN_PATH_PREFIX + getpass.getuser() if self.vo != 'def': self.token_path += '@%s' % self.vo self.token_file = self.token_path + '/' + self.TOKEN_PREFIX + token_filename_suffix self.token_exp_epoch_file = self.token_path + '/' + self.TOKEN_EXP_PREFIX + token_filename_suffix self.__authenticate() try: self.request_retries = int(config_get('client', 'request_retries')) except (NoOptionError, RuntimeError): LOG.debug('request_retries not specified in config file. Taking default.') except ValueError: self.logger.debug('request_retries must be an integer. Taking default.')