diff options
Diffstat (limited to 'opnfv_testapi/ui')
-rw-r--r-- | opnfv_testapi/ui/__init__.py | 0 | ||||
-rw-r--r-- | opnfv_testapi/ui/auth/__init__.py | 0 | ||||
-rw-r--r-- | opnfv_testapi/ui/auth/base.py | 35 | ||||
-rw-r--r-- | opnfv_testapi/ui/auth/constants.py | 18 | ||||
-rw-r--r-- | opnfv_testapi/ui/auth/jira_util.py | 66 | ||||
-rw-r--r-- | opnfv_testapi/ui/auth/rsa.pem | 27 | ||||
-rw-r--r-- | opnfv_testapi/ui/auth/sign.py | 281 | ||||
-rw-r--r-- | opnfv_testapi/ui/auth/user.py | 35 | ||||
-rw-r--r-- | opnfv_testapi/ui/root.py | 10 |
9 files changed, 472 insertions, 0 deletions
diff --git a/opnfv_testapi/ui/__init__.py b/opnfv_testapi/ui/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/opnfv_testapi/ui/__init__.py 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') diff --git a/opnfv_testapi/ui/root.py b/opnfv_testapi/ui/root.py new file mode 100644 index 0000000..5b2c922 --- /dev/null +++ b/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') |