add AAF integration and AAF simulator 35/67135/5
authorFrank Sandoval <frank.sandoval@oamtechnologies.com>
Mon, 17 Sep 2018 21:50:04 +0000 (15:50 -0600)
committerIkram Ikramullah <ikram@research.att.com>
Fri, 21 Sep 2018 20:54:58 +0000 (16:54 -0400)
Patch (#3) addresses commentse
this patch fixes unit tests. Original commit added AAF permissions checks for HAS API

Issue-ID: OPTFRA-331

Change-Id: I69519beee31f57e4ac5188604d21805266a06074
Signed-off-by: Frank Sandoval <frank.sandoval@oamtechnologies.com>
14 files changed:
conductor/conductor/api/adapters/__init__.py [new file with mode: 0644]
conductor/conductor/api/adapters/aaf/__init__.py [new file with mode: 0644]
conductor/conductor/api/adapters/aaf/aaf_authentication.py [new file with mode: 0644]
conductor/conductor/api/controllers/v1/plans.py
conductor/conductor/tests/functional/simulators/aafsim/Dockerfile [new file with mode: 0755]
conductor/conductor/tests/functional/simulators/aafsim/aafsim.py [new file with mode: 0755]
conductor/conductor/tests/functional/simulators/aafsim/requirements.txt [new file with mode: 0755]
conductor/conductor/tests/functional/simulators/aafsim/responses/get_perms_user.json [new file with mode: 0644]
conductor/conductor/tests/functional/simulators/aafsim/responses/healthcheck.json [new file with mode: 0644]
conductor/conductor/tests/functional/simulators/build_aafsim.sh [new file with mode: 0755]
conductor/conductor/tests/functional/simulators/destroy_aafsim.sh [new file with mode: 0755]
conductor/conductor/tests/functional/simulators/run_aafsim.sh [new file with mode: 0755]
conductor/conductor/tests/unit/api/controller/v1/test_plans.py
conductor/requirements.txt

diff --git a/conductor/conductor/api/adapters/__init__.py b/conductor/conductor/api/adapters/__init__.py
new file mode 100644 (file)
index 0000000..ba46563
--- /dev/null
@@ -0,0 +1,17 @@
+# -------------------------------------------------------------------------
+#   Copyright (c) 2018 OAM Technology Consulting LLC Intellectual Property
+#
+#   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.
+#
+# -------------------------------------------------------------------------
+#
diff --git a/conductor/conductor/api/adapters/aaf/__init__.py b/conductor/conductor/api/adapters/aaf/__init__.py
new file mode 100644 (file)
index 0000000..4648e6c
--- /dev/null
@@ -0,0 +1,18 @@
+#
+# -------------------------------------------------------------------------
+#   Copyright (c) 2018 OAM Technology Consulting LLC Intellectual Property
+#
+#   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.
+#
+# -------------------------------------------------------------------------
+#
\ No newline at end of file
diff --git a/conductor/conductor/api/adapters/aaf/aaf_authentication.py b/conductor/conductor/api/adapters/aaf/aaf_authentication.py
new file mode 100644 (file)
index 0000000..fca84fd
--- /dev/null
@@ -0,0 +1,151 @@
+#
+# -------------------------------------------------------------------------
+#   Copyright (c) 2018 OAM Technology Consulting LLC Intellectual Property
+#
+#   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 base64
+from datetime import datetime, timedelta
+import json
+
+from conductor.common import rest
+from conductor.i18n import _LE, _LI
+
+from oslo_log import log
+LOG = log.getLogger(__name__)
+
+from oslo_config import cfg
+CONF = cfg.CONF
+
+# TBD - read values from conductor.conf
+AAF_OPTS = [
+    cfg.BoolOpt('is_aaf_enabled',
+                default=True,
+                help='is_aaf_enabled.'),
+    cfg.IntOpt('aaf_cache_expiry_hrs',
+               default='3',
+               help='aaf_cache_expiry_hrs.'),
+    cfg.StrOpt('aaf_url',
+               default='http://aaf-service:8100/authz/perms/user/',
+               help='aaf_url.'),
+    cfg.IntOpt('aaf_retries',
+               default='3',
+               help='aaf_retries.'),
+    cfg.IntOpt('aaf_timeout',
+               default='100',
+               help='aaf_timeout.'),
+    cfg.ListOpt('aaf_user_roles',
+               default=['{"type": "org.onap.oof","instance": "plans","action": "GET"}',
+                      '{"type": "org.onap.oof","instance": "plans","action": "POST"}'],
+               help='aaf_user_roles.')
+]
+
+CONF.register_opts(AAF_OPTS, group='aaf_authentication')
+
+AUTHZ_PERMS_USER = '{}/authz/perms/user/{}'
+
+EXPIRE_TIME = 'expire_time'
+
+perm_cache = {}
+
+def clear_cache():
+    perm_cache.clear()
+
+
+def authenticate(uid, passwd):
+    try:
+        perms = get_aaf_permissions(uid, passwd)
+        return has_valid_role(perms)
+    except Exception as exp:
+        LOG.error("Error Authenticating the user {} : {}: ".format(uid, exp))
+        pass
+    return False
+
+"""
+Check whether the user has valid permissions
+return True if the user has valid permissions
+else return false
+"""
+
+def has_valid_role(perms):
+    aaf_user_roles = CONF.aaf_authentication.aaf_user_roles
+
+    permObj = json.loads(perms)
+    permList = permObj["perm"]
+    for user_role in aaf_user_roles:
+        role = json.loads(user_role)
+        userType = role["type"]
+        userInstance = role["instance"]
+        userAction = role["action"]
+        for perm in permList:
+            permType = perm["type"]
+            permInstance = perm["instance"]
+            permAction = perm["action"]
+            if userType == permType and userInstance == permInstance and \
+                (userAction == permAction or userAction == "*"):
+                return True
+    return False
+
+"""
+Make the remote aaf api call if user is not in the cache.
+
+Return the perms
+"""
+def get_aaf_permissions(uid, passwd):
+    key = base64.b64encode("{}_{}".format(uid, passwd), "ascii")
+    time_delta = timedelta(hours = CONF.aaf_authentication.aaf_cache_expiry_hrs)
+
+# TBD - test cache logic
+    perms = perm_cache.get(key)
+
+    if perms and datetime.now() < perms.get(EXPIRE_TIME):
+        LOG.debug("Returning cached value")
+        return perms
+    LOG.debug("Invoking AAF authentication API")
+    response = remote_api(passwd, uid)
+    perms = {EXPIRE_TIME: datetime.now() + time_delta, 'roles': response}
+    perm_cache[key] = perms
+    return response
+
+def remote_api(passwd, uid):
+    server_url = CONF.aaf_authentication.aaf_url.rstrip('/')
+    kwargs = {
+        "server_url": server_url,
+        "retries": CONF.aaf_authentication.aaf_retries,
+        "username": uid,
+        "password": passwd,
+        "log_debug": LOG.debug,
+        "read_timeout": CONF.aaf_authentication.aaf_timeout,
+    }
+    restReq = rest.REST(**kwargs)
+
+    headers = {"Accept": "application/json"}
+    rkwargs = {
+        "method": 'GET',
+        "path": '',
+        "headers": headers,
+    }
+    response = restReq.request(**rkwargs)
+
+    if response is None:
+        LOG.error(_LE("No response from AAF "))
+    elif response.status_code != 200:
+        LOG.error(_LE("AAF request  returned HTTP "
+                      "status {} {}, link: {}").
+                  format(response.status_code, response.reason,
+                         server_url))
+    return response.content
+
index 3858069..f70b998 100644 (file)
@@ -16,7 +16,6 @@
 #
 # -------------------------------------------------------------------------
 #
-
 import six
 import yaml
 import base64
@@ -34,8 +33,9 @@ from conductor.api.controllers import validator
 from conductor.i18n import _, _LI
 from oslo_config import cfg
 
-CONF = cfg.CONF
+from conductor.api.adapters.aaf import aaf_authentication as aaf_auth
 
+CONF = cfg.CONF
 
 LOG = log.getLogger(__name__)
 
@@ -44,14 +44,14 @@ CONDUCTOR_API_OPTS = [
                default='',
                help='Base URL for plans.'),
     cfg.StrOpt('username',
-               default='admin1',
+               default='',
                help='username for plans.'),
     cfg.StrOpt('password',
-               default='plan.15',
+               default='',
                help='password for plans.'),
     cfg.BoolOpt('basic_auth_secure',
-               default=True,
-               help='auth toggling.')
+                default=True,
+                help='auth toggling.'),
 ]
 
 CONF.register_opts(CONDUCTOR_API_OPTS, group='conductor_api')
