role based access control and result upload 53/37053/13
authorgrakiss <grakiss.wanglei@huawei.com>
Fri, 7 Jul 2017 07:06:29 +0000 (15:06 +0800)
committergrakiss <grakiss.wanglei@huawei.com>
Thu, 20 Jul 2017 01:51:50 +0000 (09:51 +0800)
1. add role for user
2. user can upload test results

Change-Id: I1c5370be7818edb0394f05e8b81f975deb98b286
Signed-off-by: grakiss <grakiss.wanglei@huawei.com>
14 files changed:
utils/test/testapi/3rd_party/static/testapi-ui/components/results/results.html
utils/test/testapi/3rd_party/static/testapi-ui/components/results/resultsController.js
utils/test/testapi/3rd_party/static/testapi-ui/shared/header/header.html
utils/test/testapi/etc/config.ini
utils/test/testapi/opnfv_testapi/common/message.py
utils/test/testapi/opnfv_testapi/resources/result_handlers.py
utils/test/testapi/opnfv_testapi/resources/result_models.py
utils/test/testapi/opnfv_testapi/router/url_mappings.py
utils/test/testapi/opnfv_testapi/tests/unit/executor.py
utils/test/testapi/opnfv_testapi/tests/unit/fake_pymongo.py
utils/test/testapi/opnfv_testapi/tests/unit/resources/test_result.py
utils/test/testapi/opnfv_testapi/ui/auth/constants.py
utils/test/testapi/opnfv_testapi/ui/auth/sign.py
utils/test/testapi/opnfv_testapi/ui/auth/user.py

index 3056e1d..2ae5339 100644 (file)
@@ -1,6 +1,23 @@
 <h3>{{ctrl.pageHeader}}</h3>
 <p>{{ctrl.pageParagraph}}</p>
-
+<form class="form-inline" ng-show="ctrl.isUserResults">
+<h4>Upload Results</h4>
+<div class="form-group col-m-3">
+     <input class="form-contrl btn btn-default" type = "file" file-model = "resultFile"/>
+</div>
+<div class="checkbox col-m-1">
+  <label>
+      <input type="checkbox" ng-model="ctrl.isPublic">public
+  </label>
+</div>
+<div class="form-group col-m-3">
+     <button class="btn btn-primary" ng-click = "ctrl.uploadFile()">upload result</button>
+</div>
+<div>
+<lable>{{ctrl.uploadState}}</label>
+</div>
+</form>
+<div class="row" style="margin-bottom:24px;"></div>
 <div class="result-filters">
     <h4>Filters</h4>
     <div class="row">
@@ -41,7 +58,6 @@
 
 <div cg-busy="{promise:ctrl.authRequest,message:'Loading'}"></div>
 <div cg-busy="{promise:ctrl.resultsRequest,message:'Loading'}"></div>
-
 <div ng-show="ctrl.data" class="results-table">
     <table ng-data="ctrl.data.result" ng-show="ctrl.data" class="table table-striped table-hover">
         <thead>
index 9e3540d..cc6cc0b 100644 (file)
         .module('testapiApp')
         .controller('ResultsController', ResultsController);
 
+    angular
+        .module('testapiApp')
+        .directive('fileModel', ['$parse', function ($parse) {
+            return {
+               restrict: 'A',
+               link: function(scope, element, attrs) {
+                  var model = $parse(attrs.fileModel);
+                  var modelSetter = model.assign;
+
+                  element.bind('change', function(){
+                     scope.$apply(function(){
+                        modelSetter(scope, element[0].files[0]);
+                     });
+                  });
+               }
+            };
+         }]);
+
     ResultsController.$inject = [
         '$scope', '$http', '$filter', '$state', 'testapiApiUrl','raiseAlert'
     ];
