Add JWT provider abstract class

Add JSON Web Token provider abstract class.
In addition to this, allow clients to configure
the token provider instance such when it is set,
the Authorization header of NSXT requests has
the bearer token value inserted.

Change-Id: Ieb701411413ec239276685f02ee1364bd2b05abd
This commit is contained in:
Abhishek Raut 2019-08-30 11:21:12 -07:00
parent 54393a235c
commit e252900cc0
7 changed files with 126 additions and 11 deletions

View File

@ -115,6 +115,7 @@ def get_default_nsxlib_config(allow_passthrough=True):
password=NSX_PASSWORD, password=NSX_PASSWORD,
retries=NSX_HTTP_RETRIES, retries=NSX_HTTP_RETRIES,
insecure=NSX_INSECURE, insecure=NSX_INSECURE,
token_provider=None,
ca_file=NSX_CERT, ca_file=NSX_CERT,
concurrent_connections=NSX_CONCURENT_CONN, concurrent_connections=NSX_CONCURENT_CONN,
http_timeout=NSX_HTTP_TIMEOUT, http_timeout=NSX_HTTP_TIMEOUT,
@ -137,6 +138,7 @@ def get_nsxlib_config_with_client_cert():
retries=NSX_HTTP_RETRIES, retries=NSX_HTTP_RETRIES,
insecure=NSX_INSECURE, insecure=NSX_INSECURE,
ca_file=NSX_CERT, ca_file=NSX_CERT,
token_provider=None,
concurrent_connections=NSX_CONCURENT_CONN, concurrent_connections=NSX_CONCURENT_CONN,
http_timeout=NSX_HTTP_TIMEOUT, http_timeout=NSX_HTTP_TIMEOUT,
http_read_timeout=NSX_HTTP_READ_TIMEOUT, http_read_timeout=NSX_HTTP_READ_TIMEOUT,
@ -224,6 +226,7 @@ class NsxClientTestCase(NsxLibTestCase):
password=password or NSX_PASSWORD, password=password or NSX_PASSWORD,
retries=retries or NSX_HTTP_RETRIES, retries=retries or NSX_HTTP_RETRIES,
insecure=insecure if insecure is not None else NSX_INSECURE, insecure=insecure if insecure is not None else NSX_INSECURE,
token_provider=None,
ca_file=ca_file or NSX_CERT, ca_file=ca_file or NSX_CERT,
concurrent_connections=(concurrent_connections or concurrent_connections=(concurrent_connections or
NSX_CONCURENT_CONN), NSX_CONCURENT_CONN),

View File

@ -53,6 +53,7 @@ class RequestsHTTPProviderTestCase(unittest.TestCase):
mock_api.nsxlib_config.password = 'nsxpassword' mock_api.nsxlib_config.password = 'nsxpassword'
mock_api.nsxlib_config.retries = 100 mock_api.nsxlib_config.retries = 100
mock_api.nsxlib_config.insecure = True mock_api.nsxlib_config.insecure = True
mock_api.nsxlib_config.token_provider = None
mock_api.nsxlib_config.ca_file = None mock_api.nsxlib_config.ca_file = None
mock_api.nsxlib_config.http_timeout = 99 mock_api.nsxlib_config.http_timeout = 99
mock_api.nsxlib_config.conn_idle_timeout = 39 mock_api.nsxlib_config.conn_idle_timeout = 39
@ -94,6 +95,36 @@ class RequestsHTTPProviderTestCase(unittest.TestCase):
self.assertEqual(cert_provider_inst, session.cert_provider) self.assertEqual(cert_provider_inst, session.cert_provider)
self.assertEqual(99, session.timeout) self.assertEqual(99, session.timeout)
@mock.patch("vmware_nsxlib.v3.cluster.NSXRequestsHTTPProvider."
"get_default_headers")
def test_new_connection_with_token_provider(self, mock_get_def_headers):
mock_api = mock.Mock()
mock_api.nsxlib_config = mock.Mock()
mock_api.nsxlib_config.retries = 100
mock_api.nsxlib_config.insecure = True
mock_api.nsxlib_config.ca_file = None
mock_api.nsxlib_config.http_timeout = 99
mock_api.nsxlib_config.conn_idle_timeout = 39
mock_api.nsxlib_config.client_cert_provider = None
token_provider_inst = mock.Mock()
mock_api.nsxlib_config.token_provider = token_provider_inst
mock_api.nsxlib_config.allow_overwrite_header = False
provider = cluster.NSXRequestsHTTPProvider()
cluster_provider = cluster.Provider('9.8.7.6', 'https://9.8.7.6',
'nsxuser', 'nsxpassword', None)
with mock.patch.object(cluster.TimeoutSession, 'request',
return_value=get_sess_create_resp()):
session = provider.new_connection(mock_api, cluster_provider)
self.assertIsNone(session.auth)
self.assertFalse(session.verify)
self.assertIsNone(session.cert)
self.assertEqual(100,
session.adapters['https://'].max_retries.total)
self.assertEqual(99, session.timeout)
mock_get_def_headers.assert_called_once_with(
mock.ANY, cluster_provider, False, token_provider_inst)
def test_validate_connection_keep_alive(self): def test_validate_connection_keep_alive(self):
mock_conn = mocks.MockRequestSessionApi() mock_conn = mocks.MockRequestSessionApi()
mock_conn.default_headers = {} mock_conn.default_headers = {}