@@ -61,7 +61,6 @@ CREATE_SCHEMA = (
     (decorators.optional('id'), types.string),
     (decorators.optional('limit'), types.integer),
     (decorators.optional('name'), types.string),
-    (decorators.optional('num_solution'), types.string),
     ('template', string_or_dict),
     (decorators.optional('template_url'), types.string),
     (decorators.optional('timeout'), types.integer),
@@ -86,11 +85,12 @@ class PlansBaseController(object):
 
     def plans_get(self, plan_id=None):
 
-        basic_auth_flag = CONF.conductor_api.basic_auth_secure
+        auth_flag = CONF.conductor_api.basic_auth_secure or CONF.aaf_authentication.is_aaf_enabled
 
+        # TBD - is healthcheck properly supported?
         if plan_id == 'healthcheck' or \
-        not basic_auth_flag or \
-        (basic_auth_flag and check_basic_auth()):
+                not auth_flag or \
+                (auth_flag and check_auth()):
             return self.plan_getid(plan_id)
 
     def plan_getid(self, plan_id):
@@ -147,6 +147,7 @@ class PlansBaseController(object):
             args.get('name')))
 
         client = pecan.request.controller
+
         transaction_id = pecan.request.headers.get('transaction-id')
         if transaction_id:
             args['template']['transaction-id'] = transaction_id