@@ -32,6 +50,7 @@
         raiseAlert) {
         var ctrl = this;
 
+        ctrl.uploadFile=uploadFile;
         ctrl.update = update;
         ctrl.open = open;
         ctrl.clearFilters = clearFilters;
@@ -76,6 +95,8 @@
         ctrl.format = 'yyyy-MM-dd';
 
         /** Check to see if this page should display user-specific results. */
+        // ctrl.isUserResults = $state.current.name === 'userResults';
+        // need auth to browse
         ctrl.isUserResults = $state.current.name === 'userResults';
 
         // Should only be on user-results-page if authenticated.
             'The most recently uploaded community test results are listed ' +
             'here.';
 
+        ctrl.uploadState = '';
+
+        ctrl.isPublic = false;
+
         if (ctrl.isUserResults) {
             ctrl.authRequest = $scope.auth.doSignCheck()
                 .then(ctrl.update);
-            ctrl.getUserProducts();
+            // ctrl.getUserProducts();
         } else {
             ctrl.update();
         }
 
+
+        function uploadFileToUrl(file, uploadUrl){
+           var fd = new FormData();
+           fd.append('file', file);
+           fd.append('public', ctrl.isPublic)
+
+           $http.post(uploadUrl, fd, {
+              transformRequest: angular.identity,
+              headers: {'Content-Type': undefined}
+           })
+
+           .success(function(data){
+              var id = data.href.substr(data.href.lastIndexOf('/')+1);
+              ctrl.uploadState = "Upload succeed. Result id is " + id;
+              ctrl.update();
+           })
+
+           .error(function(data, status){
+              ctrl.uploadState = "Upload failed. Error code is " + status;
+           });
+        }
+
+        function uploadFile(){
+           var file = $scope.resultFile;
+           console.log('file is ' );
+           console.dir(file);
+
+           var uploadUrl = testapiApiUrl + "/results/upload";
+           uploadFileToUrl(file, uploadUrl);
+        };
+
         /**
          * This will contact the TestAPI API to get a listing of test run
          * results.
index f2c49e8..85c33b6 100644 (file)
@@ -33,6 +33,7 @@ TestAPI
           </ul>
           <ul class="nav navbar-nav navbar-right">
             <li ng-class="{ active: header.isActive('/user_results')}" ng-if="auth.isAuthenticated"><a ui-sref="userResults">My Results</a></li>
+            <!--
             <li ng-if="auth.isAuthenticated" ng-class="{ active: header.isCatalogActive('user')}" class="dropdown" uib-dropdown>
                 <a role="button" class="dropdown-toggle" uib-dropdown-toggle>
                     My Catalog <strong class="caret"></strong>
@@ -42,6 +43,7 @@ TestAPI
                     <li><a ui-sref="userProducts">My Products</a></li>
                 </ul>
             </li>
+            -->
             <li ng-class="{ active: header.isActive('/profile')}" ng-if="auth.isAuthenticated"><a ui-sref="profile">Profile</a></li>
             <li ng-if="auth.isAuthenticated"><a href="" ng-click="auth.doSignOut()">Sign Out</a></li>
             <li ng-if="!auth.isAuthenticated"><a href="" ng-click="auth.doSignIn()">Sign In / Sign Up</a></li>
index 9ae2520..435188d 100644 (file)
@@ -12,7 +12,7 @@ url = http://localhost:8000/api/v1
 port = 8000
 
 # Number of results for one page (integer value)
-#results_per_page = 20
+results_per_page = 20
 
 # With debug_on set to true, error traces will be shown in HTTP responses
 debug = True
index 98536ff..951cbaf 100644 (file)
@@ -10,6 +10,10 @@ not_found_base = 'Could Not Found'
 exist_base = 'Already Exists'
 
 
+def key_error(key):
+    return "KeyError: '{}'".format(key)
+
+
 def no_body():
     return 'No Body'
 
index f9706fc..5eb1b92 100644 (file)
@@ -6,8 +6,10 @@
 # which accompanies this distribution, and is available at
 # http://www.apache.org/licenses/LICENSE-2.0
 ##############################################################################
+import logging
 from datetime import datetime
 from datetime import timedelta
+import json
 
 from bson import objectid
 
@@ -17,6 +19,7 @@ from opnfv_testapi.common import raises
 from opnfv_testapi.resources import handlers
 from opnfv_testapi.resources import result_models
 from opnfv_testapi.tornado_swagger import swagger
+from opnfv_testapi.ui.auth import constants as auth_const
 
 CONF = config.Config()
 
@@ -40,6 +43,7 @@ class GenericResultHandler(handlers.GenericApiHandler):
         query = dict()
         date_range = dict()
 
+        query['public'] = {'$not': {'$eq': 'false'}}
         for k in self.request.query_arguments.keys():
             v = self.get_query_argument(k)
             if k == 'project' or k == 'pod' or k == 'case':
@@ -56,6 +60,14 @@ class GenericResultHandler(handlers.GenericApiHandler):
                 date_range.update({'$gte': str(v)})
             elif k == 'to':
                 date_range.update({'$lt': str(v)})
+            elif k == 'signed':
+                openid = self.get_secure_cookie(auth_const.OPENID)
+                role = self.get_secure_cookie(auth_const.ROLE)
+                logging.info('role:%s', role)
+                if role:
+                    del query['public']
+                    if role != "reviewer":
+                        query['user'] = openid
             elif k != 'last' and k != 'page':
                 query[k] = v
             if date_range:
@@ -90,9 +102,10 @@ class ResultsCLHandler(GenericResultHandler):
                  - criteria : the global criteria status passed or failed
                  - trust_indicator : evaluate the stability of the test case
                    to avoid running systematically long and stable test case
+                 - signed : get logined user result
 
                 GET /results/project=functest&case=vPing&version=Arno-R1 \
-                &pod=pod_name&period=15
+                &pod=pod_name&period=15&signed
             @return 200: all test results consist with query,
                          empty list if no result is found
             @rtype: L{TestResults}
@@ -152,6 +165,10 @@ class ResultsCLHandler(GenericResultHandler):
             @type trust_indicator: L{float}
             @in trust_indicator: query
             @required trust_indicator: False
+            @param signed: user results or all results
+            @type signed: L{string}
+            @in signed: query
+            @required signed: False
         """
         limitations = {'sort': {'_id': -1}}
         last = self.get_query_argument('last', 0)
@@ -179,6 +196,9 @@ class ResultsCLHandler(GenericResultHandler):
             @raise 404: pod/project/testcase not exist
             @raise 400: body/pod_name/project_name/case_name not provided
         """
+        self._post()
+
+    def _post(self):
         def pod_query():
             return {'name': self.json_args.get('pod_name')}
 
@@ -193,9 +213,39 @@ class ResultsCLHandler(GenericResultHandler):
         carriers = [('pods', pod_query),
                     ('projects', project_query),
                     ('testcases', testcase_query)]
+
         self._create(miss_fields=miss_fields, carriers=carriers)
 
 
+class ResultsUploadHandler(ResultsCLHandler):
+    @swagger.operation(nickname="uploadTestResult")
+    def post(self):
+        """
+            @description: upload and create a test result
+            @param body: result to be created
+            @type body: L{ResultCreateRequest}
+            @in body: body
+            @rtype: L{CreateResponse}
+            @return 200: result is created.
+            @raise 404: pod/project/testcase not exist
+            @raise 400: body/pod_name/project_name/case_name not provided
+        """
+        logging.info('file upload')
+        fileinfo = self.request.files['file'][0]
+        is_public = self.get_body_argument('public')
+        logging.warning('public:%s', is_public)
+        logging.info('results is :%s', fileinfo['filename'])
+        logging.info('results is :%s', fileinfo['body'])
+        self.json_args = json.loads(fileinfo['body']).copy()
+        self.json_args['public'] = is_public
+
+        openid = self.get_secure_cookie(auth_const.OPENID)
+        if openid:
+            self.json_args['user'] = openid
+
+        super(ResultsUploadHandler, self)._post()
+
+
 class ResultsGURHandler(GenericResultHandler):
     @swagger.operation(nickname='getTestResultById')
     def get(self, result_id):
index 62a6dac..890bf82 100644 (file)
@@ -54,6 +54,8 @@ class ResultCreateRequest(models.ModelBase):
                  build_tag=None,
                  scenario=None,
                  criteria=None,
+                 user=None,
+                 public="true",
                  trust_indicator=None):
         self.pod_name = pod_name
         self.project_name = project_name
@@ -66,6 +68,8 @@ class ResultCreateRequest(models.ModelBase):
         self.build_tag = build_tag
         self.scenario = scenario
         self.criteria = criteria
+        self.user = user
+        self.public = public
         self.trust_indicator = trust_indicator if trust_indicator else TI(0)
 
 
@@ -89,7 +93,7 @@ class TestResult(models.ModelBase):
                  pod_name=None, installer=None, version=None,
                  start_date=None, stop_date=None, details=None,
                  build_tag=None, scenario=None, criteria=None,
-                 trust_indicator=None):
+                 user=None, public="true", trust_indicator=None):
         self._id = _id
         self.case_name = case_name
         self.project_name = project_name
@@ -102,6 +106,8 @@ class TestResult(models.ModelBase):
         self.build_tag = build_tag
         self.scenario = scenario
         self.criteria = criteria
+        self.user = user
+        self.public = public
         self.trust_indicator = trust_indicator
 
     @staticmethod
index a2312de..37e719b 100644 (file)
@@ -48,6 +48,7 @@ mappings = [
     # Push results with mandatory request payload parameters
     # (project, case, and pod)
     (r"/api/v1/results", result_handlers.ResultsCLHandler),
+    (r'/api/v1/results/upload', result_handlers.ResultsUploadHandler),
     (r"/api/v1/results/([^/]+)", result_handlers.ResultsGURHandler),
 
     # scenarios
@@ -64,4 +65,5 @@ mappings = [
     (r'/api/v1/auth/signin_return', sign.SigninReturnHandler),
     (r'/api/v1/auth/signout', sign.SignoutHandler),
     (r'/api/v1/profile', user.ProfileHandler),
+
 ]
index b30c325..b8f696c 100644 (file)
@@ -10,6 +10,20 @@ import functools
 import httplib
 
 
+def upload(excepted_status, excepted_response):
+    def _upload(create_request):
+        @functools.wraps(create_request)
+        def wrap(self):
+            request = create_request(self)
+            status, body = self.upload(request)
+            if excepted_status == httplib.OK:
+                getattr(self, excepted_response)(body)
+            else:
+                self.assertIn(excepted_response, body)
+        return wrap
+    return _upload
+
+
 def create(excepted_status, excepted_response):
     def _create(create_request):
         @functools.wraps(create_request)
index 04785d2..d95ff37 100644 (file)
@@ -189,9 +189,8 @@ class MemDb(object):
                 elif k == 'trust_indicator.current':
                     if content.get('trust_indicator').get('current') != v:
                         return False
-                elif content.get(k, None) != v:
+                elif not isinstance(v, dict) and content.get(k, None) != v:
                     return False
-
         return True
 
     def _find(self, *args):
index 2bff048..f199bc7 100644 (file)
@@ -10,6 +10,7 @@ import copy
 import httplib
 import unittest
 from datetime import datetime, timedelta
+import json
 
 from opnfv_testapi.common import message
 from opnfv_testapi.resources import pod_models
@@ -131,6 +132,22 @@ class TestResultBase(base.TestBase):
         _, res = self.create_d()
         return res.href.split('/')[-1]
 
+    def upload(self, req):
+        if req and not isinstance(req, str) and hasattr(req, 'format'):
+            req = req.format()
+        res = self.fetch(self.basePath + '/upload',
+                         method='POST',
+                         body=json.dumps(req),
+                         headers=self.headers)
+
+        return self._get_return(res, self.create_res)
+
+
+class TestResultUpload(TestResultBase):
+    @executor.upload(httplib.BAD_REQUEST, message.key_error('file'))
+    def test_filenotfind(self):
+        return None
+
 
 class TestResultCreate(TestResultBase):
     @executor.create(httplib.BAD_REQUEST, message.no_body())
@@ -268,6 +285,16 @@ class TestResultGet(TestResultBase):
     def test_queryLast(self):
         return self._set_query('last=1')
 
+    @executor.query(httplib.OK, '_query_success', 4)
+    def test_queryPublic(self):
+        self._create_public_data()
+        return self._set_query('')
+
+    @executor.query(httplib.OK, '_query_success', 1)
+    def test_queryPrivate(self):
+        self._create_private_data()
+        return self._set_query('public=false')
+
     @executor.query(httplib.OK, '_query_period_one', 1)
     def test_combination(self):
         return self._set_query('pod',
@@ -327,16 +354,29 @@ class TestResultGet(TestResultBase):
         self.create(req)
         return req
 
+    def _create_public_data(self, **kwargs):
+        req = copy.deepcopy(self.req_d)
+        req.public = 'true'
+        self.create(req)
+        return req
+
+    def _create_private_data(self, **kwargs):
+        req = copy.deepcopy(self.req_d)
+        req.public = 'false'
+        self.create(req)
+        return req
+
     def _set_query(self, *args):
         def get_value(arg):
             return self.__getattribute__(arg) \
                 if arg != 'trust_indicator' else self.trust_indicator.current
         uri = ''
         for arg in args:
-            if '=' in arg:
-                uri += arg + '&'
-            else:
-                uri += '{}={}&'.format(arg, get_value(arg))
+            if arg:
+                if '=' in arg:
+                    uri += arg + '&'
+                else:
+                    uri += '{}={}&'.format(arg, get_value(arg))
         return uri[0: -1]
 
 
index 43f69d7..44ccb46 100644 (file)
@@ -1,4 +1,6 @@
 OPENID = 'openid'
+ROLE = 'role'
+DEFAULT_ROLE = 'user'
 
 # OpenID parameters
 OPENID_MODE = 'openid.mode'
index 6a9d94e..5b36225 100644 (file)
@@ -1,4 +1,7 @@
 from six.moves.urllib import parse
+from tornado import gen
+from tornado import web
+import logging
 
 from opnfv_testapi.common import config
 from opnfv_testapi.ui.auth import base
@@ -31,20 +34,31 @@ class SigninHandler(base.BaseHandler):
 
 
 class SigninReturnHandler(base.BaseHandler):
+    @web.asynchronous
+    @gen.coroutine
     def get(self):
         if self.get_query_argument(const.OPENID_MODE) == 'cancel':
             self._auth_failure('Authentication canceled.')
 
         openid = self.get_query_argument(const.OPENID_CLAIMED_ID)
-        user_info = {
+        role = const.DEFAULT_ROLE
+        new_user_info = {
             'openid': openid,
             'email': self.get_query_argument(const.OPENID_NS_SREG_EMAIL),
-            'fullname': self.get_query_argument(const.OPENID_NS_SREG_FULLNAME)
+            'fullname': self.get_query_argument(const.OPENID_NS_SREG_FULLNAME),
+            const.ROLE: role
         }
+        user = yield self.db_find_one({'openid': openid})
+        if not user:
+            self.db_save(self.table, new_user_info)
+            logging.info('save to db:%s', new_user_info)
+        else:
+            role = user.get(const.ROLE)
 
-        self.db_save(self.table, user_info)
-        if not self.get_secure_cookie('openid'):
-            self.set_secure_cookie('openid', openid)
+        self.clear_cookie(const.OPENID)
+        self.clear_cookie(const.ROLE)
+        self.set_secure_cookie(const.OPENID, openid)
+        self.set_secure_cookie(const.ROLE, role)
         self.redirect(url=CONF.ui_url)
 
     def _auth_failure(self, message):
@@ -57,9 +71,8 @@ class SigninReturnHandler(base.BaseHandler):
 class SignoutHandler(base.BaseHandler):
     def get(self):
         """Handle signout request."""
-        openid = self.get_secure_cookie(const.OPENID)
-        if openid:
-            self.clear_cookie(const.OPENID)
+        self.clear_cookie(const.OPENID)
+        self.clear_cookie(const.ROLE)
         params = {'openid_logout': CONF.osid_openid_logout_endpoint}
         url = parse.urljoin(CONF.ui_url,
                             '/#/logout?' + parse.urlencode(params))
index 140bca5..2fca2a8 100644 (file)
@@ -17,7 +17,7 @@ class ProfileHandler(base.BaseHandler):
                     "openid": user.get('openid'),
                     "email": user.get('email'),
                     "fullname": user.get('fullname'),
-                    "is_admin": False
+                    "role": user.get('role', 'user')
                 })
             except Exception:
                 pass