summaryrefslogtreecommitdiffstats
path: root/cvp/opnfv_testapi/ui
diff options
context:
space:
mode:
authorhongbo tian <hongbo.tianhongbo@huawei.com>2017-09-28 09:28:41 +0000
committerGerrit Code Review <gerrit@opnfv.org>2017-09-28 09:28:41 +0000
commit61820ee85c967d90021e4089c6a7907046685639 (patch)
tree0c3f23e2146f0855fb5be0ff55343d59e3a9c9ab /cvp/opnfv_testapi/ui
parent0cc2fdefa9e6e959e7c69beede736738f339f636 (diff)
parent0cf6b232ac9cf128ee9183a27c08f4f74ab2e2e6 (diff)
Merge "add api&web services for cvp"
Diffstat (limited to 'cvp/opnfv_testapi/ui')
-rw-r--r--cvp/opnfv_testapi/ui/__init__.py0
-rw-r--r--cvp/opnfv_testapi/ui/auth/__init__.py0
-rw-r--r--cvp/opnfv_testapi/ui/auth/base.py35
-rw-r--r--cvp/opnfv_testapi/ui/auth/constants.py18
-rw-r--r--cvp/opnfv_testapi/ui/auth/jira_util.py66
-rw-r--r--cvp/opnfv_testapi/ui/auth/rsa.pem27
-rw-r--r--cvp/opnfv_testapi/ui/auth/sign.py281
-rw-r--r--cvp/opnfv_testapi/ui/auth/user.py35
-rw-r--r--cvp/opnfv_testapi/ui/root.py10
9 files changed, 472 insertions, 0 deletions
diff --git a/cvp/opnfv_testapi/ui/__init__.py b/cvp/opnfv_testapi/ui/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/cvp/opnfv_testapi/ui/__init__.py
diff --git a/cvp/opnfv_testapi/ui/auth/__init__.py b/cvp/opnfv_testapi/ui/auth/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/cvp/opnfv_testapi/ui/auth/__init__.py
diff --git a/cvp/opnfv_testapi/ui/auth/base.py b/cvp/opnfv_testapi/ui/auth/base.py
new file mode 100644
index 00000000..bea87c4d
--- /dev/null
+++ b/cvp/opnfv_testapi/ui/auth/base.py
@@ -0,0 +1,35 @@
+import random
+import string
+
+from six.moves.urllib import parse
+
+from opnfv_testapi.resources import handlers
+
+
+class BaseHandler(handlers.GenericApiHandler):
+ def __init__(self, application, request, **kwargs):
+ super(BaseHandler, self).__init__(application, request, **kwargs)
+ self.table = 'users'
+
+ def set_cookies(self, cookies):
+ for cookie_n, cookie_v in cookies:
+ self.set_secure_cookie(cookie_n, cookie_v)
+
+
+def get_token(length=30):
+ """Get random token."""
+ return ''.join(random.choice(string.ascii_lowercase)
+ for i in range(length))
+
+
+def set_query_params(url, params):
+ """Set params in given query."""
+ url_parts = parse.urlparse(url)
+ url = parse.urlunparse((
+ url_parts.scheme,
+ url_parts.netloc,
+ url_parts.path,
+ url_parts.params,
+ parse.urlencode(params),
+ url_parts.fragment))
+ return url
diff --git a/cvp/opnfv_testapi/ui/auth/constants.py b/cvp/opnfv_testapi/ui/auth/constants.py
new file mode 100644
index 00000000..44ccb46d
--- /dev/null
+++ b/cvp/opnfv_testapi/ui/auth/constants.py
@@ -0,0 +1,18 @@
+OPENID = 'openid'
+ROLE = 'role'
+DEFAULT_ROLE = 'user'
+
+# OpenID parameters
+OPENID_MODE = 'openid.mode'
+OPENID_NS = 'openid.ns'
+OPENID_RETURN_TO = 'openid.return_to'
+OPENID_CLAIMED_ID = 'openid.claimed_id'
+OPENID_IDENTITY = 'openid.identity'
+OPENID_REALM = 'openid.realm'
+OPENID_NS_SREG = 'openid.ns.sreg'
+OPENID_NS_SREG_REQUIRED = 'openid.sreg.required'
+OPENID_NS_SREG_EMAIL = 'openid.sreg.email'
+OPENID_NS_SREG_FULLNAME = 'openid.sreg.fullname'
+OPENID_ERROR = 'openid.error'
+
+CSRF_TOKEN = 'csrf_token'
diff --git a/cvp/opnfv_testapi/ui/auth/jira_util.py b/cvp/opnfv_testapi/ui/auth/jira_util.py
new file mode 100644
index 00000000..5ec91a71
--- /dev/null
+++ b/cvp/opnfv_testapi/ui/auth/jira_util.py
@@ -0,0 +1,66 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+import base64
+import os
+
+import oauth2 as oauth
+from jira import JIRA
+from tlslite.utils import keyfactory
+from opnfv_testapi.common.config import CONF
+
+
+class SignatureMethod_RSA_SHA1(oauth.SignatureMethod):
+ name = 'RSA-SHA1'
+
+ def signing_base(self, request, consumer, token):
+ if not hasattr(request, 'normalized_url') or \
+ request.normalized_url is None:
+ raise ValueError("Base URL for request is not set.")
+
+ sig = (
+ oauth.escape(request.method),
+ oauth.escape(request.normalized_url),
+ oauth.escape(request.get_normalized_parameters()),
+ )
+
+ key = '%s&' % oauth.escape(consumer.secret)
+ if token:
+ key += oauth.escape(token.secret)
+ raw = '&'.join(sig)
+ return key, raw
+
+ def sign(self, request, consumer, token):
+ """Builds the base signature string."""
+ key, raw = self.signing_base(request, consumer, token)
+
+ module_dir = os.path.dirname(__file__) # get current directory
+ with open(module_dir + '/rsa.pem', 'r') as f:
+ data = f.read()
+ privateKeyString = data.strip()
+ privatekey = keyfactory.parsePrivateKey(privateKeyString)
+ raw = str.encode(raw)
+ signature = privatekey.hashAndSign(raw)
+ return base64.b64encode(signature)
+
+
+def get_jira(access_token):
+ module_dir = os.path.dirname(__file__) # get current directory
+ with open(module_dir + '/rsa.pem', 'r') as f:
+ key_cert = f.read()
+
+ oauth_dict = {
+ 'access_token': access_token['oauth_token'],
+ 'access_token_secret': access_token['oauth_token_secret'],
+ 'consumer_key': CONF.jira_oauth_consumer_key,
+ 'key_cert': key_cert
+ }
+
+ return JIRA(server=CONF.jira_jira_url, oauth=oauth_dict)
diff --git a/cvp/opnfv_testapi/ui/auth/rsa.pem b/cvp/opnfv_testapi/ui/auth/rsa.pem
new file mode 100644
index 00000000..5ec1bbf1
--- /dev/null
+++ b/cvp/opnfv_testapi/ui/auth/rsa.pem
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAyDe0zz8Gpr1dh31c7R8LGV4p+wT0s2sUTtha1ex3GXzhEewQ
+Cx3WUW/tttnBGd0oVVNMzoaIPbQJgPnuyVx2VugELLIdPHd9ngPDq09YWzK/J3VW
+3I0sQLA5xVmGv32z4Uz7vbc7/4UM+FXPqUmhWBysR0zsm/nlLqbLs08GEyt2ZT+v
+rNQnYtWA6YuL2OHSHvkQlwibpezHzs6LV7A8mQjWbptu0FL229h6pNSyV7YM459w
+Z5RzBsPybl07P5HOVtkSizIFJ+HLc6yp4cVxjCgk4rMKyPBSG9dBEaHMlBCis61R
+2lTbCJiy7SnWGiMd28WKuvu2k8T4A9k2FzvYwwIDAQABAoIBAGqhOFtTjqBIo8If
+4tiqOsgE3UjBp+zR71vaX+4kZH2fg2J/HUA+YMC4YpqKOAwlO3DNz08CWRa7hoA5
+G5ID+0ZnhKmlJmronG8GRDQ9KqpPSXyjQmJtkQ7Wi73t4xSixqUL0dqE9qAr5O9x
+DAp1m0cI5juG3VBoc0U4Ma5KPMsB3jceeV446ZsU07LSgTIOfLNzq6oEWLWhBzLj
+rDRcGyB6iNxCsNacruW3DKrDg1cMqWqjxt6Tf4LuTWYFmGedTIktmn7VZDgXcbkK
+a7sCRr7P0br6zuIFak1ugkUECDwNznLz3+QgW/iaay6NL6qEpnMLg3Z44kP+BLma
+h5g/SvECgYEA7ewD4lG8s/iz5OIinVHIW10Bc8pEMX3+8cVCo+rq7YbWG+HqXFrv
+DUcyRu/O3SHpc4ozkhRMTsVK5xGUuWGlLG9Hit5R4Ra8oHurJMsFUqjaptd9roHi
+CMmynCFupqBwDoxMig5KxvuDqbOmo2yQOelP/UEnC+qlrux5+lClx4sCgYEA125H
+KPAi30FkRJ/7pzlNtcqzNYQh6xdgcrDIsU1zHRa4AOPYSD+WSkb7wAbns5WLlOM8
+wScpUijyfu56YDizHuID4QW4ddKGVLEbx4tt8CiPLzweeFsP/FSfpd+OK0EDs8wP
+S0b81rCkJKvGljfdl/wY3mYXOu0RZzXB55N1GqkCgYAscy+2lLbAmPJjDKyS37ii
++RlQXLWo2XVMDiKJJVaG0e4mf2qdno+S135ZKmxne/J1l5hS7l/jR5Da4rn6eHe3
+eYLQOwDpIKpVAUXUNenkq49OJGxisflc0vH/oW9eyhKlZSjXkhv+WPccOWgkmB/J
+8gDzu7xjyY7yw1N2pKKUSQKBgQCfhdB5twALk698xX6igGNT10pGuZYoMEJCCzhB
+WlmAU79jIVSZg0R1sgRfWH2gVH9se6wUVzxY02tlpI/HypSQrMo0iXji/kZsVk18
+wHljGZWVY44ojz3SGpOxT05GJzlnnRZCJsm47EpPwUcnGy0iixGbNbvD7aIya/Mu
+2NkhKQKBgBgLvhfU3sU6XYrF99L63W1vcDyoXcsmQQtz2EzPflFkdcLYoeHo13XW
+Apv7EeX+zqaeqx0v7xuVYWyde5ux9+vII4al0jToabLcd0y2k0Oxmjv40K1YVYsu
+ZqoLXriNHf4NkqgQAFu8FfV1S9RTl6+3X4z6yzf09ustxiw3KWCz
+-----END RSA PRIVATE KEY-----
diff --git a/cvp/opnfv_testapi/ui/auth/sign.py b/cvp/opnfv_testapi/ui/auth/sign.py
new file mode 100644
index 00000000..dbb40ed0
--- /dev/null
+++ b/cvp/opnfv_testapi/ui/auth/sign.py
@@ -0,0 +1,281 @@
+##############################################################################
+# Copyright (c) 2015 Orange
+# guyrodrigue.koffi@orange.com / koffirodrigue@gmail.com
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from six.moves.urllib import parse
+from tornado import gen
+from tornado import web
+
+from cas import CASClient
+from opnfv_testapi.ui.auth.jira_util import SignatureMethod_RSA_SHA1
+from opnfv_testapi.ui.auth.jira_util import get_jira
+
+from opnfv_testapi.common.config import CONF
+from opnfv_testapi.db import api as dbapi
+from opnfv_testapi.ui.auth import base
+from opnfv_testapi.ui.auth import constants as const
+
+import logging
+import oauth2 as oauth
+
+root = logging.getLogger()
+root.setLevel(logging.DEBUG)
+
+
+class SigninHandler(base.BaseHandler):
+ def get(self):
+ signin_type = self.get_query_argument("type")
+ self.set_secure_cookie("signin_type", signin_type)
+ if signin_type == "openstack":
+ self.signin_with_openstack()
+ if signin_type == "jira":
+ self.signin_with_jira()
+ if signin_type == "cas":
+ self.signin_with_cas()
+
+ def signin_with_cas(self):
+ client = CASClient(
+ version='2',
+ renew=False,
+ extra_login_params=False,
+ server_url=CONF.lfid_url,
+ service_url=CONF.lfid_return_url
+ )
+ redirect_url = client.get_login_url()
+ self.redirect(url=redirect_url, permanent=False)
+
+ def signin_with_openstack(self):
+ csrf_token = base.get_token()
+ return_endpoint = parse.urljoin(CONF.api_url,
+ CONF.osid_openid_return_to)
+ return_to = base.set_query_params(return_endpoint,
+ {const.CSRF_TOKEN: csrf_token})
+
+ params = {
+ const.OPENID_MODE: CONF.osid_openid_mode,
+ const.OPENID_NS: CONF.osid_openid_ns,
+ const.OPENID_RETURN_TO: return_to,
+ const.OPENID_CLAIMED_ID: CONF.osid_openid_claimed_id,
+ const.OPENID_IDENTITY: CONF.osid_openid_identity,
+ const.OPENID_REALM: CONF.api_url,
+ const.OPENID_NS_SREG: CONF.osid_openid_ns_sreg,
+ const.OPENID_NS_SREG_REQUIRED: CONF.osid_openid_sreg_required,
+ }
+ url = CONF.osid_openstack_openid_endpoint
+ url = base.set_query_params(url, params)
+ self.redirect(url=url, permanent=False)
+
+ def signin_with_jira(self):
+ consumer = oauth.Consumer(CONF.jira_oauth_consumer_key,
+ CONF.jira_oauth_consumer_secret)
+ client = oauth.Client(consumer)
+ client.set_signature_method(SignatureMethod_RSA_SHA1())
+
+ # Step 1. Get a request token from Jira.
+ try:
+ resp, content = client.request(CONF.jira_oauth_request_token_url,
+ "POST")
+ except Exception as e:
+ logging.error('Connect jira exception: %s', e)
+ self._auth_failure('Error: Connection to Jira failed. \
+ Please contact an Administrator')
+ return
+
+ if resp['status'] != '200':
+ logging.error('Connect jira error: %s', resp)
+ self._auth_failure('Error: Connection to Jira failed. \
+ Error code(%s). \
+ Please contact an Administrator' % (resp['status']))
+ return
+
+ # Step 2. Store the request token in a session for later use.
+ logging.warning('content is %s', content)
+ request_token = dict(parse.parse_qsl(content.decode()))
+ self.set_secure_cookie('oauth_token', request_token['oauth_token'])
+ self.set_secure_cookie('oauth_token_secret',
+ request_token['oauth_token_secret'])
+
+ # Step 3. Redirect the user to the authentication URL.
+ url = CONF.jira_oauth_authorize_url + '?oauth_token=' + \
+ request_token['oauth_token'] + \
+ '&oauth_callback=' + CONF.jira_oauth_callback_url
+ self.redirect(url=url, permanent=False)
+
+ def _auth_failure(self, message):
+ params = {'message': message}
+ url = parse.urljoin(CONF.ui_url,
+ '/#/auth_failure?' + parse.urlencode(params))
+ self.redirect(url)
+
+
+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)
+ 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),
+ const.ROLE: role
+ }
+ user = yield dbapi.db_find_one(self.table, {'openid': openid})
+ if not user:
+ dbapi.db_save(self.table, new_user_info)
+ else:
+ role = user.get(const.ROLE)
+
+ 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)
+
+
+class SigninReturnCasHandler(base.BaseHandler):
+ @web.asynchronous
+ @gen.coroutine
+ def get(self):
+ logging.warning("cas return")
+ ticket = self.get_query_argument('ticket')
+ logging.warning("ticket:%s", ticket)
+ client = CASClient(
+ version='2',
+ renew=False,
+ extra_login_params=False,
+ server_url=CONF.lfid_url,
+ service_url=CONF.lfid_return_url
+ )
+ user, attrs, _ = client.verify_ticket(ticket)
+ logging.debug("user:%s", user)
+ logging.debug("attr:%s", attrs)
+ openid = user
+ role = const.DEFAULT_ROLE
+ new_user_info = {
+ 'openid': openid,
+ 'email': attrs['mail'],
+ 'fullname': attrs['profile_name_full'],
+ const.ROLE: role
+ }
+ user = yield dbapi.db_find_one(self.table, {'openid': openid})
+ if not user:
+ dbapi.db_save(self.table, new_user_info)
+ else:
+ role = user.get(const.ROLE)
+
+ self.clear_cookie(const.OPENID)
+ self.clear_cookie(const.ROLE)
+ self.clear_cookie('ticket')
+ self.set_secure_cookie(const.OPENID, openid)
+ self.set_secure_cookie(const.ROLE, role)
+ self.set_secure_cookie('ticket', ticket)
+
+ self.redirect("/")
+
+
+class SigninReturnJiraHandler(base.BaseHandler):
+ @web.asynchronous
+ @gen.coroutine
+ def get(self):
+ logging.warning("jira return")
+ # Step 1. Use the request token in the session to build a new client.
+ consumer = oauth.Consumer(CONF.jira_oauth_consumer_key,
+ CONF.jira_oauth_consumer_secret)
+ token = oauth.Token(self.get_secure_cookie('oauth_token'),
+ self.get_secure_cookie('oauth_token_secret'))
+ client = oauth.Client(consumer, token)
+ client.set_signature_method(SignatureMethod_RSA_SHA1())
+
+ # Step 2. Request the authorized access token from Jira.
+ try:
+ resp, content = client.request(CONF.jira_oauth_access_token_url,
+ "POST")
+ except Exception as e:
+ logging.error("Connect jira exception:%s", e)
+ self._auth_failure('Error: Connection to Jira failed. \
+ Please contact an Administrator')
+ if resp['status'] != '200':
+ logging.error("Connect jira error:%s", resp)
+ self._auth_failure('Error: Connection to Jira failed. \
+ Please contact an Administrator')
+ access_token = dict(parse.parse_qsl(content.decode()))
+ logging.warning("access_token: %s", access_token)
+
+ # jira = JIRA(server=CONF.jira_jira_url, oauth=oauth_dict)
+ jira = get_jira(access_token)
+ lf_id = jira.current_user()
+ logging.warning("lf_id: %s", lf_id)
+ user = jira.myself()
+ logging.warning("user: %s", user)
+ # Step 3. Lookup the user or create them if they don't exist.
+ role = const.DEFAULT_ROLE
+ new_user_info = {
+ 'openid': lf_id,
+ 'email': user['emailAddress'],
+ 'fullname': user['displayName'],
+ const.ROLE: role
+ }
+ user = yield dbapi.db_find_one(self.table, {'openid': lf_id})
+ if not user:
+ dbapi.db_save(self.table, new_user_info)
+ else:
+ role = user.get(const.ROLE)
+
+ self.clear_cookie(const.OPENID)
+ self.clear_cookie(const.ROLE)
+ self.set_secure_cookie(const.OPENID, lf_id)
+ self.set_secure_cookie(const.ROLE, role)
+ self.redirect(url=CONF.ui_url)
+
+ def _auth_failure(self, message):
+ params = {'message': message}
+ url = parse.urljoin(CONF.ui_url,
+ '/#/auth_failure?' + parse.urlencode(params))
+ self.redirect(url)
+
+
+class SignoutHandler(base.BaseHandler):
+ def get(self):
+ """Handle signout request."""
+ self.clear_cookie(const.OPENID)
+ self.clear_cookie(const.ROLE)
+ signin_type = self.get_secure_cookie("signin_type")
+ if signin_type == "openstack":
+ self.signout_openstack()
+ if signin_type == "jira":
+ self.signout_jira()
+ if signin_type == 'cas':
+ self.signout_cas()
+
+ def signout_openstack(self):
+ params = {'openid_logout': CONF.osid_openid_logout_endpoint}
+ url = parse.urljoin(CONF.ui_url,
+ '/#/logout?' + parse.urlencode(params))
+ self.redirect(url)
+
+ def signout_jira(self):
+ params = {'alt_token': ''}
+ url = parse.urljoin(CONF.jira_jira_url,
+ '/logout?' + parse.urlencode(params))
+ self.redirect(url)
+
+ def signout_cas(self):
+ client = CASClient(
+ version='2',
+ renew=False,
+ extra_login_params=False,
+ server_url=CONF.lfid_url,
+ service_url=CONF.lfid_return_url
+ )
+ url = client.get_logout_url(CONF.ui_url)
+ self.redirect(url)
diff --git a/cvp/opnfv_testapi/ui/auth/user.py b/cvp/opnfv_testapi/ui/auth/user.py
new file mode 100644
index 00000000..a695da45
--- /dev/null
+++ b/cvp/opnfv_testapi/ui/auth/user.py
@@ -0,0 +1,35 @@
+##############################################################################
+# Copyright (c) 2015 Orange
+# guyrodrigue.koffi@orange.com / koffirodrigue@gmail.com
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+from tornado import gen
+from tornado import web
+
+from opnfv_testapi.common import raises
+from opnfv_testapi.db import api as dbapi
+from opnfv_testapi.ui.auth import base
+
+
+class ProfileHandler(base.BaseHandler):
+ @web.asynchronous
+ @gen.coroutine
+ def get(self):
+ openid = self.get_secure_cookie('openid')
+ if openid:
+ try:
+ user = yield dbapi.db_find_one(self.table, {'openid': openid})
+ self.finish_request({
+ "openid": user.get('openid'),
+ "email": user.get('email'),
+ "fullname": user.get('fullname'),
+ "role": user.get('role', 'user'),
+ "type": self.get_secure_cookie('signin_type')
+ })
+ except Exception:
+ pass
+ raises.Unauthorized('Unauthorized')
diff --git a/cvp/opnfv_testapi/ui/root.py b/cvp/opnfv_testapi/ui/root.py
new file mode 100644
index 00000000..5b2c922d
--- /dev/null
+++ b/cvp/opnfv_testapi/ui/root.py
@@ -0,0 +1,10 @@
+from opnfv_testapi.resources.handlers import GenericApiHandler
+from opnfv_testapi.common.config import CONF
+
+
+class RootHandler(GenericApiHandler):
+ def get_template_path(self):
+ return CONF.static_path
+
+ def get(self):
+ self.render('testapi-ui/index.html')