Files
update/software-client/software_client/common/http.py
Joseph Vazhappilly 169b84bc6c Restrict software commands to keystone admin role
When using Keystone auth for software cli, only user with 'admin' role
is allowed to run any commands. When using software cli without 'sudo',
all software commands require user with 'admin' role.

This review also update the exception handling and error reporting.

Test Plan:
PASS: A Keystone user in the 'admin' project with 'admin' role should
      be able to run ALL 'software' commands WITHOUT SUDO
PASS: A Keystone user in the 'admin' project with only 'member' and/or
      'reader' role should NOT be able to run ANY 'software' commands
      WITHOUT SUDO

Story: 2010676
Task: 49754

Change-Id: I46653021b1a82bccded5eb870dc0907cd5c2351b
Signed-off-by: Joseph Vazhappilly <joseph.vazhappillypaily@windriver.com>
2024-03-25 03:24:32 -04:00

785 lines
29 KiB
Python

# Copyright 2013-2024 Wind River, Inc.
# Copyright 2012 Openstack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
import copy
import hashlib
import httplib2
from keystoneauth1 import adapter
import logging
import os
from oslo_serialization import jsonutils
from oslo_utils import encodeutils
import requests
from requests_toolbelt import MultipartEncoder
import socket
import six
from six.moves.urllib.parse import urlparse
try:
import ssl
except ImportError:
# TODO(bcwaldon): Handle this failure more gracefully
pass
try:
import json
except ImportError:
import simplejson as json
from software_client import exc as exceptions
_logger = logging.getLogger(__name__)
CHUNKSIZE = 1024 * 64 # 64kB
SENSITIVE_HEADERS = ('X-Auth-Token',)
UPLOAD_REQUEST_TIMEOUT = 1800
USER_AGENT = 'software_client'
API_VERSION = '/v1'
DEFAULT_API_VERSION = 'latest'
# httplib2 retries requests on socket.timeout which
# is not idempotent and can lead to orhan objects.
# See: https://code.google.com/p/httplib2/issues/detail?id=124
httplib2.RETRIES = 1
if os.environ.get('SOFTWARE_CLIENT_DEBUG'):
ch = logging.StreamHandler()
_logger.setLevel(logging.DEBUG)
_logger.addHandler(ch)
class ServiceCatalog(object):
"""Helper methods for dealing with a Keystone Service Catalog."""
def __init__(self, resource_dict):
self.catalog = resource_dict
def get_token(self):
"""Fetch token details fron service catalog."""
token = {'id': self.catalog['access']['token']['id'],
'expires': self.catalog['access']['token']['expires'], }
try:
token['user_id'] = self.catalog['access']['user']['id']
token['tenant_id'] = (
self.catalog['access']['token']['tenant']['id'])
except Exception:
# just leave the tenant and user out if it doesn't exist
pass
return token
def url_for(self, attr=None, filter_value=None,
service_type='usm', endpoint_type='publicURL'):
"""Fetch the URL from the Neutron service for
a particular endpoint type. If none given, return
publicURL.
"""
catalog = self.catalog['access'].get('serviceCatalog', [])
matching_endpoints = []
for service in catalog:
if service['type'] != service_type:
continue
endpoints = service['endpoints']
for endpoint in endpoints:
if not filter_value or endpoint.get(attr) == filter_value:
matching_endpoints.append(endpoint)
if not matching_endpoints:
raise exceptions.EndpointNotFound()
elif len(matching_endpoints) > 1:
raise exceptions.AmbiguousEndpoints(reason=matching_endpoints)
else:
if endpoint_type not in matching_endpoints[0]:
raise exceptions.EndpointTypeNotFound(reason=endpoint_type)
return matching_endpoints[0][endpoint_type]
def _extract_error_json_text(body_json):
error_json = {}
if 'error_message' in body_json:
raw_msg = body_json['error_message']
if 'error' in raw_msg:
raw_error = jsonutils.loads(raw_msg)
error_json = {'faultstring': raw_error.get('error'),
'debuginfo': raw_error.get('info')}
elif 'error_message' in raw_msg:
raw_error = jsonutils.loads(raw_msg)
raw_msg = raw_error['error_message']
error_json = jsonutils.loads(raw_msg)
return error_json
def _extract_error_json(body, resp):
"""Return error_message from the HTTP response body."""
try:
content_type = resp.headers.get("Content-Type", "")
except AttributeError:
content_type = ""
if content_type.startswith("application/json"):
try:
body_json = resp.json()
return _extract_error_json_text(body_json)
except AttributeError:
body_json = jsonutils.loads(body)
return _extract_error_json_text(body_json)
except ValueError:
return {}
else:
try:
body_json = jsonutils.loads(body)
return _extract_error_json_text(body_json)
except ValueError:
return {}
class SessionClient(adapter.LegacyJsonAdapter):
def __init__(self, *args, **kwargs):
self.user_agent = USER_AGENT
self.api_version = 'v' + kwargs.pop('api_version')
super(SessionClient, self).__init__(*args, **kwargs)
def _http_request(self, url, method, **kwargs):
version_str = '/' + self.api_version
if url.startswith(version_str):
url = url[len(version_str):]
kwargs.setdefault('user_agent', self.user_agent)
kwargs.setdefault('auth', self.auth)
kwargs.setdefault('endpoint_override', self.endpoint_override)
# Copy the kwargs so we can reuse the original in case of redirects
kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {}))
kwargs['headers'].setdefault('User-Agent', self.user_agent)
endpoint_filter = kwargs.setdefault('endpoint_filter', {})
endpoint_filter.setdefault('interface', self.interface)
endpoint_filter.setdefault('service_type', self.service_type)
endpoint_filter.setdefault('region_name', self.region_name)
resp = self.session.request(url, method,
raise_exc=False, **kwargs)
if 400 <= resp.status_code < 600:
error_json = _extract_error_json(resp.content, resp)
raise exceptions.from_response(
resp, error_json.get('faultstring'),
error_json.get('debuginfo'), method, url)
elif resp.status_code in (300, 301, 302, 305):
raise exceptions.from_response(resp, method=method, url=url)
return resp
def json_request(self, method, url, **kwargs):
kwargs.setdefault('headers', {})
kwargs['headers'].setdefault('Content-Type', 'application/json')
kwargs['headers'].setdefault('Accept', 'application/json')
if 'body' in kwargs:
kwargs['data'] = jsonutils.dumps(kwargs.pop('body'))
resp = self._http_request(url, method, **kwargs)
body = resp.content
content_type = resp.headers.get('content-type', None)
status = resp.status_code
if status == 204 or status == 205 or content_type is None:
return resp, list()
if 'application/json' in content_type:
try:
body = resp.json()
except ValueError:
_logger.error('Could not decode response body as JSON')
else:
body = None
return resp, body
def multipart_request(self, method, url, **kwargs):
kwargs.setdefault('headers', {})
kwargs['headers'].setdefault('Content-Type', 'application/json')
kwargs['headers'].setdefault('Accept', 'application/json')
if 'body' in kwargs:
kwargs['data'] = kwargs.pop('body')
resp = self._http_request(url, method, **kwargs)
body = resp.content
content_type = resp.headers.get('content-type', None)
status = resp.status_code
if status == 204 or status == 205 or content_type is None:
return resp, list()
if 'application/json' in content_type:
try:
body = resp.json()
except ValueError:
_logger.error('Could not decode response body as JSON')
else:
body = None
return resp, body
def raw_request(self, method, url, **kwargs):
kwargs.setdefault('headers', {})
kwargs['headers'].setdefault('Content-Type',
'application/octet-stream')
return self._http_request(url, method, **kwargs)
def _get_connection_url(self, url):
endpoint = self.endpoint_override
version = self.api_version
# if 'v1 in both, remove 'v1' from endpoint
if version in endpoint and version in url:
endpoint = endpoint.replace('/' + version, '', 1)
# if 'v1 not in both, add 'v1' to endpoint
elif version not in endpoint and version not in url:
endpoint = endpoint.rstrip('/') + '/' + version
return endpoint.rstrip('/') + '/' + url.lstrip('/')
def upload_request_with_data(self, method, url, **kwargs):
requests_url = self._get_connection_url(url)
headers = {"X-Auth-Token": self.session.get_token()}
files = {'file': ("for_upload",
kwargs['body'],
)}
data = kwargs.get('data')
req = requests.post(requests_url, headers=headers, files=files,
data=data)
return req.json()
def upload_request_with_multipart(self, method, url, **kwargs):
requests_url = self._get_connection_url(url)
fields = kwargs.get('data')
enc = MultipartEncoder(fields)
headers = {'Content-Type': enc.content_type,
"X-Auth-Token": self.session.get_token()}
response = requests.post(requests_url, data=enc, headers=headers)
if kwargs.get('check_exceptions'):
if response.status_code != 200:
err_message = _extract_error_json(response.text, response)
fault_text = (
err_message.get("faultstring")
or "Unknown error in SessionClient while uploading request with multipart"
)
raise exceptions.HTTPBadRequest(fault_text)
return response.json()
class HTTPClient(httplib2.Http):
"""Handles the REST calls and responses, include authn."""
#################
# INIT
#################
def __init__(self, endpoint,
username=None, tenant_name=None, tenant_id=None,
password=None, auth_url=None,
token=None, region_name=None, timeout=7200,
endpoint_url=None, insecure=False,
endpoint_type='publicURL',
ca_cert=None, log_credentials=False,
**kwargs):
if 'ca_file' in kwargs and kwargs['ca_file']:
ca_cert = kwargs['ca_file']
super(HTTPClient, self).__init__(timeout=timeout, ca_certs=ca_cert)
self.username = username
self.tenant_name = tenant_name
self.tenant_id = tenant_id
self.password = password
self.auth_url = auth_url.rstrip('/') if auth_url else None
self.endpoint_type = endpoint_type
self.region_name = region_name
self.auth_token = token
self.auth_tenant_id = None
self.auth_user_id = None
self.content_type = 'application/json'
self.endpoint_url = endpoint
self.log_credentials = log_credentials
self.connection_params = self.get_connection_params(self.endpoint_url, **kwargs)
self.local_root = kwargs.get('local_root', False)
self.api_version = 'v' + kwargs.pop('api_version')
# httplib2 overrides
self.disable_ssl_certificate_validation = insecure
self.service_catalog = None
#################
# REQUEST
#################
@staticmethod
def http_log_resp(_logger, resp, body=None):
if not _logger.isEnabledFor(logging.DEBUG):
return
resp_status_code = resp.get('status_code') or ""
resp_headers = resp.get('headers') or ""
_logger.debug("RESP:%(code)s %(headers)s %(body)s\n",
{'code': resp_status_code,
'headers': resp_headers,
'body': body})
@staticmethod
def http_log_req(_logger, args, kwargs):
if not _logger.isEnabledFor(logging.DEBUG):
return
string_parts = ['curl -i']
for element in args:
if element in ('GET', 'POST', 'DELETE', 'PUT'):
string_parts.append(' -X %s' % element)
else:
string_parts.append(' %s' % element)
for (key, value) in kwargs['headers'].items():
if key in SENSITIVE_HEADERS:
v = value.encode('utf-8')
h = hashlib.sha256(v)
d = h.hexdigest()
value = "{SHA256}%s" % d
header = ' -H "%s: %s"' % (key, value)
string_parts.append(header)
if 'body' in kwargs and kwargs['body']:
string_parts.append(" -d '%s'" % (kwargs['body']))
req = encodeutils.safe_encode("".join(string_parts))
_logger.debug("REQ: %s", req)
def _cs_request(self, *args, **kwargs):
kargs = {}
kargs.setdefault('headers', kwargs.get('headers', {}))
if 'content_type' in kwargs:
kargs['headers']['Content-Type'] = kwargs['content_type']
kargs['headers']['Accept'] = kwargs['content_type']
else:
kargs['headers']['Content-Type'] = self.content_type
kargs['headers']['Accept'] = self.content_type
if self.auth_token:
kargs['headers']['X-Auth-Token'] = self.auth_token
if 'body' in kwargs:
kargs['body'] = kwargs['body']
if self.log_credentials:
log_kargs = kargs
else:
log_kargs = self._strip_credentials(kargs)
self.http_log_req(_logger, args, log_kargs)
try:
resp, body = self.request(*args, **kargs)
except requests.exceptions.SSLError as e:
raise exceptions.SslCertificateValidationError(reason=str(e))
except Exception as e:
# Wrap the low-level connection error (socket timeout, redirect
# limit, decompression error, etc) into our custom high-level
# connection exception (it is excepted in the upper layers of code)
_logger.debug("throwing ConnectionFailed : %s", e)
raise exceptions.CommunicationError(str(e))
finally:
# Temporary Fix for gate failures. RPC calls and HTTP requests
# seem to be stepping on each other resulting in bogus fd's being
# picked up for making http requests
self.connections.clear()
# Read body into string if it isn't obviously image data
body_str = None
if 'content-type' in resp and resp['content-type'] != 'application/octet-stream':
body_str = ''.join([chunk for chunk in body.decode('utf8')])
self.http_log_resp(_logger, resp, body_str)
body = body_str
else:
self.http_log_resp(_logger, resp, body)
status_code = self.get_status_code(resp)
if status_code == 401:
raise exceptions.HTTPUnauthorized(body)
elif status_code == 403:
reason = "Not allowed/Proper role is needed"
if body_str is not None:
error_json = self._extract_error_json(body_str)
reason = error_json.get('faultstring')
if reason is None:
reason = error_json.get('description')
raise exceptions.Forbidden(reason)
elif 400 <= status_code < 600:
_logger.warn("Request returned failure status: %s", status_code) # pylint: disable=deprecated-method
error_json = self._extract_error_json(body_str)
raise exceptions.from_response(
resp, error_json.get('faultstring'),
error_json.get('debuginfo'), *args)
elif status_code in (300, 301, 302, 305):
raise exceptions.from_response(resp, *args)
return resp, body
def json_request(self, method, url, **kwargs):
if not self.local_root:
self.authenticate_and_fetch_endpoint_url()
# Perform the request once. If we get a 401 back then it
# might be because the auth token expired, so try to
# re-authenticate and try again. If it still fails, bail.
kwargs.setdefault('headers', {})
kwargs['headers'].setdefault('Content-Type', 'application/json')
kwargs['headers'].setdefault('Accept', 'application/json')
if 'body' in kwargs:
kwargs['body'] = json.dumps(kwargs['body'])
connection_url = self._get_connection_url(url)
try:
resp, body_iter = self._cs_request(connection_url,
method, **kwargs)
except exceptions.HTTPUnauthorized:
self.authenticate()
resp, body_iter = self._cs_request(
connection_url, method, **kwargs)
content_type = resp['content-type'] \
if resp.get('content-type', None) else None
# Add status_code attribute to make compatible with session resp
setattr(resp, 'status_code', resp.status)
if resp.status == 204 or resp.status == 205 or content_type is None:
return resp, list()
if 'application/json' in content_type:
body = ''.join([chunk for chunk in body_iter])
try:
body = json.loads(body)
except ValueError:
_logger.error('Could not decode response body as JSON')
else:
body = None
return resp, body
def multipart_request(self, method, url, **kwargs):
return self.upload_request_with_multipart(method, url, **kwargs)
def raw_request(self, method, url, **kwargs):
if not self.local_root:
self.authenticate_and_fetch_endpoint_url()
kwargs.setdefault('headers', {})
kwargs['headers'].setdefault('Content-Type',
'application/octet-stream')
connection_url = self._get_connection_url(url)
return self._cs_request(connection_url, method, **kwargs)
def upload_request_with_data(self, method, url, **kwargs):
if not self.local_root:
self.authenticate_and_fetch_endpoint_url()
connection_url = self._get_connection_url(url)
headers = {"X-Auth-Token": self.auth_token}
files = {'file': ("for_upload",
kwargs['body'],
)}
data = kwargs.get('data')
req = requests.post(connection_url, headers=headers,
files=files, data=data,
timeout=UPLOAD_REQUEST_TIMEOUT)
return req.json()
def upload_request_with_multipart(self, method, url, **kwargs):
if not self.local_root:
self.authenticate_and_fetch_endpoint_url()
connection_url = self._get_connection_url(url)
response = requests.post(connection_url,
data=kwargs.get('body'),
headers=kwargs.get('headers'),
timeout=UPLOAD_REQUEST_TIMEOUT)
return response, response.json()
#################
# AUTHENTICATE
#################
def authenticate_and_fetch_endpoint_url(self):
if not self.auth_token:
self.authenticate()
if not self.endpoint_url:
self._get_endpoint_url()
def authenticate(self):
if self.auth_url is None:
raise exceptions.HTTPUnauthorized("No auth_url provided")
token_url = self.auth_url + "/tokens"
if self.tenant_id:
body = {'auth': {'passwordCredentials':
{'username': self.username,
'password': self.password, },
'tenantId': self.tenant_id, }, }
else:
body = {'auth': {'passwordCredentials':
{'username': self.username,
'password': self.password, },
'tenantName': self.tenant_name, }, }
resp, resp_body = self._cs_request(token_url, "POST",
body=json.dumps(body),
content_type="application/json")
status_code = self.get_status_code(resp)
if status_code != 200:
raise exceptions.HTTPUnauthorized(resp_body)
if resp_body:
try:
resp_body = json.loads(resp_body)
except ValueError:
pass
else:
resp_body = None
self._extract_service_catalog(resp_body)
_logger.debug("Authenticated user %s", self.username)
def get_auth_info(self):
return {'auth_token': self.auth_token,
'auth_tenant_id': self.auth_tenant_id,
'auth_user_id': self.auth_user_id,
'endpoint_url': self.endpoint_url}
#################
# UTILS
#################
def _extract_error_json(self, body):
error_json = {}
try:
body_json = json.loads(body)
if 'error' in body_json:
error_json = {'faultstring': body_json.get('error'),
'debuginfo': body_json.get('info')}
elif 'error_message' in body_json:
raw_msg = body_json['error_message']
error_json = json.loads(raw_msg)
except ValueError:
return {}
return error_json
def _strip_credentials(self, kwargs):
if kwargs.get('body') and self.password:
log_kwargs = kwargs.copy()
log_kwargs['body'] = kwargs['body'].replace(self.password,
'REDACTED')
return log_kwargs
else:
return kwargs
def _extract_service_catalog(self, body):
"""Set the client's service catalog from the response data."""
self.service_catalog = ServiceCatalog(body)
try:
sc = self.service_catalog.get_token()
self.auth_token = sc['id']
self.auth_tenant_id = sc.get('tenant_id')
self.auth_user_id = sc.get('user_id')
except KeyError:
raise exceptions.HTTPUnauthorized()
if not self.endpoint_url:
self.endpoint_url = self.service_catalog.url_for(
attr='region', filter_value=self.region_name,
endpoint_type=self.endpoint_type)
def _get_endpoint_url(self):
url = self.auth_url + '/tokens/%s/endpoints' % self.auth_token
try:
resp, body = self._cs_request(url, "GET")
except exceptions.HTTPUnauthorized:
# rollback to authenticate() to handle case when neutron client
# is initialized just before the token is expired
self.authenticate()
return self.endpoint_url
body = json.loads(body)
for endpoint in body.get('endpoints', []):
if (endpoint['type'] == 'usm' and endpoint.get('region') == self.region_name):
if self.endpoint_type not in endpoint:
raise exceptions.EndpointTypeNotFound(
reason=self.endpoint_type)
return endpoint[self.endpoint_type]
raise exceptions.EndpointNotFound()
def _get_connection_url(self, url):
(_class, _args, _kwargs) = self.connection_params
base_url = _args[2]
# Since some packages send endpoint with 'v1' and some don't,
# the postprocessing for both options will be done here
# Instead of doing a fix in each of these packages
endpoint = self.endpoint_url
version = self.api_version
# if 'v1 in both, remove 'v1' from endpoint
if version in base_url and version in url:
endpoint = endpoint.replace('/' + version, '', 1)
# if 'v1 not in both, add 'v1' to endpoint
elif version not in base_url and version not in url:
endpoint = endpoint.rstrip('/') + '/' + version
return endpoint.rstrip('/') + '/' + url.lstrip('/')
@staticmethod
def get_connection_params(endpoint, **kwargs):
parts = urlparse(endpoint)
_args = (parts.hostname, parts.port, parts.path)
_kwargs = {'timeout': (float(kwargs.get('timeout'))
if kwargs.get('timeout') else 600)}
if parts.scheme == 'https':
_class = VerifiedHTTPSConnection
_kwargs['ca_file'] = kwargs.get('ca_file', None)
_kwargs['cert_file'] = kwargs.get('cert_file', None)
_kwargs['key_file'] = kwargs.get('key_file', None)
_kwargs['insecure'] = kwargs.get('insecure', False)
elif parts.scheme == 'http':
_class = six.moves.http_client.HTTPConnection
else:
msg = 'Unsupported scheme: %s' % parts.scheme
raise exceptions.EndpointException(reason=msg)
return (_class, _args, _kwargs)
def get_status_code(self, response):
"""Returns the integer status code from the response.
Either a Webob.Response (used in testing) or httplib.Response
is returned.
"""
if hasattr(response, 'status_int'):
return response.status_int
else:
return response.status
class VerifiedHTTPSConnection(six.moves.http_client.HTTPSConnection):
"""httplib-compatibile connection using client-side SSL authentication
:see http://code.activestate.com/recipes/
577548-https-httplib-client-connection-with-certificate-v/
"""
def __init__(self, host, port, key_file=None, cert_file=None,
ca_file=None, timeout=None, insecure=False):
six.moves.http_client.HTTPSConnection.__init__(self, host, port,
key_file=key_file,
cert_file=cert_file)
self.key_file = key_file
self.cert_file = cert_file
if ca_file is not None:
self.ca_file = ca_file
else:
self.ca_file = self.get_system_ca_file()
self.timeout = timeout
self.insecure = insecure
def connect(self):
"""Connect to a host on a given (SSL) port.
If ca_file is pointing somewhere, use it to check Server Certificate.
Redefined/copied and extended from httplib.py:1105 (Python 2.6.x).
This is needed to pass cert_reqs=ssl.CERT_REQUIRED as parameter to
ssl.wrap_socket(), which forces SSL to check server certificate against
our client certificate.
"""
sock = socket.create_connection((self.host, self.port), self.timeout)
if self._tunnel_host:
self.sock = sock
self._tunnel()
if self.insecure is True:
kwargs = {'cert_reqs': ssl.CERT_NONE}
else:
kwargs = {'cert_reqs': ssl.CERT_REQUIRED, 'ca_certs': self.ca_file}
if self.cert_file:
kwargs['certfile'] = self.cert_file
if self.key_file:
kwargs['keyfile'] = self.key_file
self.sock = ssl.wrap_socket(sock, **kwargs)
@staticmethod
def get_system_ca_file():
"""Return path to system default CA file."""
# Standard CA file locations for Debian/Ubuntu, RedHat/Fedora,
# Suse, FreeBSD/OpenBSD
ca_path = ['/etc/ssl/certs/ca-certificates.crt',
'/etc/pki/tls/certs/ca-bundle.crt',
'/etc/ssl/ca-bundle.pem',
'/etc/ssl/cert.pem']
for ca in ca_path:
if os.path.exists(ca):
return ca
return None
class ResponseBodyIterator(object):
"""A class that acts as an iterator over an HTTP response."""
def __init__(self, resp):
self.resp = resp
def __iter__(self):
while True:
yield six.next() # pylint: disable=next-method-called
def next(self): # pylint: disable=next-method-defined
chunk = self.resp.read(CHUNKSIZE)
if chunk:
return chunk
else:
raise StopIteration()
def construct_http_client(endpoint=None, username=None, password=None,
endpoint_type=None, auth_url=None, **kwargs):
session = kwargs.pop('session', None)
auth = kwargs.pop('auth', None)
if session:
# SessionClient
if 'endpoint_override' not in kwargs and endpoint:
kwargs['endpoint_override'] = endpoint
if 'service_type' not in kwargs:
kwargs['service_type'] = 'usm'
if 'interface' not in kwargs and endpoint_type:
kwargs['interface'] = endpoint_type
if 'region_name' in kwargs:
kwargs['additional_headers'] = {
'X-Region-Name': kwargs['region_name']}
return SessionClient(session, auth=auth, **kwargs)
else:
# httplib2
return HTTPClient(endpoint=endpoint, username=username,
password=password, endpoint_type=endpoint_type,
auth_url=auth_url, **kwargs)