diff --git a/vmware_nsxlib/tests/unit/v3/nsxlib_testcase.py b/vmware_nsxlib/tests/unit/v3/nsxlib_testcase.py index 3cfd280b..f63a1266 100644 --- a/vmware_nsxlib/tests/unit/v3/nsxlib_testcase.py +++ b/vmware_nsxlib/tests/unit/v3/nsxlib_testcase.py @@ -115,6 +115,7 @@ def get_default_nsxlib_config(allow_passthrough=True): password=NSX_PASSWORD, retries=NSX_HTTP_RETRIES, insecure=NSX_INSECURE, + token_provider=None, ca_file=NSX_CERT, concurrent_connections=NSX_CONCURENT_CONN, http_timeout=NSX_HTTP_TIMEOUT, @@ -137,6 +138,7 @@ def get_nsxlib_config_with_client_cert(): retries=NSX_HTTP_RETRIES, insecure=NSX_INSECURE, ca_file=NSX_CERT, + token_provider=None, concurrent_connections=NSX_CONCURENT_CONN, http_timeout=NSX_HTTP_TIMEOUT, http_read_timeout=NSX_HTTP_READ_TIMEOUT, @@ -224,6 +226,7 @@ class NsxClientTestCase(NsxLibTestCase): password=password or NSX_PASSWORD, retries=retries or NSX_HTTP_RETRIES, insecure=insecure if insecure is not None else NSX_INSECURE, + token_provider=None, ca_file=ca_file or NSX_CERT, concurrent_connections=(concurrent_connections or NSX_CONCURENT_CONN), diff --git a/vmware_nsxlib/tests/unit/v3/test_cluster.py b/vmware_nsxlib/tests/unit/v3/test_cluster.py index bbfed149..c99d7976 100644 --- a/vmware_nsxlib/tests/unit/v3/test_cluster.py +++ b/vmware_nsxlib/tests/unit/v3/test_cluster.py @@ -53,6 +53,7 @@ class RequestsHTTPProviderTestCase(unittest.TestCase): mock_api.nsxlib_config.password = 'nsxpassword' mock_api.nsxlib_config.retries = 100 mock_api.nsxlib_config.insecure = True + mock_api.nsxlib_config.token_provider = None mock_api.nsxlib_config.ca_file = None mock_api.nsxlib_config.http_timeout = 99 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(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): mock_conn = mocks.MockRequestSessionApi() mock_conn.default_headers = {} diff --git a/vmware_nsxlib/v3/cluster.py b/vmware_nsxlib/v3/cluster.py index 425f467d..9df99d7d 100644 --- a/vmware_nsxlib/v3/cluster.py +++ b/vmware_nsxlib/v3/cluster.py @@ -213,7 +213,9 @@ class NSXRequestsHTTPProvider(AbstractHTTPProvider): config.http_read_timeout) if 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) # NSX v3 doesn't use redirects @@ -233,7 +235,8 @@ class NSXRequestsHTTPProvider(AbstractHTTPProvider): session.mount('https://', adapter) self.get_default_headers(session, provider, - config.allow_overwrite_header) + config.allow_overwrite_header, + config.token_provider) return session @@ -246,22 +249,38 @@ class NSXRequestsHTTPProvider(AbstractHTTPProvider): def is_conn_open_exception(self, exception): 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""" 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 & # X-XSRF-TOKEN for future requests 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 # may not be provided. # If provided, backend treats these credentials as authentication # and ignores client cert as principal identity indication. req_data = 'j_username=%s&j_password=%s' % (provider.username, 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 # the certificate generation try: @@ -294,10 +313,6 @@ class NSXRequestsHTTPProvider(AbstractHTTPProvider): "headers %(hdr)s", {'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): """Indicator of overall cluster health. diff --git a/vmware_nsxlib/v3/config.py b/vmware_nsxlib/v3/config.py index 198f9f53..c0283691 100644 --- a/vmware_nsxlib/v3/config.py +++ b/vmware_nsxlib/v3/config.py @@ -42,6 +42,9 @@ class NsxLibConfig(object): "insecure" is set to True. If "insecure" is set to False and ca_file is unset, the system root CAs will 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 manager. @@ -95,6 +98,7 @@ class NsxLibConfig(object): client_cert_provider=None, insecure=True, ca_file=None, + token_provider=None, concurrent_connections=10, retries=3, http_timeout=10, @@ -127,6 +131,7 @@ class NsxLibConfig(object): self.conn_idle_timeout = conn_idle_timeout self.http_provider = http_provider self.client_cert_provider = client_cert_provider + self.token_provider = token_provider self.max_attempts = max_attempts self.plugin_scope = plugin_scope self.plugin_tag = plugin_tag diff --git a/vmware_nsxlib/v3/exceptions.py b/vmware_nsxlib/v3/exceptions.py index a4c3c0a4..4fe0cab4 100644 --- a/vmware_nsxlib/v3/exceptions.py +++ b/vmware_nsxlib/v3/exceptions.py @@ -150,6 +150,10 @@ class BadXSRFToken(ManagerError): message = _("Bad or expired XSRF token") +class BadJSONWebTokenProviderRequest(NsxLibException): + message = _("Bad or expired JSON web token request from provider: %(msg)s") + + class ServiceClusterUnavailable(ManagerError): message = _("Service cluster: '%(cluster_id)s' is unavailable. Please, " "check NSX setup and/or configuration") diff --git a/vmware_nsxlib/v3/lib.py b/vmware_nsxlib/v3/lib.py index bcf460dd..3d4c8d35 100644 --- a/vmware_nsxlib/v3/lib.py +++ b/vmware_nsxlib/v3/lib.py @@ -33,7 +33,9 @@ class NsxLibBase(object): self.nsx_version = None self.nsx_api = None + self.default_headers = None self.set_config(nsxlib_config) + self.set_default_headers(nsxlib_config) # create the Cluster self.cluster = cluster.NSXClusteredAPI(self.nsxlib_config) @@ -44,7 +46,8 @@ class NsxLibBase(object): nsx_api_managers=self.nsxlib_config.nsx_api_managers, max_attempts=self.nsxlib_config.max_attempts, 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.client, self.nsxlib_config) @@ -61,6 +64,18 @@ class NsxLibBase(object): validate_connection_method=self.validate_connection_method, 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 def client_url_prefix(self): pass diff --git a/vmware_nsxlib/v3/token_provider.py b/vmware_nsxlib/v3/token_provider.py new file mode 100644 index 00000000..cfe9e918 --- /dev/null +++ b/vmware_nsxlib/v3/token_provider.py @@ -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