@@ -290,12 +291,13 @@ class PlansController(PlansBaseController):
         if args and args['name']:
             LOG.info('Plan name: {}'.format(args['name']))
 
-        basic_auth_flag = CONF.conductor_api.basic_auth_secure
+        auth_flag = CONF.conductor_api.basic_auth_secure or CONF.aaf_authentication.is_aaf_enabled
 
-        # Create the plan only when the basic authentication is disabled or pass the authentication check
-        if not basic_auth_flag or \
-        (basic_auth_flag and check_basic_auth()):
+        # Create the plan only when the basic authentication is disabled or pass the authenticaiton check
+        if not auth_flag or \
+                (auth_flag and check_auth()):
             plan = self.plan_create(args)
+
         if not plan:
             error('/errors/server_error', _('Unable to create Plan.'))
         else:
@@ -307,11 +309,13 @@ class PlansController(PlansBaseController):
         """Pecan subcontroller routing callback"""
         return PlansItemController(uuid4), remainder
 
-def check_basic_auth():
+
+def check_auth():
     """
     Returns True/False if the username/password of Basic Auth match/not match
+    Will also check role-based access controls if AAF integration configured
     :return boolean value
-     """
+    """
 
     try:
         if pecan.request.headers['Authorization'] and verify_user(pecan.request.headers['Authorization']):
@@ -323,19 +327,22 @@ def check_basic_auth():
             user_pw = auth_str.split(' ')[1]
             decode_user_pw = base64.b64decode(user_pw)
             list_id_pw = decode_user_pw.split(':')
-            LOG.error("Incorrect username={}/password={}".format(list_id_pw[0],list_id_pw[1]))
+            LOG.error("Incorrect username={} / password={}".format(list_id_pw[0], list_id_pw[1]))
     except:
         error('/errors/basic_auth_error', _('Unauthorized: The request does not '
                                             'provide any HTTP authentication (basic authentication)'))
+        plan = False
 
     if not plan:
         error('/errors/authentication_error', _('Invalid credentials: username or password is incorrect'))
+
     return plan
 
 
 def verify_user(authstr):
     """
