aboutsummaryrefslogtreecommitdiffstats
path: root/opnfv_testapi/ui/auth
diff options
context:
space:
mode:
authorxudan <xudan16@huawei.com>2018-07-06 05:16:40 -0400
committerxudan <xudan16@huawei.com>2018-07-06 05:21:42 -0400
commitb3e40f026d655501bfa581452c447784604ecb05 (patch)
tree406f8bfc1abc1b33f98153d03abd34ef7b0e2fe9 /opnfv_testapi/ui/auth
parentb1b0ea32d1a296c7d055c5391261dcad6be48c63 (diff)
Move all web portal code to the new repo dovetail-webportal
This is only the first step to simply copy the file here. There still need some more work to make sure all work well. All the changes will be submitted with other patches to make it easily to review. JIRA: DOVETAIL-671 Change-Id: I64d32a9df562184166b6199e2719f298687d1a0a Signed-off-by: xudan <xudan16@huawei.com>
Diffstat (limited to 'opnfv_testapi/ui/auth')
-rw-r--r--opnfv_testapi/ui/auth/__init__.py0
-rw-r--r--opnfv_testapi/ui/auth/base.py35
-rw-r--r--opnfv_testapi/ui/auth/constants.py18
-rw-r--r--opnfv_testapi/ui/auth/jira_util.py66
-rw-r--r--opnfv_testapi/ui/auth/rsa.pem27
-rw-r--r--opnfv_testapi/ui/auth/sign.py281
-rw-r--r--opnfv_testapi/ui/auth/user.py35
7 files changed, 462 insertions, 0 deletions
diff --git a/opnfv_testapi/ui/auth/__init__.py b/opnfv_testapi/ui/auth/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/opnfv_testapi/ui/auth/__init__.py
diff --git a/opnfv_testapi/ui/auth/base.py b/opnfv_testapi/ui/auth/base.py
new file mode 100644
index 0000000..bea87c4
--- /dev/null
+++ b/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/opnfv_testapi/ui/auth/constants.py b/opnfv_testapi/ui/auth/constants.py
new file mode 100644
index 0000000..44ccb46
--- /dev/null
+++ b/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/opnfv_testapi/ui/auth/jira_util.py b/opnfv_testapi/ui/auth/jira_util.py
new file mode 100644
index 0000000..5ec91a7
--- /dev/null
+++ b/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/opnfv_testapi/ui/auth/rsa.pem b/opnfv_testapi/ui/auth/rsa.pem
new file mode 100644
index 0000000..5ec1bbf
--- /dev/null
+++ b/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/opnfv_testapi/ui/auth/sign.py b/opnfv_testapi/ui/auth/sign.py
new file mode 100644
index 0000000..dbb40ed
--- /dev/null
+++ b/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/opnfv_testapi/ui/auth/user.py b/opnfv_testapi/ui/auth/user.py
new file mode 100644
index 0000000..a695da4
--- /dev/null
+++ b/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')