View File

@ -213,7 +213,9 @@ class NSXRequestsHTTPProvider(AbstractHTTPProvider):
config.http_read_timeout) config.http_read_timeout)
if config.client_cert_provider: if config.client_cert_provider:
session.cert_provider = config.client_cert_provider session.cert_provider = config.client_cert_provider
else: # Set the headers with Auth info when token provider is set,
# otherwise set the username and password
elif not config.token_provider:
session.auth = (provider.username, provider.password) session.auth = (provider.username, provider.password)
# NSX v3 doesn't use redirects # NSX v3 doesn't use redirects
@ -233,7 +235,8 @@ class NSXRequestsHTTPProvider(AbstractHTTPProvider):
session.mount('https://', adapter) session.mount('https://', adapter)
self.get_default_headers(session, provider, self.get_default_headers(session, provider,
config.allow_overwrite_header) config.allow_overwrite_header,
config.token_provider)
return session return session
@ -246,22 +249,38 @@ class NSXRequestsHTTPProvider(AbstractHTTPProvider):
def is_conn_open_exception(self, exception): def is_conn_open_exception(self, exception):
return isinstance(exception, requests_exceptions.ConnectTimeout) return isinstance(exception, requests_exceptions.ConnectTimeout)
def get_default_headers(self, session, provider, allow_overwrite_header): def get_default_headers(self, session, provider, allow_overwrite_header,
token_provider=None):
"""Get the default headers that should be added to future requests""" """Get the default headers that should be added to future requests"""
session.default_headers = {} session.default_headers = {}
# Add allow-overwrite if configured
if allow_overwrite_header:
session.default_headers['X-Allow-Overwrite'] = 'true'
# Perform the initial session create and get the relevant jsessionid & # Perform the initial session create and get the relevant jsessionid &
# X-XSRF-TOKEN for future requests # X-XSRF-TOKEN for future requests
req_data = '' req_data = ''
if not session.cert_provider: req_headers = {'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded'}
# Insert the JWT in Auth header if using tokens for auth
if token_provider:
try:
token_value = token_provider.get_token()
bearer_token = token_provider.get_header_value(token_value)
token_header = {"Authorization": bearer_token}
session.default_headers.update(token_header)
req_headers.update(token_header)
except exceptions.BadJSONWebTokenProviderRequest as e:
LOG.error("Session create failed for endpoint %s due to "
"error in retrieving JSON Web Token: %s",
provider.url, e)
elif not session.cert_provider:
# With client certificate authentication, username and password # With client certificate authentication, username and password
# may not be provided. # may not be provided.
# If provided, backend treats these credentials as authentication # If provided, backend treats these credentials as authentication
# and ignores client cert as principal identity indication. # and ignores client cert as principal identity indication.
req_data = 'j_username=%s&j_password=%s' % (provider.username, req_data = 'j_username=%s&j_password=%s' % (provider.username,
provider.password) provider.password)
req_headers = {'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded'}
# Cannot use the certificate at this stage, because it is used for # Cannot use the certificate at this stage, because it is used for
# the certificate generation # the certificate generation
try: try:
@ -294,10 +313,6 @@ class NSXRequestsHTTPProvider(AbstractHTTPProvider):
"headers %(hdr)s", "headers %(hdr)s",
{'url': provider.url, 'hdr': session.default_headers}) {'url': provider.url, 'hdr': session.default_headers})
# Add allow-overwrite if configured
if allow_overwrite_header:
session.default_headers['X-Allow-Overwrite'] = 'true'
class ClusterHealth(object): class ClusterHealth(object):
"""Indicator of overall cluster health. """Indicator of overall cluster health.

View File