-    authenticate user as per config file
+    authenticate user as per config file or AAF authentication service
+    :param authstr:
     :return boolean value
     """
     user_dict = dict()
@@ -347,7 +354,17 @@ def verify_user(authstr):
     user_dict['password'] = list_id_pw[1]
     password = CONF.conductor_api.password
     username = CONF.conductor_api.username
-    if username == user_dict['username'] and password == user_dict['password']:
-        return True
+
+#    print ("plans.verify_user(): Expected username/password: {}/{}".format(username, password))
+#    print ("plans.verify_user(): Provided username/password: {}/{}".format(user_dict['username'], user_dict['password']))
+
+    retVal = False
+
+    if CONF.aaf_authentication.is_aaf_enabled:
+        retVal = aaf_auth.authenticate(user_dict['username'], user_dict['password'])
     else:
-        return False
+        if username == user_dict['username'] and password == user_dict['password']:
+            retVal = True
+
+    return retVal
+
diff --git a/conductor/conductor/tests/functional/simulators/aafsim/Dockerfile b/conductor/conductor/tests/functional/simulators/aafsim/Dockerfile
new file mode 100755 (executable)
index 0000000..5970b3b
--- /dev/null
@@ -0,0 +1,39 @@
+#
+# -------------------------------------------------------------------------
+#   Copyright (c) 2018 AT&T Intellectual Property
+#
+#   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.
+#
+# -------------------------------------------------------------------------
+#
+# Use an official Python runtime as a parent image
+FROM python:2.7
+
+# Set the working directory to /su/python/webpy-rest-dockerized
+WORKDIR /opt/aafsim
+
+# Copy the current directory contents into the container at /app
+ADD ./  /opt/aafsim
+
+# Install any needed packages specified in requirements.txt
+RUN pip install web.py
+
+# Make port 80 available to the world outside this container
+EXPOSE 8081
+
+# Define environment variable
+ENV NAME aafsim
+
+# Run aafsim.py when the container launches
+CMD ["/bin/sh", "-c", "python -u aafsim.py 8081 > /tmp/aafsim.log 2>&1"]
+
diff --git a/conductor/conductor/tests/functional/simulators/aafsim/aafsim.py b/conductor/conductor/tests/functional/simulators/aafsim/aafsim.py
new file mode 100755 (executable)
index 0000000..c3cea8f
--- /dev/null
@@ -0,0 +1,87 @@
+#
+# -------------------------------------------------------------------------
+#   Copyright (c) 2018 OAM Technology Consulting LLC Intellectual Property
+#
+#   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 web
+import web.webapi
+import json
+
+from subprocess import Popen, PIPE
+from oslo_log import log
+LOG = log.getLogger(__name__)
+
+urls = (
+  '/healthcheck','healthcheck',
+  '/authz/perms/user/','get_perms_user',
+)
+
+myhelp = {"/conductorsim/help":"provides help"}
+myok = {"ok":"ok"}
+json_data={}
+
+replydir = "./responses/"
+
+def replyToAafGet(web, replydir, replyfile):
+        LOG.debug("------------------------------------------------------")
+        fullreply = replydir + replyfile
+        trid=web.ctx.env.get('X_TRANSACTIONID','111111')
+        #print ("X-TransactionId : {}".format(trid))
+        LOG.debug("this is the context : {}".format(web.ctx.fullpath))
+        with open(fullreply) as json_file:
+            json_data = json.load(json_file)
+            LOG.debug(json_data)
+        web.header('Content-Type', 'application/json')
+        web.header('X-TransactionId', trid)
+        return json.dumps(json_data)
+
+class healthcheck:
+    def GET(self):
+        LOG.debug("------------------------------------------------------")
+        replyfile = "healthcheck.json"
+        #replyToAaiGet (web, replydir, replyfile)
+        fullreply = replydir + replyfile
+        trid=web.ctx.env.get('X_TRANSACTIONID','111111')
+        #print ("X-TransactionId : {}".format(trid))
+        LOG.debug("this is the context : {}".format(web.ctx.fullpath))
+        with open(fullreply) as json_file:
+            json_data = json.load(json_file)
+            LOG.debug(json_data)
+        web.header('Content-Type', 'application/json')
+        web.header('X-TransactionId', trid)
+        return json.dumps(json_data)
+
+class get_perms_user:
+    def GET(self):
+        LOG.debug("------------------------------------------------------")
+        replyfile = "get_perms_user.json"
+        #replyToAafGet (web, replydir, replyfile)
+        fullreply = replydir + replyfile
+        trid=web.ctx.env.get('X_TRANSACTIONID','111111')
+        #print ("X-TransactionId : {}".format(trid))
+        LOG.debug("this is the context : {}".format(web.ctx.fullpath))
+        with open(fullreply) as json_file:
+            json_data = json.load(json_file)
+            LOG.debug(json_data)
+        web.header('Content-Type', 'application/json')
+        web.header('X-TransactionId', trid)
+        return json.dumps(json_data)
+
+
+
+if __name__ == "__main__":
+    app = web.application(urls, globals())
+    app.run()
diff --git a/conductor/conductor/tests/functional/simulators/aafsim/requirements.txt b/conductor/conductor/tests/functional/simulators/aafsim/requirements.txt
new file mode 100755 (executable)
index 0000000..c077218
--- /dev/null
@@ -0,0 +1 @@
+web
diff --git a/conductor/conductor/tests/functional/simulators/aafsim/responses/get_perms_user.json b/conductor/conductor/tests/functional/simulators/aafsim/responses/get_perms_user.json
new file mode 100644 (file)
index 0000000..d923ca5
--- /dev/null
@@ -0,0 +1,6 @@
+{"perm": [{"instance": "plans", "action": "POST", "type": "org.onap.oof"},
+         {"instance": "plans", "action": "GET", "type": "org.onap.oof"}]
+}
+                 
+
+
diff --git a/conductor/conductor/tests/functional/simulators/aafsim/responses/healthcheck.json b/conductor/conductor/tests/functional/simulators/aafsim/responses/healthcheck.json
new file mode 100644 (file)
index 0000000..b0bd6da
--- /dev/null
@@ -0,0 +1 @@
+{"status":"success"}
diff --git a/conductor/conductor/tests/functional/simulators/build_aafsim.sh b/conductor/conductor/tests/functional/simulators/build_aafsim.sh
new file mode 100755 (executable)
index 0000000..db6b4d1
--- /dev/null
@@ -0,0 +1,23 @@
+#!/bin/bash
+#
+# -------------------------------------------------------------------------
+#   Copyright (c) 2018 OAM Technology Consulting LLC Intellectual Property
+#
+#   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.
+#
+# -------------------------------------------------------------------------
+#
+# depends on execution from ../simulators directly, parent of ./aafsim
+cd ./aafsim
+docker build -t aafsim .
+
diff --git a/conductor/conductor/tests/functional/simulators/destroy_aafsim.sh b/conductor/conductor/tests/functional/simulators/destroy_aafsim.sh
new file mode 100755 (executable)
index 0000000..b242dd0
--- /dev/null
@@ -0,0 +1,23 @@
+#!/bin/bash
+#
+# -------------------------------------------------------------------------
+#   Copyright (c) 2018 OAM Technology Consulting LLC Intellectual Property
+#
+#   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.
+#
+# -------------------------------------------------------------------------
+#
+docker stop aafsim
+docker rm aafsim
+docker rmi aafsim
+
diff --git a/conductor/conductor/tests/functional/simulators/run_aafsim.sh b/conductor/conductor/tests/functional/simulators/run_aafsim.sh
new file mode 100755 (executable)
index 0000000..0039bbc
--- /dev/null
@@ -0,0 +1,22 @@
+#!/bin/bash
+#
+# -------------------------------------------------------------------------
+#   Copyright (c) 2018 OAM Technology Consulting LLC Intellectual Property
+#
+#   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.
+#
+# -------------------------------------------------------------------------
+#
+#   Note mapping to external port 8082. avoids conflict with aaisim
+docker run -d --name aafsim -p 8082:8081  aafsim
+
index a0cd0c8..f0a28ec 100644 (file)
@@ -35,21 +35,24 @@ class TestPlansController(base_api.BaseApiTest):
         self.assertEqual(204, actual_response.status_int)
         self.assertEqual("GET,POST", actual_response.headers['Allow'])
 
+    @mock.patch('conductor.api.adapters.aaf.aaf_authentication.authenticate')
     @mock.patch.object(plans.LOG, 'error')
     @mock.patch.object(plans.LOG, 'debug')
     @mock.patch.object(plans.LOG, 'warning')
     @mock.patch.object(plans.LOG, 'info')
     @mock.patch('conductor.common.music.messaging.component.RPCClient.call')
     def test_index_get(self, rpc_mock, info_mock, warning_mock, debug_mock,
-                       error_mock):
+                       error_mock, aaf_mock):
         req_json_file = './conductor/tests/unit/api/controller/v1/plans.json'
         params = json.loads(open(req_json_file).read())
         plan_id = str(uuid.uuid4())
         params['id'] = plan_id
         rpc_mock.return_value = {'plans': [params]}
+        aaf_mock.return_value = True
         actual_response = self.app.get('/v1/plans', extra_environ=self.extra_environment)
         self.assertEqual(200, actual_response.status_int)
 
+    @mock.patch('conductor.api.adapters.aaf.aaf_authentication.authenticate')
     @mock.patch.object(plans.LOG, 'error')
     @mock.patch.object(plans.LOG, 'debug')
     @mock.patch.object(plans.LOG, 'warning')
@@ -57,14 +60,17 @@ class TestPlansController(base_api.BaseApiTest):
     @mock.patch('conductor.common.music.messaging.component.RPCClient.call')
     def test_index_post_error(self, rpc_mock, info_mock, warning_mock,
                               debug_mock,
-                              error_mock):
+                              error_mock,
+                                aaf_mock):
         req_json_file = './conductor/tests/unit/api/controller/v1/plans.json'
         params = jsonutils.dumps(json.loads(open(req_json_file).read()))
         rpc_mock.return_value = {}
+        aaf_mock.return_value = True
         response = self.app.post('/v1/plans', params=params,
                                  expect_errors=True, extra_environ=self.extra_environment)
         self.assertEqual(500, response.status_int)
 
+    @mock.patch('conductor.api.adapters.aaf.aaf_authentication.authenticate')
     @mock.patch.object(plans.LOG, 'error')
     @mock.patch.object(plans.LOG, 'debug')
     @mock.patch.object(plans.LOG, 'warning')
@@ -72,7 +78,8 @@ class TestPlansController(base_api.BaseApiTest):
     @mock.patch('conductor.common.music.messaging.component.RPCClient.call')
     def test_index_post_success(self, rpc_mock, info_mock, warning_mock,
                                 debug_mock,
-                                error_mock):
+                                error_mock,
+                                aaf_mock):
         req_json_file = './conductor/tests/unit/api/controller/v1/plans.json'
         params = json.loads(open(req_json_file).read())
         mock_params = copy.deepcopy(params)
@@ -80,6 +87,7 @@ class TestPlansController(base_api.BaseApiTest):
 
         mock_params['id'] = plan_id
         rpc_mock.return_value = {'plan': mock_params}
+        aaf_mock.return_value = True
         params = json.dumps(params)
         response = self.app.post('/v1/plans', params=params,
                                  expect_errors=True, extra_environ=self.extra_environment)
@@ -94,26 +102,30 @@ class TestPlansController(base_api.BaseApiTest):
 
 class TestPlansItemController(base_api.BaseApiTest):
 
+    @mock.patch('conductor.api.adapters.aaf.aaf_authentication.authenticate')
     @mock.patch('conductor.common.music.messaging.component.RPCClient.call')
-    def test_index_options(self, rpc_mock):
+    def test_index_options(self, rpc_mock, aaf_mock):
         req_json_file = './conductor/tests/unit/api/controller/v1/plans.json'
         params = json.loads(open(req_json_file).read())
         plan_id = str(uuid.uuid4())
         params['id'] = plan_id
         rpc_mock.return_value = {'plans': [params]}
+        aaf_mock.return_value = True
         url = '/v1/plans/' + plan_id
         print(url)
         actual_response = self.app.options(url=url, expect_errors=True, extra_environ=self.extra_environment)
         self.assertEqual(204, actual_response.status_int)
         self.assertEqual("GET,DELETE", actual_response.headers['Allow'])
 
+    @mock.patch('conductor.api.adapters.aaf.aaf_authentication.authenticate')
     @mock.patch('conductor.common.music.messaging.component.RPCClient.call')
-    def test_index_httpmethod_notallowed(self, rpc_mock):
+    def test_index_httpmethod_notallowed(self, rpc_mock, aaf_mock):
         req_json_file = './conductor/tests/unit/api/controller/v1/plans.json'
         params = json.loads(open(req_json_file).read())
         plan_id = str(uuid.uuid4())
         params['id'] = plan_id
         rpc_mock.return_value = {'plans': [params]}
+        aaf_mock.return_value = True
         url = '/v1/plans/' + plan_id
         actual_response = self.app.put(url=url, expect_errors=True, extra_environ=self.extra_environment)
         self.assertEqual(405, actual_response.status_int)
@@ -122,35 +134,41 @@ class TestPlansItemController(base_api.BaseApiTest):
         actual_response = self.app.post(url=url, expect_errors=True, extra_environ=self.extra_environment)
         self.assertEqual(405, actual_response.status_int)
 
+    @mock.patch('conductor.api.adapters.aaf.aaf_authentication.authenticate')
     @mock.patch('conductor.common.music.messaging.component.RPCClient.call')
-    def test_index_get(self, rpc_mock):
+    def test_index_get(self, rpc_mock, aaf_mock):
         req_json_file = './conductor/tests/unit/api/controller/v1/plans.json'
         params = json.loads(open(req_json_file).read())
         plan_id = str(uuid.uuid4())
         params['id'] = plan_id
         expected_response = {'plans': [params]}
         rpc_mock.return_value = {'plans': [params]}
+        aaf_mock.return_value = True
         url = '/v1/plans/' + plan_id
         actual_response = self.app.get(url=url, expect_errors=True, extra_environ=self.extra_environment)
         self.assertEqual(200, actual_response.status_int)
         self.assertJsonEqual(expected_response,
                              json.loads(actual_response.body))
 
+    @mock.patch('conductor.api.adapters.aaf.aaf_authentication.authenticate')
     @mock.patch('conductor.common.music.messaging.component.RPCClient.call')
-    def test_index_get_non_exist(self, rpc_mock):
+    def test_index_get_non_exist(self, rpc_mock, aaf_mock):
         rpc_mock.return_value = {'plans': []}
+        aaf_mock.return_value = True
         plan_id = str(uuid.uuid4())
         url = '/v1/plans/' + plan_id
         actual_response = self.app.get(url=url, expect_errors=True, extra_environ=self.extra_environment)
         self.assertEqual(404, actual_response.status_int)
 
+    @mock.patch('conductor.api.adapters.aaf.aaf_authentication.authenticate')
     @mock.patch('conductor.common.music.messaging.component.RPCClient.call')
-    def test_index_delete(self, rpc_mock):
+    def test_index_delete(self, rpc_mock, aaf_mock):
         req_json_file = './conductor/tests/unit/api/controller/v1/plans.json'
         params = json.loads(open(req_json_file).read())
         plan_id = str(uuid.uuid4())
         params['id'] = plan_id
         rpc_mock.return_value = {'plans': [params]}
+        aaf_mock.return_value = True
         url = '/v1/plans/' + plan_id
         actual_response = self.app.delete(url=url, expect_errors=True, extra_environ=self.extra_environment)
         self.assertEqual(204, actual_response.status_int)
index d09c960..6bc9dba 100644 (file)
@@ -23,4 +23,5 @@ requests[security]!=2.9.0,>=2.8.1 # Apache-2.0
 six>=1.9.0 # MIT, also required by futurist
 stevedore>=1.9.0 # Apache-2.0, also required by oslo.config
 WebOb>=1.2.3 # MIT
-onapsmsclient>=0.0.3
\ No newline at end of file
+onapsmsclient>=0.0.3
+Flask>=0.11.1
\ No newline at end of file