From 4e699aec35a8edf24a150b8828d8c4251901499b Mon Sep 17 00:00:00 2001 From: Yusuke Niimi Date: Fri, 14 Oct 2022 08:56:06 +0000 Subject: [PATCH] OAuth 2.0 Mutual-TLS Support Provide the option to use mutual TLS client authentication when accessing external servers from Tacker. Oauth2MtlsAuthHandle has been added to support Mutual-TLS client authentication for access from Tacker to external NFVO servers and notification endpoints using user-provided Mutual-TLS client certificates. Implements: blueprint support-oauth2-mtls Change-Id: Ib1b33bccac85ba8c68aeebd460876bb38a4917fa --- .zuul.yaml | 1 + ...-support-oauth2-mtls-1ef757cca82059cf.yaml | 10 ++ tacker/alarm_receiver.py | 2 - .../api/schemas/common_types.py | 19 +++- .../sol_refactored/api/schemas/vnflcm_v2.py | 20 +++- tacker/sol_refactored/common/config.py | 22 ++++- tacker/sol_refactored/common/exceptions.py | 4 + tacker/sol_refactored/common/http_client.py | 82 ++++++++++++++++- tacker/sol_refactored/common/pm_job_utils.py | 6 ++ .../common/subscription_utils.py | 74 +++++++++++++-- tacker/sol_refactored/controller/vnffm_v1.py | 48 ++-------- tacker/sol_refactored/controller/vnflcm_v2.py | 34 +------ tacker/sol_refactored/controller/vnfpm_v2.py | 42 +-------- tacker/sol_refactored/nfvo/nfvo_client.py | 18 +++- .../common/subscription_authentication.py | 32 +++++++ .../common/test_pm_job_utils.py | 34 ++++--- .../common/test_subscription_utils.py | 82 +++++++++++++++++ .../controller/test_vnffm_v1.py | 84 +++++++++++++++-- .../controller/test_vnflcm_v2.py | 91 ++++++++++++++++--- .../controller/test_vnfpm_v2.py | 45 --------- .../sol_refactored/nfvo/test_nfvo_client.py | 63 ++++++++++++- 21 files changed, 598 insertions(+), 215 deletions(-) create mode 100644 releasenotes/notes/bp-support-oauth2-mtls-1ef757cca82059cf.yaml diff --git a/.zuul.yaml b/.zuul.yaml index a147f0c7b..649418456 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -349,6 +349,7 @@ token_endpoint: http://127.0.0.1:9990 client_id: 229ec984de7547b2b662e968961af5a4 client_password: devstack + use_client_secret_basic: True tox_envlist: dsvm-functional-sol-separated-nfvo-v2 # TODO(manpreetk): As per the 2023.1 testing runtime, we need to run atleast diff --git a/releasenotes/notes/bp-support-oauth2-mtls-1ef757cca82059cf.yaml b/releasenotes/notes/bp-support-oauth2-mtls-1ef757cca82059cf.yaml new file mode 100644 index 000000000..354cad201 --- /dev/null +++ b/releasenotes/notes/bp-support-oauth2-mtls-1ef757cca82059cf.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + [`blueprint support-oauth2-mtls `_] + Provide the option to use mutual TLS client authentication when accessing + external servers from Tacker. OAuth2MtlsAuthHandle has been added to + support Mutual-TLS client authentication for access from Tacker to external + NFVO servers and notification endpoints using user-provided Mutual-TLS + client certificates. + diff --git a/tacker/alarm_receiver.py b/tacker/alarm_receiver.py index 9094b4b6d..6a104ee84 100644 --- a/tacker/alarm_receiver.py +++ b/tacker/alarm_receiver.py @@ -17,7 +17,6 @@ from urllib import parse from oslo_config import cfg from oslo_log import log as logging from oslo_serialization import jsonutils -from oslo_utils import strutils from tacker._i18n import _ from tacker.vnfm.monitor_drivers.token import Token @@ -49,7 +48,6 @@ def config_opts(): class AlarmReceiver(wsgi.Middleware): def process_request(self, req): - LOG.debug('Process request: %s', strutils.mask_password(req)) if req.method != 'POST': return url = req.url diff --git a/tacker/sol_refactored/api/schemas/common_types.py b/tacker/sol_refactored/api/schemas/common_types.py index 0db86b656..feef8b823 100644 --- a/tacker/sol_refactored/api/schemas/common_types.py +++ b/tacker/sol_refactored/api/schemas/common_types.py @@ -122,7 +122,7 @@ _IpAddresses = { 'additionalProperties': True } -# SOL013 8.3.4 +# SOL013 v3.5.1 8.3.4 SubscriptionAuthentication = { 'type': 'object', 'properties': { @@ -133,7 +133,7 @@ SubscriptionAuthentication = { 'enum': [ 'BASIC', 'OAUTH2_CLIENT_CREDENTIALS', - 'TLS_CERT'] + 'OAUTH2_CLIENT_CERT'] } }, 'paramsBasic': { @@ -150,6 +150,21 @@ SubscriptionAuthentication = { 'clientPassword': {'type': 'string'}, 'tokenEndpoint': {'type': 'string'} } + }, + 'paramsOauth2ClientCert': { + 'type': 'object', + 'properties': { + 'clientId': {'type': 'string'}, + 'certificateRef': {'type': 'object', + 'properties': { + 'type': {'type': 'string'}, + 'value': {'type': 'string'} + }, + 'required': ['type', 'value'] + }, + 'tokenEndpoint': {'type': 'string'} + }, + 'required': ['clientId', 'certificateRef', 'tokenEndpoint'] } }, 'required': ['authType'], diff --git a/tacker/sol_refactored/api/schemas/vnflcm_v2.py b/tacker/sol_refactored/api/schemas/vnflcm_v2.py index b6f41f4f4..557dde940 100644 --- a/tacker/sol_refactored/api/schemas/vnflcm_v2.py +++ b/tacker/sol_refactored/api/schemas/vnflcm_v2.py @@ -157,7 +157,7 @@ ChangeExtVnfConnectivityRequest_V200 = { 'additionalProperties': True, } -# SOL013 8.3.4 +# SOL013 v3.5.1 8.3.4 _SubscriptionAuthentication = { 'type': 'object', 'properties': { @@ -168,7 +168,7 @@ _SubscriptionAuthentication = { 'enum': [ 'BASIC', 'OAUTH2_CLIENT_CREDENTIALS', - 'TLS_CERT'] + 'OAUTH2_CLIENT_CERT'] } }, 'paramsBasic': { @@ -191,6 +191,22 @@ _SubscriptionAuthentication = { # NOTE: must be specified since the way to specify them out of # band is not supported. 'required': ['clientId', 'clientPassword', 'tokenEndpoint'] + }, + 'paramsOauth2ClientCert': { + 'type': 'object', + 'properties': { + 'clientId': {'type': 'string'}, + 'certificateRef': { + 'type': 'object', + 'properties': { + 'type': {'type': 'string'}, + 'value': {'type': 'string'} + }, + 'required': ['type', 'value'] + }, + 'tokenEndpoint': {'type': 'string'} + }, + 'required': ['clientId', 'certificateRef', 'tokenEndpoint'] } }, 'required': ['authType'], diff --git a/tacker/sol_refactored/common/config.py b/tacker/sol_refactored/common/config.py index 25f7754aa..a8e163fdd 100644 --- a/tacker/sol_refactored/common/config.py +++ b/tacker/sol_refactored/common/config.py @@ -50,6 +50,14 @@ VNFM_OPTS = [ default=0, # 0 means no paging help=_('Paged response size of the query result ' 'for VNF LCM operation occurrences.')), + cfg.StrOpt('notification_mtls_ca_cert_file', + default='', + help=_('CA Certificate file used by OAuth2.0 mTLS ' + 'authentication.')), + cfg.StrOpt('notification_mtls_client_cert_file', + default='', + help=_('Client Certificate file used by OAuth2.0 mTLS ' + 'authentication.')), cfg.IntOpt('notify_connect_retries', default=0, # 0 means no retry help=_('Number of retries that should be attempted for ' @@ -121,13 +129,25 @@ NFVO_OPTS = [ cfg.StrOpt('vnf_package_cache_dir', default='/opt/stack/data/tacker/vnf_package_cache', help=_('Vnf package content cache directory.')), + cfg.StrOpt('mtls_ca_cert_file', + default='', + help=_('CA Certificate file used by OAuth2.0 mTLS ' + 'authentication.')), + cfg.StrOpt('mtls_client_cert_file', + default='', + help=_('Client Certificate file used by OAuth2.0 mTLS ' + 'authentication.')), cfg.BoolOpt('test_callback_uri', default=True, help=_('Check to get notification from callback Uri.')), cfg.ListOpt('test_grant_zone_list', default=["nova"], help=_('Zones used for test which returned in Grant ' - 'response.')) + 'response.')), + cfg.BoolOpt('use_client_secret_basic', + default=False, + help=_('Use password authenticatiojn if True, ' + 'use certificate authentication if False.')) ] CONF.register_opts(NFVO_OPTS, 'v2_nfvo') diff --git a/tacker/sol_refactored/common/exceptions.py b/tacker/sol_refactored/common/exceptions.py index b7a2f89f4..be65b22d2 100644 --- a/tacker/sol_refactored/common/exceptions.py +++ b/tacker/sol_refactored/common/exceptions.py @@ -378,6 +378,10 @@ class OIDCAuthFailed(SolHttpError400): " Detail: %(detail)s") +class AuthTypeNotFound(SolHttpError400): + message = _("AuthType parameter %(auth_type)s is not found") + + class HelmOperationFailed(SolHttpError422): title = 'Helm operation failed' # detail set in the code diff --git a/tacker/sol_refactored/common/http_client.py b/tacker/sol_refactored/common/http_client.py index 01fdb6234..a2d08ab5b 100644 --- a/tacker/sol_refactored/common/http_client.py +++ b/tacker/sol_refactored/common/http_client.py @@ -15,6 +15,7 @@ import abc +import urllib from keystoneauth1 import adapter from keystoneauth1 import http_basic @@ -188,10 +189,10 @@ class NoAuthHandle(AuthHandle): return session.Session(auth=auth, verify=False) -class Oauth2AuthPlugin(plugin.FixedEndpointPlugin): +class OAuth2AuthPlugin(plugin.FixedEndpointPlugin): def __init__(self, endpoint, token_endpoint, client_id, client_password): - super(Oauth2AuthPlugin, self).__init__(endpoint) + super(OAuth2AuthPlugin, self).__init__(endpoint) self.token_endpoint = token_endpoint self.client_id = client_id self.client_password = client_password @@ -230,10 +231,85 @@ class OAuth2AuthHandle(AuthHandle): self.client_password = client_password def get_auth(self, context=None): - return Oauth2AuthPlugin(self.endpoint, self.token_endpoint, + return OAuth2AuthPlugin(self.endpoint, self.token_endpoint, self.client_id, self.client_password) def get_session(self, auth, service_type): _session = session.Session(auth=auth, verify=False) return adapter.Adapter(session=_session, service_type=service_type) + + +class CertAuthMtlsHandle(AuthHandle): + + def __init__(self, endpoint, verify_cert, client_cert): + self.endpoint = endpoint + self.verify_cert = verify_cert + self.client_cert = client_cert + + def get_auth(self, context=None): + return noauth.NoAuth(endpoint=self.endpoint) + + def get_session(self, auth, service_type): + return session.Session(auth=auth, verify=self.verify_cert, + cert=self.client_cert) + + +class OAuth2MtlsAuthPlugin(plugin.FixedEndpointPlugin): + + def __init__(self, endpoint, token_endpoint, client_id, + verify_cert, client_cert): + super(OAuth2MtlsAuthPlugin, self).__init__(endpoint) + self.token_endpoint = token_endpoint + self.client_id = client_id + self.verify_cert = verify_cert + self.client_cert = client_cert + + def get_token(self, session, **kwargs): + auth = CertAuthMtlsHandle(self.endpoint, self.verify_cert, + self.client_cert) + client = HttpClient(auth) + + url = f'{self.token_endpoint}' + data = { + 'grant_type': 'client_credentials', + 'client_id': self.client_id + } + data = urllib.parse.urlencode(data) + + resp, resp_body = client.do_request(url, "POST", + body=data, content_type='application/x-www-form-urlencoded') + + if resp.status_code != 200: + LOG.error("get OAuth2 mTLS token failed: %d" % resp.status_code) + return + + return resp_body['access_token'] + + def get_headers(self, session, **kwargs): + token = self.get_token(session) + if not token: + return None + auth = 'Bearer %s' % token + return {'Authorization': auth} + + +class OAuth2MtlsAuthHandle(AuthHandle): + + def __init__(self, endpoint, token_endpoint, client_id, + verify_cert, client_cert): + self.endpoint = endpoint + self.token_endpoint = token_endpoint + self.client_id = client_id + self.verify_cert = verify_cert + self.client_cert = client_cert + + def get_auth(self, context=None): + return OAuth2MtlsAuthPlugin(self.endpoint, self.token_endpoint, + self.client_id, self.verify_cert, self.client_cert) + + def get_session(self, auth, service_type): + _session = session.Session(auth=auth, verify=self.verify_cert, + cert=self.client_cert) + return adapter.Adapter(session=_session, + service_type=service_type) diff --git a/tacker/sol_refactored/common/pm_job_utils.py b/tacker/sol_refactored/common/pm_job_utils.py index ae021c989..9504e27d8 100644 --- a/tacker/sol_refactored/common/pm_job_utils.py +++ b/tacker/sol_refactored/common/pm_job_utils.py @@ -103,6 +103,12 @@ def _get_notification_auth_handle(pm_job): param = pm_job.authentication.paramsOauth2ClientCredentials return http_client.OAuth2AuthHandle( None, param.tokenEndpoint, param.clientId, param.clientPassword) + if pm_job.authentication.obj_attr_is_set('paramsOauth2ClientCert'): + param = pm_job.authentication.paramsOauth2ClientCert + verify_cert = CONF.v2_vnfm.notification_mtls_ca_cert_file + client_cert = CONF.v2_vnfm.notification_mtls_client_cert_file + return http_client.OAuth2MtlsAuthHandle(None, + param.tokenEndpoint, param.clientId, verify_cert, client_cert) return None diff --git a/tacker/sol_refactored/common/subscription_utils.py b/tacker/sol_refactored/common/subscription_utils.py index 4036779fd..654e48204 100644 --- a/tacker/sol_refactored/common/subscription_utils.py +++ b/tacker/sol_refactored/common/subscription_utils.py @@ -51,16 +51,28 @@ def subsc_href(subsc_id, endpoint): def _get_notification_auth_handle(subsc): - if not subsc.obj_attr_is_set('authentication'): + auth_req = subsc.get('authentication', None) + if auth_req: + auth = objects.SubscriptionAuthentication( + authType=auth_req['authType'] + ) + if 'OAUTH2_CLIENT_CERT' in auth.authType: + param = subsc.authentication.paramsOauth2ClientCert + verify_cert = CONF.v2_vnfm.notification_mtls_ca_cert_file + client_cert = CONF.v2_vnfm.notification_mtls_client_cert_file + return http_client.OAuth2MtlsAuthHandle(None, + param.tokenEndpoint, param.clientId, verify_cert, client_cert) + elif 'OAUTH2_CLIENT_CREDENTIALS' in auth.authType: + param = subsc.authentication.paramsOauth2ClientCredentials + return http_client.OAuth2AuthHandle(None, + param.tokenEndpoint, param.clientId, param.clientPassword) + elif 'BASIC' in auth.authType: + param = subsc.authentication.paramsBasic + return http_client.BasicAuthHandle(param.userName, param.password) + else: + raise sol_ex.AuthTypeNotFound(auth.authType) + else: return http_client.NoAuthHandle() - elif subsc.authentication.obj_attr_is_set('paramsBasic'): - param = subsc.authentication.paramsBasic - return http_client.BasicAuthHandle(param.userName, param.password) - elif subsc.authentication.obj_attr_is_set( - 'paramsOauth2ClientCredentials'): - param = subsc.authentication.paramsOauth2ClientCredentials - return http_client.OAuth2AuthHandle(None, - param.tokenEndpoint, param.clientId, param.clientPassword) # not reach here @@ -273,3 +285,47 @@ def make_delete_inst_notif_data(subsc, inst, endpoint): # vnfLcmOpOcc: is not necessary ) return notif_data + + +def check_http_client_auth(auth_req): + auth = objects.SubscriptionAuthentication( + authType=auth_req['authType'] + ) + if 'OAUTH2_CLIENT_CERT' in auth.authType: + oauth2_mtls_req = auth_req.get('paramsOauth2ClientCert') + if oauth2_mtls_req is None: + msg = "paramsOauth2ClientCert must be specified." + raise sol_ex.InvalidSubscription(sol_detail=msg) + auth.paramsOauth2ClientCert = ( + objects.SubscriptionAuthentication_ParamsOauth2ClientCert( + clientId=oauth2_mtls_req.get('clientId'), + certificateRef=oauth2_mtls_req.get('certificateRef'), + tokenEndpoint=oauth2_mtls_req.get('tokenEndpoint') + ) + ) + elif 'OAUTH2_CLIENT_CREDENTIALS' in auth.authType: + oauth2_req = auth_req.get('paramsOauth2ClientCredentials') + if oauth2_req is None: + msg = "paramsOauth2ClientCredentials must be specified." + raise sol_ex.InvalidSubscription(sol_detail=msg) + auth.paramsOauth2ClientCredentials = ( + objects.SubscriptionAuthentication_ParamsOauth2( + clientId=oauth2_req.get('clientId'), + clientPassword=oauth2_req.get('clientPassword'), + tokenEndpoint=oauth2_req.get('tokenEndpoint') + ) + ) + elif 'BASIC' in auth.authType: + basic_req = auth_req.get('paramsBasic') + if basic_req is None: + msg = "ParamsBasic must be specified." + raise sol_ex.InvalidSubscription(sol_detail=msg) + auth.paramsBasic = ( + objects.SubscriptionAuthentication_ParamsBasic( + userName=basic_req.get('userName'), + password=basic_req.get('password') + ) + ) + else: + raise sol_ex.AuthTypeNotFound(auth.authType) + return auth diff --git a/tacker/sol_refactored/controller/vnffm_v1.py b/tacker/sol_refactored/controller/vnffm_v1.py index de4ad7dae..093175d1d 100644 --- a/tacker/sol_refactored/controller/vnffm_v1.py +++ b/tacker/sol_refactored/controller/vnffm_v1.py @@ -24,7 +24,9 @@ from tacker.sol_refactored.common import config from tacker.sol_refactored.common import coordinate from tacker.sol_refactored.common import exceptions as sol_ex from tacker.sol_refactored.common import fm_alarm_utils -from tacker.sol_refactored.common import fm_subscription_utils as subsc_utils +from tacker.sol_refactored.common import fm_subscription_utils\ + as fm_subsc_utils +from tacker.sol_refactored.common import subscription_utils as subsc_utils from tacker.sol_refactored.controller import vnffm_view from tacker.sol_refactored.nfvo import nfvo_client from tacker.sol_refactored import objects @@ -117,47 +119,15 @@ class VnfFmControllerV1(sol_wsgi.SolAPIController): auth_req = body.get('authentication') if auth_req: - auth = objects.SubscriptionAuthentication( - authType=auth_req['authType'] - ) - if 'BASIC' in auth.authType: - basic_req = auth_req.get('paramsBasic') - if basic_req is None: - msg = "ParamsBasic must be specified." - raise sol_ex.InvalidSubscription(sol_detail=msg) - auth.paramsBasic = ( - objects.SubscriptionAuthentication_ParamsBasic( - userName=basic_req.get('userName'), - password=basic_req.get('password') - ) - ) - - if 'OAUTH2_CLIENT_CREDENTIALS' in auth.authType: - oauth2_req = auth_req.get('paramsOauth2ClientCredentials') - if oauth2_req is None: - msg = "paramsOauth2ClientCredentials must be specified." - raise sol_ex.InvalidSubscription(sol_detail=msg) - auth.paramsOauth2ClientCredentials = ( - objects.SubscriptionAuthentication_ParamsOauth2( - clientId=oauth2_req.get('clientId'), - clientPassword=oauth2_req.get('clientPassword'), - tokenEndpoint=oauth2_req.get('tokenEndpoint') - ) - ) - - if 'TLS_CERT' in auth.authType: - msg = "'TLS_CERT' is not supported at the moment." - raise sol_ex.InvalidSubscription(sol_detail=msg) - - subsc.authentication = auth + subsc.authentication = subsc_utils.check_http_client_auth(auth_req) if CONF.v2_nfvo.test_callback_uri: - subsc_utils.test_notification(subsc) + fm_subsc_utils.test_notification(subsc) subsc.create(context) resp_body = self._subsc_view.detail(subsc) - self_href = subsc_utils.subsc_href(subsc.id, self.endpoint) + self_href = fm_subsc_utils.subsc_href(subsc.id, self.endpoint) return sol_wsgi.SolResponse( 201, resp_body, version=api_version.CURRENT_FM_VERSION, @@ -173,7 +143,7 @@ class VnfFmControllerV1(sol_wsgi.SolAPIController): page_size = CONF.v2_vnfm.subscription_page_size pager = self._subsc_view.parse_pager(request, page_size) - subscs = subsc_utils.get_subsc_all(request.context, + subscs = fm_subsc_utils.get_subsc_all(request.context, marker=pager.marker) resp_body = self._subsc_view.detail_list(subscs, filters, None, pager) @@ -183,7 +153,7 @@ class VnfFmControllerV1(sol_wsgi.SolAPIController): link=pager.get_link()) def subscription_show(self, request, id): - subsc = subsc_utils.get_subsc(request.context, id) + subsc = fm_subsc_utils.get_subsc(request.context, id) resp_body = self._subsc_view.detail(subsc) @@ -192,7 +162,7 @@ class VnfFmControllerV1(sol_wsgi.SolAPIController): def subscription_delete(self, request, id): context = request.context - subsc = subsc_utils.get_subsc(request.context, id) + subsc = fm_subsc_utils.get_subsc(request.context, id) subsc.delete(context) diff --git a/tacker/sol_refactored/controller/vnflcm_v2.py b/tacker/sol_refactored/controller/vnflcm_v2.py index c07d2a9ad..67ab40108 100644 --- a/tacker/sol_refactored/controller/vnflcm_v2.py +++ b/tacker/sol_refactored/controller/vnflcm_v2.py @@ -486,39 +486,7 @@ class VnfLcmControllerV2(sol_wsgi.SolAPIController): auth_req = body.get('authentication') if auth_req: - auth = objects.SubscriptionAuthentication( - authType=auth_req['authType'] - ) - if 'BASIC' in auth.authType: - basic_req = auth_req.get('paramsBasic') - if basic_req is None: - msg = "ParamsBasic must be specified." - raise sol_ex.InvalidSubscription(sol_detail=msg) - auth.paramsBasic = ( - objects.SubscriptionAuthentication_ParamsBasic( - userName=basic_req.get('userName'), - password=basic_req.get('password') - ) - ) - - if 'OAUTH2_CLIENT_CREDENTIALS' in auth.authType: - oauth2_req = auth_req.get('paramsOauth2ClientCredentials') - if oauth2_req is None: - msg = "paramsOauth2ClientCredentials must be specified." - raise sol_ex.InvalidSubscription(sol_detail=msg) - auth.paramsOauth2ClientCredentials = ( - objects.SubscriptionAuthentication_ParamsOauth2( - clientId=oauth2_req.get('clientId'), - clientPassword=oauth2_req.get('clientPassword'), - tokenEndpoint=oauth2_req.get('tokenEndpoint') - ) - ) - - if 'TLS_CERT' in auth.authType: - msg = "'TLS_CERT' is not supported at the moment." - raise sol_ex.InvalidSubscription(sol_detail=msg) - - subsc.authentication = auth + subsc.authentication = subsc_utils.check_http_client_auth(auth_req) if CONF.v2_nfvo.test_callback_uri: subsc_utils.test_notification(subsc) diff --git a/tacker/sol_refactored/controller/vnfpm_v2.py b/tacker/sol_refactored/controller/vnfpm_v2.py index a25ca38b2..9ad2a5040 100644 --- a/tacker/sol_refactored/controller/vnfpm_v2.py +++ b/tacker/sol_refactored/controller/vnfpm_v2.py @@ -28,6 +28,7 @@ from tacker.sol_refactored.common import coordinate from tacker.sol_refactored.common import exceptions as sol_ex from tacker.sol_refactored.common import monitoring_plugin_base as plugin from tacker.sol_refactored.common import pm_job_utils +from tacker.sol_refactored.common import subscription_utils as subsc_utils from tacker.sol_refactored.common import vnf_instance_utils as inst_utils from tacker.sol_refactored.controller import vnfpm_view from tacker.sol_refactored.nfvo import nfvo_client @@ -58,42 +59,6 @@ OBJ_TYPE_TO_METRIC_LISt = { } -def _check_http_client_auth(auth_req): - auth = objects.SubscriptionAuthentication( - authType=auth_req['authType'] - ) - - if 'BASIC' in auth.authType: - basic_req = auth_req.get('paramsBasic') - if basic_req is None: - msg = "ParamsBasic must be specified." - raise sol_ex.InvalidSubscription(sol_detail=msg) - auth.paramsBasic = ( - objects.SubscriptionAuthentication_ParamsBasic( - userName=basic_req.get('userName'), - password=basic_req.get('password') - ) - ) - - if 'OAUTH2_CLIENT_CREDENTIALS' in auth.authType: - oauth2_req = auth_req.get('paramsOauth2ClientCredentials') - if oauth2_req is None: - msg = "paramsOauth2ClientCredentials must be specified." - raise sol_ex.InvalidSubscription(sol_detail=msg) - auth.paramsOauth2ClientCredentials = ( - objects.SubscriptionAuthentication_ParamsOauth2( - clientId=oauth2_req.get('clientId'), - clientPassword=oauth2_req.get('clientPassword'), - tokenEndpoint=oauth2_req.get('tokenEndpoint') - ) - ) - - if 'TLS_CERT' in auth.authType: - msg = "'TLS_CERT' is not supported at the moment." - raise sol_ex.InvalidSubscription(sol_detail=msg) - return auth - - def _check_performance_metric_or_group( obj_type, metric_group, performance_metric): # Check whether the object_type is consistent with the corresponding @@ -186,7 +151,8 @@ class VnfPmControllerV2(sol_wsgi.SolAPIController): # authentication auth_req = body.get('authentication') if auth_req: - pm_job.authentication = _check_http_client_auth(auth_req) + pm_job.authentication = subsc_utils.check_http_client_auth( + auth_req) # metadata metadata = body.get('metadata') @@ -246,7 +212,7 @@ class VnfPmControllerV2(sol_wsgi.SolAPIController): if body.get("callbackUri"): pm_job.callbackUri = body.get("callbackUri") if body.get("authentication"): - pm_job.authentication = _check_http_client_auth( + pm_job.authentication = subsc_utils.check_http_client_auth( body.get("authentication")) if CONF.v2_nfvo.test_callback_uri: diff --git a/tacker/sol_refactored/nfvo/nfvo_client.py b/tacker/sol_refactored/nfvo/nfvo_client.py index ddb47f493..d7c282dfa 100644 --- a/tacker/sol_refactored/nfvo/nfvo_client.py +++ b/tacker/sol_refactored/nfvo/nfvo_client.py @@ -47,11 +47,19 @@ class NfvoClient(object): if CONF.v2_nfvo.use_external_nfvo: self.is_local = False self.endpoint = CONF.v2_nfvo.endpoint - auth_handle = http_client.OAuth2AuthHandle( - self.endpoint, - CONF.v2_nfvo.token_endpoint, - CONF.v2_nfvo.client_id, - CONF.v2_nfvo.client_password) + if CONF.v2_nfvo.use_client_secret_basic: + auth_handle = http_client.OAuth2AuthHandle( + self.endpoint, + CONF.v2_nfvo.token_endpoint, + CONF.v2_nfvo.client_id, + CONF.v2_nfvo.client_password) + else: + auth_handle = http_client.OAuth2MtlsAuthHandle( + self.endpoint, + CONF.v2_nfvo.token_endpoint, + CONF.v2_nfvo.client_id, + CONF.v2_nfvo.mtls_ca_cert_file, + CONF.v2_nfvo.mtls_client_cert_file) self.client = http_client.HttpClient(auth_handle) self.grant_api_version = CONF.v2_nfvo.grant_api_version self.vnfpkgm_api_version = CONF.v2_nfvo.vnfpkgm_api_version diff --git a/tacker/sol_refactored/objects/common/subscription_authentication.py b/tacker/sol_refactored/objects/common/subscription_authentication.py index aad049c56..9b94dd808 100644 --- a/tacker/sol_refactored/objects/common/subscription_authentication.py +++ b/tacker/sol_refactored/objects/common/subscription_authentication.py @@ -31,6 +31,7 @@ class SubscriptionAuthentication(base.TackerObject, valid_values=[ 'BASIC', 'OAUTH2_CLIENT_CREDENTIALS', + 'OAUTH2_CLIENT_CERT', 'TLS_CERT', ], nullable=False), @@ -38,6 +39,9 @@ class SubscriptionAuthentication(base.TackerObject, 'SubscriptionAuthentication_ParamsBasic', nullable=True), 'paramsOauth2ClientCredentials': fields.ObjectField( 'SubscriptionAuthentication_ParamsOauth2', nullable=True), + 'paramsOauth2ClientCert': fields.ObjectField( + 'SubscriptionAuthentication_ParamsOauth2ClientCert', + nullable=True), } @@ -70,3 +74,31 @@ class SubscriptionAuthentication_ParamsOauth2( 'clientPassword': fields.StringField(nullable=True), 'tokenEndpoint': fields.UriField(nullable=True), } + + +@base.TackerObjectRegistry.register +class SubscriptionAuthentication_ParamsOauth2ClientCert( + base.TackerObject, base.TackerObjectDictCompat): + + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'clientId': fields.StringField(nullable=False), + 'cerficateRef': fields.ObjectField( + 'ParamsOauth2ClientCert_CertificateRef', nullable=False), + 'tokenEndpoint': fields.UriField(nullable=False), + } + + +@base.TackerObjectRegistry.register +class ParamsOauth2ClientCert_CertificateRef( + base.TackerObject, base.TackerObjectDictCompat): + + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'type': fields.StringField(nullable=False), + 'value': fields.StringField(nullable=False), + } diff --git a/tacker/tests/unit/sol_refactored/common/test_pm_job_utils.py b/tacker/tests/unit/sol_refactored/common/test_pm_job_utils.py index b916b6315..6eb0c737d 100644 --- a/tacker/tests/unit/sol_refactored/common/test_pm_job_utils.py +++ b/tacker/tests/unit/sol_refactored/common/test_pm_job_utils.py @@ -136,9 +136,7 @@ class TestPmJobUtils(base.BaseTestCase): id='pm_job_1', authentication=pm_job_1_auth) result = pm_job_utils._get_notification_auth_handle(pm_job_1) - res = type(result).__name__ - name = type(http_client.BasicAuthHandle('test', 'test')).__name__ - self.assertEqual(name, res) + self.assertIsInstance(result, http_client.BasicAuthHandle) pm_job_2 = objects.PmJobV2( id='pm_job_2', @@ -153,19 +151,27 @@ class TestPmJobUtils(base.BaseTestCase): ) ) result = pm_job_utils._get_notification_auth_handle(pm_job_2) - res = type(result).__name__ - name = type(http_client.OAuth2AuthHandle( - None, 'tokenEndpoint', 'test', 'test')).__name__ - self.assertEqual(name, res) + self.assertIsInstance(result, http_client.OAuth2AuthHandle) - pm_job_3 = objects.PmJobV2(id='pm_job_3', - authentication=( - objects.SubscriptionAuthentication( - authType=["TLS_CERT"], - )) - ) + pm_job_3 = objects.PmJobV2( + id='pm_job_3', + authentication=objects.SubscriptionAuthentication( + authType=["OAUTH2_CLIENT_CERT"], + paramsOauth2ClientCert=( + objects.SubscriptionAuthentication_ParamsOauth2ClientCert( + clientId='test', + certificateRef=objects. + ParamsOauth2ClientCert_CertificateRef( + type='x5t#256', + value='03c6e188d1fe5d3da8c9bc9a8dc531a2' + 'b3ecf812b03aede9bec7ba1b410b6b64' + ), + tokenEndpoint='http://127.0.0.1/token' + )) + ) + ) result = pm_job_utils._get_notification_auth_handle(pm_job_3) - self.assertEqual(None, result) + self.assertIsInstance(result, http_client.OAuth2MtlsAuthHandle) @mock.patch.object(http_client.HttpClient, 'do_request') def test_test_notification(self, mock_do_request): diff --git a/tacker/tests/unit/sol_refactored/common/test_subscription_utils.py b/tacker/tests/unit/sol_refactored/common/test_subscription_utils.py index f029f3685..62a1d3866 100644 --- a/tacker/tests/unit/sol_refactored/common/test_subscription_utils.py +++ b/tacker/tests/unit/sol_refactored/common/test_subscription_utils.py @@ -74,6 +74,7 @@ class TestSubscriptionUtils(base.BaseTestCase): id='sub-2', verbosity='SHORT', callbackUri='http://127.0.0.1/callback', authentication=objects.SubscriptionAuthentication( + authType=['BASIC'], paramsBasic=objects.SubscriptionAuthentication_ParamsBasic( userName='test', password='test'))) @@ -84,6 +85,7 @@ class TestSubscriptionUtils(base.BaseTestCase): id='sub-3', verbosity='SHORT', callbackUri='http://127.0.0.1/callback', authentication=objects.SubscriptionAuthentication( + authType=['OAUTH2_CLIENT_CREDENTIALS'], paramsOauth2ClientCredentials=( objects.SubscriptionAuthentication_ParamsOauth2( clientId='test', clientPassword='test', @@ -92,6 +94,25 @@ class TestSubscriptionUtils(base.BaseTestCase): # execute oauth2 subsc_utils.send_notification(subsc_oauth2, notif_data_no_auth) + subsc_oauth2_mtls = objects.LccnSubscriptionV2( + id='sub-4', verbosity='SHORT', + callbackUri='http://127.0.0.1/callback', + authentication=objects.SubscriptionAuthentication( + authType=["OAUTH2_CLIENT_CERT"], + paramsOauth2ClientCert=( + objects.SubscriptionAuthentication_ParamsOauth2ClientCert( + clientId='test', + certificateRef=objects. + ParamsOauth2ClientCert_CertificateRef( + type='x5t#256', + value='03c6e188d1fe5d3da8c9bc9a8dc531a2' + 'b3ecf812b03aede9bec7ba1b410b6b64' + ), + tokenEndpoint='http://127.0.0.1/token')))) + + # execute oauth2 mtls + subsc_utils.send_notification(subsc_oauth2_mtls, notif_data_no_auth) + @mock.patch.object(http_client.HttpClient, 'do_request') def test_send_notification_error_code(self, mock_resp): subsc_no_auth = objects.LccnSubscriptionV2( @@ -389,3 +410,64 @@ class TestSubscriptionUtils(base.BaseTestCase): self.assertEqual('subsc-1', result.subscriptionId) self.assertEqual('test-instance', result.vnfInstanceId) + + def test_check_http_client_auth(self): + auth_req_1 = { + 'authType': ['BASIC'], + 'paramsBasic': { + 'userName': 'test', + 'password': 'test' + }, + } + result = subsc_utils.check_http_client_auth(auth_req_1) + self.assertEqual(['BASIC'], result.authType) + + auth_req_2 = { + 'authType': ['OAUTH2_CLIENT_CREDENTIALS'], + 'paramsOauth2ClientCredentials': { + 'clientId': 'test', + 'clientPassword': 'test', + 'tokenEndpoint': + 'http://127.0.0.1/token' + } + } + result = subsc_utils.check_http_client_auth(auth_req_2) + self.assertEqual(['OAUTH2_CLIENT_CREDENTIALS'], result.authType) + + auth_req_3 = { + 'authType': ['OAUTH2_CLIENT_CERT'], + 'paramsOauth2ClientCert': { + 'clientId': 'test', + 'certificateRef': { + 'type': 'x5t#256', + 'value': '03c6e188d1fe5d3da8c9bc9a8dc531a2' + 'b3ecf812b03aede9bec7ba1b410b6b64' + }, + 'tokenEndpoint': 'http://127.0.0.1/token' + } + } + result = subsc_utils.check_http_client_auth(auth_req_3) + self.assertEqual(['OAUTH2_CLIENT_CERT'], result.authType) + + def test_check_http_client_auth_error(self): + auth_req_1 = { + 'authType': ['BASIC'], + 'paramsBasic': None + } + self.assertRaises(sol_ex.InvalidSubscription, + subsc_utils.check_http_client_auth, + auth_req=auth_req_1) + + auth_req_2 = { + 'authType': ['OAUTH2_CLIENT_CREDENTIALS'], + } + self.assertRaises(sol_ex.InvalidSubscription, + subsc_utils.check_http_client_auth, + auth_req=auth_req_2) + + auth_req_3 = { + 'authType': ['OAUTH2_CLIENT_CERT'] + } + self.assertRaises(sol_ex.InvalidSubscription, + subsc_utils.check_http_client_auth, + auth_req=auth_req_3) diff --git a/tacker/tests/unit/sol_refactored/controller/test_vnffm_v1.py b/tacker/tests/unit/sol_refactored/controller/test_vnffm_v1.py index a3230c380..efb0d1fc4 100644 --- a/tacker/tests/unit/sol_refactored/controller/test_vnffm_v1.py +++ b/tacker/tests/unit/sol_refactored/controller/test_vnffm_v1.py @@ -111,14 +111,61 @@ class TestVnffmV1(base.BaseTestCase): @mock.patch.object(objects.base.TackerPersistentObject, 'create') @mock.patch.object(subsc_utils, 'test_notification') def test_subscription_create(self, mock_test, mock_create): - body = { + body_1 = { "callbackUri": "http://127.0.0.1:6789/notification", "authentication": { - "authType": ["BASIC", "OAUTH2_CLIENT_CREDENTIALS"], + "authType": ["BASIC", "OAUTH2_CLIENT_CREDENTIALS", + "OAUTH2_CLIENT_CERT"], "paramsBasic": { "userName": "test", "password": "test" }, + "paramsOauth2ClientCredentials": { + "clientId": "test", + "clientPassword": "test", + "tokenEndpoint": "https://127.0.0.1/token" + }, + "paramsOauth2ClientCert": { + "clientId": "test", + "certificateRef": { + "type": "x5t#256", + "value": "03c6e188d1fe5d3da8c9bc9a8dc531a2" + "b3ecf812b03aede9bec7ba1b410b6b64" + }, + "tokenEndpoint": "https://127.0.0.1/token" + } + }, + "filter": fakes_for_fm.fm_subsc_example['filter'] + } + result = self.controller.subscription_create( + request=self.request, body=body_1) + self.assertEqual(201, result.status) + self.assertEqual(body_1['callbackUri'], result.body['callbackUri']) + self.assertEqual(body_1['filter'], result.body['filter']) + self.assertIsNone(result.body.get('authentication')) + + body_2 = { + "callbackUri": "http://127.0.0.1:6789/notification", + "authentication": { + "authType": ["BASIC"], + "paramsBasic": { + "userName": "test", + "password": "test" + } + }, + "filter": fakes_for_fm.fm_subsc_example['filter'] + } + result = self.controller.subscription_create( + request=self.request, body=body_2) + self.assertEqual(201, result.status) + self.assertEqual(body_2['callbackUri'], result.body['callbackUri']) + self.assertEqual(body_2['filter'], result.body['filter']) + self.assertIsNone(result.body.get('authentication')) + + body_3 = { + "callbackUri": "http://127.0.0.1:6789/notification", + "authentication": { + "authType": ["OAUTH2_CLIENT_CREDENTIALS"], "paramsOauth2ClientCredentials": { "clientId": "test", "clientPassword": "test", @@ -128,10 +175,33 @@ class TestVnffmV1(base.BaseTestCase): "filter": fakes_for_fm.fm_subsc_example['filter'] } result = self.controller.subscription_create( - request=self.request, body=body) + request=self.request, body=body_3) self.assertEqual(201, result.status) - self.assertEqual(body['callbackUri'], result.body['callbackUri']) - self.assertEqual(body['filter'], result.body['filter']) + self.assertEqual(body_3['callbackUri'], result.body['callbackUri']) + self.assertEqual(body_3['filter'], result.body['filter']) + self.assertIsNone(result.body.get('authentication')) + + body_4 = { + "callbackUri": "http://127.0.0.1:6789/notification", + "authentication": { + "authType": ["OAUTH2_CLIENT_CERT"], + "paramsOauth2ClientCert": { + "clientId": "test", + "certificateRef": { + "type": "x5t#256", + "value": "03c6e188d1fe5d3da8c9bc9a8dc531a2" + "b3ecf812b03aede9bec7ba1b410b6b64" + }, + "tokenEndpoint": "https://127.0.0.1/token" + } + }, + "filter": fakes_for_fm.fm_subsc_example['filter'] + } + result = self.controller.subscription_create( + request=self.request, body=body_4) + self.assertEqual(201, result.status) + self.assertEqual(body_4['callbackUri'], result.body['callbackUri']) + self.assertEqual(body_4['filter'], result.body['filter']) self.assertIsNone(result.body.get('authentication')) def test_invalid_subscripion(self): @@ -161,13 +231,13 @@ class TestVnffmV1(base.BaseTestCase): body = { "callbackUri": "http://127.0.0.1:6789/notification", "authentication": { - "authType": ["TLS_CERT"] + "authType": ["OAUTH2_CLIENT_CERT"] } } ex = self.assertRaises(sol_ex.InvalidSubscription, self.controller.subscription_create, request=self.request, body=body) - self.assertEqual("'TLS_CERT' is not supported at the moment.", + self.assertEqual("paramsOauth2ClientCert must be specified.", ex.detail) @mock.patch.object(subsc_utils, 'get_subsc_all') diff --git a/tacker/tests/unit/sol_refactored/controller/test_vnflcm_v2.py b/tacker/tests/unit/sol_refactored/controller/test_vnflcm_v2.py index a17b1364d..5a4e7d73b 100644 --- a/tacker/tests/unit/sol_refactored/controller/test_vnflcm_v2.py +++ b/tacker/tests/unit/sol_refactored/controller/test_vnflcm_v2.py @@ -539,8 +539,8 @@ class TestVnflcmV2(db_base.SqlTestCase): body=body) self.assertEqual(202, result.status) - def test_invalid_subscripion(self): - body = { + def test_invalid_subscription(self): + body_1 = { "callbackUri": "http://127.0.0.1:6789/notification", "authentication": { "authType": ["BASIC"] @@ -548,10 +548,10 @@ class TestVnflcmV2(db_base.SqlTestCase): } ex = self.assertRaises(sol_ex.InvalidSubscription, self.controller.subscription_create, request=self.request, - body=body) + body=body_1) self.assertEqual("ParamsBasic must be specified.", ex.detail) - body = { + body_2 = { "callbackUri": "http://127.0.0.1:6789/notification", "authentication": { "authType": ["OAUTH2_CLIENT_CREDENTIALS"] @@ -559,32 +559,77 @@ class TestVnflcmV2(db_base.SqlTestCase): } ex = self.assertRaises(sol_ex.InvalidSubscription, self.controller.subscription_create, request=self.request, - body=body) + body=body_2) self.assertEqual("paramsOauth2ClientCredentials must be specified.", ex.detail) - body = { + body_3 = { "callbackUri": "http://127.0.0.1:6789/notification", "authentication": { - "authType": ["TLS_CERT"] + "authType": ["OAUTH2_CLIENT_CERT"] } } ex = self.assertRaises(sol_ex.InvalidSubscription, self.controller.subscription_create, request=self.request, - body=body) - self.assertEqual("'TLS_CERT' is not supported at the moment.", + body=body_3) + self.assertEqual("paramsOauth2ClientCert must be specified.", ex.detail) @mock.patch.object(subsc_utils, 'test_notification') def test_subscription_create_201(self, mock_test): - body = { + body_1 = { "callbackUri": "http://127.0.0.1:6789/notification", "authentication": { - "authType": ["BASIC", "OAUTH2_CLIENT_CREDENTIALS"], + "authType": ["BASIC", "OAUTH2_CLIENT_CREDENTIALS", + "OAUTH2_CLIENT_CERT"], "paramsBasic": { "userName": "test", "password": "test" }, + "paramsOauth2ClientCredentials": { + "clientId": "test", + "clientPassword": "test", + "tokenEndpoint": "https://127.0.0.1/token" + }, + "paramsOauth2ClientCert": { + "clientId": "test", + "certificateRef": { + "type": "x5t#256", + "value": "03c6e188d1fe5d3da8c9bc9a8dc531a2" + "b3ecf812b03aede9bec7ba1b410b6b64" + }, + "tokenEndpoint": "https://127.0.0.1/token" + } + }, + "filter": { + "operationTypes": [fields.LcmOperationType.INSTANTIATE] + } + } + result = self.controller.subscription_create( + request=self.request, body=body_1) + self.assertEqual(201, result.status) + + body_2 = { + "callbackUri": "http://127.0.0.1:6789/notification", + "authentication": { + "authType": ["BASIC"], + "paramsBasic": { + "userName": "test", + "password": "test" + } + }, + "filter": { + "operationTypes": [fields.LcmOperationType.INSTANTIATE] + } + } + result = self.controller.subscription_create( + request=self.request, body=body_2) + self.assertEqual(201, result.status) + + body_3 = { + "callbackUri": "http://127.0.0.1:6789/notification", + "authentication": { + "authType": ["OAUTH2_CLIENT_CREDENTIALS"], "paramsOauth2ClientCredentials": { "clientId": "test", "clientPassword": "test", @@ -596,7 +641,29 @@ class TestVnflcmV2(db_base.SqlTestCase): } } result = self.controller.subscription_create( - request=self.request, body=body) + request=self.request, body=body_3) + self.assertEqual(201, result.status) + + body_4 = { + "callbackUri": "http://127.0.0.1:6789/notification", + "authentication": { + "authType": ["OAUTH2_CLIENT_CERT"], + "paramsOauth2ClientCert": { + "clientId": "test", + "certificateRef": { + "type": "x5t#256", + "value": "03c6e188d1fe5d3da8c9bc9a8dc531a2" + "b3ecf812b03aede9bec7ba1b410b6b64" + }, + "tokenEndpoint": "https://127.0.0.1/token" + } + }, + "filter": { + "operationTypes": [fields.LcmOperationType.INSTANTIATE] + } + } + result = self.controller.subscription_create( + request=self.request, body=body_4) self.assertEqual(201, result.status) @mock.patch.object(subsc_utils, 'get_subsc_all') diff --git a/tacker/tests/unit/sol_refactored/controller/test_vnfpm_v2.py b/tacker/tests/unit/sol_refactored/controller/test_vnfpm_v2.py index eb5f0d58d..d597bc344 100644 --- a/tacker/tests/unit/sol_refactored/controller/test_vnfpm_v2.py +++ b/tacker/tests/unit/sol_refactored/controller/test_vnfpm_v2.py @@ -45,51 +45,6 @@ class TestVnfpmV2(base.BaseTestCase): self.endpoint = CONF.v2_vnfm.endpoint self._pm_job_view = vnfpm_view.PmJobViewBuilder(self.endpoint) - def test_check_http_client_auth(self): - auth_req = { - 'authType': ['BASIC'], - 'paramsBasic': None - } - self.assertRaises(sol_ex.InvalidSubscription, - vnfpm_v2._check_http_client_auth, - auth_req=auth_req) - - auth_req_1 = { - 'authType': ['BASIC'], - 'paramsBasic': { - 'userName': 'test', - 'password': 'test' - }, - } - result = vnfpm_v2._check_http_client_auth(auth_req_1) - self.assertEqual(['BASIC'], result.authType) - - auth_req_2 = { - 'authType': ['OAUTH2_CLIENT_CREDENTIALS'], - 'paramsOauth2ClientCredentials': { - 'clientId': 'test', - 'clientPassword': 'test', - 'tokenEndpoint': - 'http://127.0.0.1/token' - } - } - result = vnfpm_v2._check_http_client_auth(auth_req_2) - self.assertEqual(['OAUTH2_CLIENT_CREDENTIALS'], result.authType) - - auth_req_3 = { - 'authType': ['OAUTH2_CLIENT_CREDENTIALS'], - } - self.assertRaises(sol_ex.InvalidSubscription, - vnfpm_v2._check_http_client_auth, - auth_req=auth_req_3) - - auth_req_4 = { - 'authType': ['TLS_CERT'] - } - self.assertRaises(sol_ex.InvalidSubscription, - vnfpm_v2._check_http_client_auth, - auth_req=auth_req_4) - def test_check_performance_metric_or_group(self): vnfpm_v2._check_performance_metric_or_group( obj_type='Vnf', diff --git a/tacker/tests/unit/sol_refactored/nfvo/test_nfvo_client.py b/tacker/tests/unit/sol_refactored/nfvo/test_nfvo_client.py index ea58699fa..e8c4875c8 100644 --- a/tacker/tests/unit/sol_refactored/nfvo/test_nfvo_client.py +++ b/tacker/tests/unit/sol_refactored/nfvo/test_nfvo_client.py @@ -17,6 +17,7 @@ import requests from unittest import mock import zipfile +from oslo_config import cfg from oslo_log import log as logging from oslo_utils import uuidutils @@ -314,11 +315,24 @@ class TestNfvoClient(base.BaseTestCase): self.nfvo_client.grant_api_version = '1.4.0' self.nfvo_client.vnfpkgm_api_version = '2.1.0' + cfg.CONF.set_override("use_external_nfvo", True, group="v2_nfvo") + cfg.CONF.set_override("mtls_ca_cert_file", "/path/to/cacert", + group="v2_nfvo") + cfg.CONF.set_override("mtls_client_cert_file", "/path/to/clientcert", + group="v2_nfvo") + cfg.CONF.set_override("token_endpoint", "http://127.0.0.1:9990/token", + group="v2_nfvo") + cfg.CONF.set_override("client_id", "test", group="v2_nfvo") + self.addCleanup(mock.patch.stopall) + mock.patch('os.makedirs').start() + self.nfvo_client_mtls = nfvo_client.NfvoClient() + @mock.patch.object(local_nfvo.LocalNfvo, 'onboarded_show') @mock.patch.object(http_client.HttpClient, 'do_request') def test_get_vnf_package_info_vnfd( self, mock_request, mock_onboarded_show): # local nfvo + cfg.CONF.clear_override("use_external_nfvo", group="v2_nfvo") self.nfvo_client.is_local = True mock_onboarded_show.return_value = vnfd_utils.Vnfd( vnfd_id=SAMPLE_VNFD_ID) @@ -326,47 +340,77 @@ class TestNfvoClient(base.BaseTestCase): self.context, SAMPLE_VNFD_ID) self.assertEqual(SAMPLE_VNFD_ID, result.vnfd_id) - # external nfvo + # external nfvo oauth2 + cfg.CONF.clear_override("mtls_client_cert_file", group="v2_nfvo") + cfg.CONF.set_override("use_external_nfvo", True, group="v2_nfvo") self.nfvo_client.is_local = False mock_request.return_value = (requests.Response(), _vnfpkg_body_example) result = self.nfvo_client.get_vnf_package_info_vnfd( self.context, SAMPLE_VNFD_ID) self.assertEqual(SAMPLE_VNFD_ID, result.vnfdId) + # external nfvo oauth2 mtls + cfg.CONF.set_override("mtls_client_cert_file", "/path/to/clientcert", + group="v2_nfvo") + result = self.nfvo_client_mtls.get_vnf_package_info_vnfd( + self.context, SAMPLE_VNFD_ID) + self.assertEqual(SAMPLE_VNFD_ID, result.vnfdId) + @mock.patch.object(http_client.HttpClient, 'do_request') def test_onboarded_show_vnfd(self, mock_request): # local nfvo + cfg.CONF.clear_override("use_external_nfvo", group="v2_nfvo") self.nfvo_client.is_local = True result = self.nfvo_client.onboarded_show_vnfd( self.context, SAMPLE_VNFD_ID) self.assertIsNone(result) - # external nfvo + # external nfvo oauth2 + cfg.CONF.clear_override("mtls_client_cert_file", group="v2_nfvo") + cfg.CONF.set_override("use_external_nfvo", True, group="v2_nfvo") self.nfvo_client.is_local = False mock_request.return_value = (requests.Response(), 'test') result = self.nfvo_client.onboarded_show_vnfd( self.context, SAMPLE_VNFD_ID) self.assertEqual('test', result) + # external nfvo oauth2 mtls + cfg.CONF.set_override("mtls_client_cert_file", "/path/to/clientcert", + group="v2_nfvo") + result = self.nfvo_client_mtls.onboarded_show_vnfd( + self.context, SAMPLE_VNFD_ID) + self.assertEqual('test', result) + @mock.patch.object(http_client.HttpClient, 'do_request') def test_onboarded_package_content(self, mock_request): # local nfvo + cfg.CONF.clear_override("use_external_nfvo", group="v2_nfvo") self.nfvo_client.is_local = True result = self.nfvo_client.onboarded_package_content( self.context, SAMPLE_VNFD_ID) self.assertIsNone(result) - # external nfvo + # external nfvo oauth2 + cfg.CONF.clear_override("mtls_client_cert_file", group="v2_nfvo") + cfg.CONF.set_override("use_external_nfvo", True, group="v2_nfvo") self.nfvo_client.is_local = False mock_request.return_value = (requests.Response(), 'test') result = self.nfvo_client.onboarded_package_content( self.context, SAMPLE_VNFD_ID) self.assertEqual('test', result) + # external nfvo oauth2 mtls + cfg.CONF.set_override("mtls_client_cert_file", "/path/to/clientcert", + group="v2_nfvo") + result = self.nfvo_client_mtls.onboarded_package_content( + self.context, SAMPLE_VNFD_ID) + self.assertEqual('test', result) + @mock.patch.object(local_nfvo.LocalNfvo, 'grant') @mock.patch.object(http_client.HttpClient, 'do_request') def test_grant(self, mock_request, mock_grant): # local nfvo + cfg.CONF.clear_override("use_external_nfvo", group="v2_nfvo") self.nfvo_client.is_local = True mock_grant.return_value = objects.GrantV1.from_dict(_grant_res) grant_req = objects.GrantRequestV1.from_dict(_inst_grant_req_example) @@ -377,6 +421,8 @@ class TestNfvoClient(base.BaseTestCase): 'vimAssets']['softwareImages'][0]['vimSoftwareImageId']) # external nfvo + cfg.CONF.clear_override("mtls_client_cert_file", group="v2_nfvo") + cfg.CONF.set_override("use_external_nfvo", True, group="v2_nfvo") self.nfvo_client.is_local = False mock_request.return_value = (requests.Response(), _grant_res) grant_res = self.nfvo_client.grant(self.context, grant_req) @@ -385,11 +431,21 @@ class TestNfvoClient(base.BaseTestCase): self.assertEqual('44fd5841-ce98-4d54-a828-6ef51f941c8a', result[ 'vimAssets']['softwareImages'][0]['vimSoftwareImageId']) + # external nfvo oauth2 mtls + cfg.CONF.set_override("mtls_client_cert_file", "/path/to/clientcert", + group="v2_nfvo") + grant_res = self.nfvo_client_mtls.grant(self.context, grant_req) + result = grant_res.to_dict() + self.assertIsNotNone(result['addResources']) + self.assertEqual('44fd5841-ce98-4d54-a828-6ef51f941c8a', result[ + 'vimAssets']['softwareImages'][0]['vimSoftwareImageId']) + @mock.patch.object(objects.base.TackerPersistentObject, 'get_all') @mock.patch.object(subsc_utils, 'send_notification') @mock.patch.object(local_nfvo.LocalNfvo, 'recv_inst_create_notification') def test_send_inst_create_notification( self, mock_recv, mock_send, mock_subscs): + cfg.CONF.clear_override("use_external_nfvo", group="v2_nfvo") self.nfvo_client.is_local = True inst = objects.VnfInstanceV2(id='test-instance') mock_subscs.return_value = [objects.LccnSubscriptionV2(id='subsc-1')] @@ -403,6 +459,7 @@ class TestNfvoClient(base.BaseTestCase): @mock.patch.object(local_nfvo.LocalNfvo, 'recv_inst_delete_notification') def test_send_inst_delete_notification( self, mock_recv, mock_send, mock_subscs): + cfg.CONF.clear_override("use_external_nfvo", group="v2_nfvo") self.nfvo_client.is_local = True inst = objects.VnfInstanceV2(id='test-instance') mock_subscs.return_value = [objects.LccnSubscriptionV2(id='subsc-1')]