@ -42,6 +42,9 @@ class NsxLibConfig(object):
"insecure" is set to True. If "insecure" is set to "insecure" is set to True. If "insecure" is set to
False and ca_file is unset, the system root CAs will False and ca_file is unset, the system root CAs will
be used to verify the server certificate. be used to verify the server certificate.
:param token_provider: None, or instance of implemented AbstractJWTProvider
which will return the JSON Web Token used in the
requests in NSX for authorization.
:param concurrent_connections: Maximum concurrent connections to each NSX :param concurrent_connections: Maximum concurrent connections to each NSX
manager. manager.
@ -95,6 +98,7 @@ class NsxLibConfig(object):
client_cert_provider=None, client_cert_provider=None,
insecure=True, insecure=True,
ca_file=None, ca_file=None,
token_provider=None,
concurrent_connections=10, concurrent_connections=10,
retries=3, retries=3,
http_timeout=10, http_timeout=10,
@ -127,6 +131,7 @@ class NsxLibConfig(object):
self.conn_idle_timeout = conn_idle_timeout self.conn_idle_timeout = conn_idle_timeout
self.http_provider = http_provider self.http_provider = http_provider
self.client_cert_provider = client_cert_provider self.client_cert_provider = client_cert_provider
self.token_provider = token_provider
self.max_attempts = max_attempts self.max_attempts = max_attempts
self.plugin_scope = plugin_scope self.plugin_scope = plugin_scope
self.plugin_tag = plugin_tag self.plugin_tag = plugin_tag

View File

@ -150,6 +150,10 @@ class BadXSRFToken(ManagerError):
message = _("Bad or expired XSRF token") message = _("Bad or expired XSRF token")
class BadJSONWebTokenProviderRequest(NsxLibException):
message = _("Bad or expired JSON web token request from provider: %(msg)s")
class ServiceClusterUnavailable(ManagerError): class ServiceClusterUnavailable(ManagerError):
message = _("Service cluster: '%(cluster_id)s' is unavailable. Please, " message = _("Service cluster: '%(cluster_id)s' is unavailable. Please, "
"check NSX setup and/or configuration") "check NSX setup and/or configuration")

View File

@ -33,7 +33,9 @@ class NsxLibBase(object):
self.nsx_version = None self.nsx_version = None
self.nsx_api = None self.nsx_api = None
self.default_headers = None
self.set_config(nsxlib_config) self.set_config(nsxlib_config)
self.set_default_headers(nsxlib_config)
# create the Cluster # create the Cluster
self.cluster = cluster.NSXClusteredAPI(self.nsxlib_config) self.cluster = cluster.NSXClusteredAPI(self.nsxlib_config)
@ -44,7 +46,8 @@ class NsxLibBase(object):
nsx_api_managers=self.nsxlib_config.nsx_api_managers, nsx_api_managers=self.nsxlib_config.nsx_api_managers,
max_attempts=self.nsxlib_config.max_attempts, max_attempts=self.nsxlib_config.max_attempts,
url_path_base=self.client_url_prefix, url_path_base=self.client_url_prefix,
rate_limit_retry=self.nsxlib_config.rate_limit_retry) rate_limit_retry=self.nsxlib_config.rate_limit_retry,
default_headers=self.default_headers)
self.general_apis = utils.NsxLibApiBase( self.general_apis = utils.NsxLibApiBase(
self.client, self.nsxlib_config) self.client, self.nsxlib_config)
@ -61,6 +64,18 @@ class NsxLibBase(object):
validate_connection_method=self.validate_connection_method, validate_connection_method=self.validate_connection_method,
url_base=self.client_url_prefix) url_base=self.client_url_prefix)
def set_default_headers(self, nsxlib_config):
"""Set the default headers with token information"""
if nsxlib_config.token_provider:
try:
token_value = nsxlib_config.token_provider.get_token()
except exceptions.BadJSONWebTokenProviderRequest as e:
LOG.error("Error in retrieving JSON Web Token: %s", e)
return
bearer_token = "Bearer %s" % token_value
self.default_headers = self.default_headers or {}
self.default_headers["Authorization"] = bearer_token
@abc.abstractproperty @abc.abstractproperty
def client_url_prefix(self): def client_url_prefix(self):
pass pass

View File

@ -0,0 +1,42 @@
# Copyright 2019 VMware, Inc.
# 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 abc
import six
# NOTE: Consider inheriting from an abstract TokenProvider class to share
# interface with XSRF token
@six.add_metaclass(abc.ABCMeta)
class AbstractJWTProvider(object):
"""Interface for providers of JSON Web Tokens(JWT)
Responsible to provide the token value and refresh it once expired,
or on demand, for authorization of requests to NSX.
"""
@abc.abstractmethod
def get_token(self, refresh_token=False):
"""Request JWT value.
:param refresh_token: Boolean value, indicating whether a new token
value is to be retrieved.
:raises vmware_nsxlib.v3.exceptions.BadJSONWebTokenProviderRequest:
"""
pass
def get_header_value(self, token_value):
return "Bearer %s" % token_value