[PATCH V4 0/5] Switch to a traditional login flow

From: ShaoHe Feng <shaohef@linux.vnet.ibm.com> V3 -> V4: improve the login page. V2 -> V3: improve when to show timeout message V1 -> V2: when username or password is wrong, back to login page with an error message. when session time out, back to login page with an error message. ShaoHe Feng (5): create a new login page redirect the URL to login page when session timeout or first login when login successfully, redirect to the last page. login page prompts error when username or password is wrong login page prompts error when session timeout src/kimchi/auth.py | 53 +++++++++++++++++---- src/kimchi/config.py.in | 7 +++ src/kimchi/root.py | 38 +++++++++++---- src/kimchi/server.py | 2 + tests/test_rest.py | 1 - ui/images/progressing.gif | Bin 0 -> 1152 bytes ui/js/src/kimchi.main.js | 14 ++++-- ui/pages/login.html.tmpl | 118 ++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 209 insertions(+), 24 deletions(-) create mode 100644 ui/images/progressing.gif create mode 100644 ui/pages/login.html.tmpl -- 1.9.3

From: ShaoHe Feng <shaohef@linux.vnet.ibm.com> this page is used for the session timeout or first login. Signed-off-by: ShaoHe Feng <shaohef@linux.vnet.ibm.com> Signed-off-by: Yu Xin Huo <huoyuxin@linux.vnet.ibm.com> --- ui/images/progressing.gif | Bin 0 -> 1152 bytes ui/pages/login.html.tmpl | 110 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 ui/images/progressing.gif create mode 100644 ui/pages/login.html.tmpl diff --git a/ui/images/progressing.gif b/ui/images/progressing.gif new file mode 100644 index 0000000000000000000000000000000000000000..6552d41d9d4c874091bb51931c6adf64a95e8bd0 GIT binary patch literal 1152 zcmZ?wbhEHb6k!ly*v!E2|NsBHckiA#b7s?~O&uK_IXO9@p`oUxrgCy}4A_9;e{Mh5 zkYH!W09PYD17=2`8pWS1oFWVy3_2k7AY&O=+5}E^`WY^hIhD@I;2`qkhUamCHs#2? zD{Sl5ddqAu;o;E8n07H&VTOd$9w+z3Od8dz)1OOlg@hfQ?6P6gg)Nhut~h8s_iHy| zTx?f<DfNuF4qs+in1T70o7P)P87f1<g5(0~t-`bz%`;<c7;R*Hby&0+Cd*D$nyx)7 zxH>hoEs9-2GEYbl<TO?^r*$AZO&~F#<dR{_!xK!0-EtLPZ*p|yWoF`%>SmZA?aQ<9 zUT3kA$h=8X2{%vLaEeZ2FfFvxHCy+@)2xTXXx&nW19L8~I4mhFCLSR*N5YUN_g8RC zxvG$d3rC2Yoni>Hr8Z+mk+UtMt&Fyriq3S|nYyz*lh|s_)v7fmBnyPWjzbSOeSwpn ztM^*GK3gr@WX{W=;H4@s$Eix`o#&Z+UC|vq;?Mm$*qz>q{MpOLKD$V>lW^GM@gRp< zTfqAxmTMW#NQtz1Ow(GUENaLTnG@~OSSl%)a-~@^*=_L^Wu6LazRZG$dI^iu%U-z% zg<00g*_8=1ySk){v9Pk(*~<9qvQD2qQDdsojG1{NY_a__TQemj7eK-u-Fqm>f>utm zWMS=L&jJU059@Suul#8<RCK0juuU^pO9X~|9`=wYHdzoCYCehQmh?GGSt;IO&EY61 z^|cU^WKC}pQa3GC)^bS)J5HFrADAqr$@=Tg_KIeUMR6Q@nr+9QW_y9zNsLj!T)@YO zmFW>EN`0IXPrZ>S5M3ptD%hrTMQhc8T>+_g4tF#}E)IMpc<k8J3C>bEce!tTI4V-^ zEEL|{;B8wW%<SUK!k*>CYhksVX{CWv466RGb|aXRcN%49rjY*aHrnWY;d4a(eT< sn9_<U58lHODbH9KGQC;#6@s@G?AG7t-SQycLr1Fr#FC=U9tH+$0KoWQ9smFU literal 0 HcmV?d00001 diff --git a/ui/pages/login.html.tmpl b/ui/pages/login.html.tmpl new file mode 100644 index 0000000..bf93732 --- /dev/null +++ b/ui/pages/login.html.tmpl @@ -0,0 +1,110 @@ +#* + * Project Kimchi + * + * Copyright IBM, Corp. 2014 + * + * 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. + *# +#unicode UTF-8 +#import gettext +#from kimchi.cachebust import href +#silent t = gettext.translation($lang.domain, $lang.localedir, languages=$lang.lang) +#silent _ = t.gettext +#silent _t = t.gettext +#from kimchi.config import get_version +<!DOCTYPE html> +<html lang="$lang.lang[0]"> +<head> +<meta charset="UTF-8"> +<title>Kimchi</title> +<meta http-equiv="X-UA-Compatible" content="IE=edge"/> +<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" /> +<link rel="shortcut icon" href="images/logo.ico"> +<link rel="stylesheet" href="$href('css/theme-default.min.css')"> +<script src="$href('libs/jquery-1.10.0.min.js')"></script> +<script src="$href('libs/jquery-ui.min.js')"></script> +<script src="$href('libs/jquery-ui-i18n.min.js')"></script> +<script src="$href('js/kimchi.min.js')"></script> +<style type="text/css"> +.topbar select { + float: right; + margin-top: 12px; + margin-right: 10px; +} +.login-area { + margin: 120px auto 0; +} +.login-panel { + width: 315px; +} +.err-area { + height: 80px; +} +.err-mess { + color: #C85305; +} +</style> +<script> +function changeLang() { + var lang = document.getElementById('userLang').value; + kimchi.cookie.set('kimchiLang', lang, 365); + window.location.reload(); +} +function setLang() { + var defaultLang = 'en_US'; + var clientLang = document.getElementsByTagName("html")[0].getAttribute("lang"); + var persistLang = kimchi.cookie.get('kimchiLang'); + document.getElementById("userLang").value = persistLang || clientLang || defaultLang; +} +function updateBtnLabel() { + document.getElementById("login").style.display = "none"; + document.getElementById("logging").style.display = ""; +} +function init() { + setLang(); +} +</script> +</head> +<body onload="init()"> +<div class="container topbar"> + <span id="logo"><img alt="Project Kimchi" src="images/theme-default/logo-white.png"></span> + <select id="userLang" onchange="changeLang()"> + <option value="en_US">English (US)</option> + <option value="zh_CN">中文(简体)</option> + <option value="pt_BR">Português (Brasil)</option> + </select> +</div> +<div id="login-window" class="login-area"> + <div class="err-area"> + <div id="messUserPass" class="err-mess" style="display: none;">$_("The username or password you entered is incorrect. Please try again.")</div> + <div id="messSession" class="err-mess" style="display: none;">$_("Session timeout, please re-login.")</div> + </div> + <form id="form-login" action="/login" method="POST" class="login-panel" onsubmit="updateBtnLabel();"> + <div class="row"> + <input type="text" id="username" name="username" required="required" placeholder="$_("User Name")" autofocus/> + <div id="username-msg" class="msg-required"></div> + </div> + <div class="row"> + <input type="password" id="password" name="password" required="required" placeholder="$_("Password")" /> + <div id="password-msg" class="msg-required"></div> + </div> + <div class="row"> + <button id="btn-login" class="btn-normal"> + <label id="login">$_("Log in")</label> + <label id="logging" style="display: none;">$_("Logging in...")</label> + </button> + </div> + </form> +</div> +</body> +</html> -- 1.9.3

From: ShaoHe Feng <shaohef@linux.vnet.ibm.com> If the content type is application/json still raise 401 status code. And let UI redirect to login page. or the backe redirects to login page directly. enable kimchi-ui.html authentication protected. and update the test case Signed-off-by: ShaoHe Feng <shaohef@linux.vnet.ibm.com> Signed-off-by: Yu Xin Huo <huoyuxin@linux.vnet.ibm.com> --- src/kimchi/auth.py | 11 +++++++++++ src/kimchi/config.py.in | 3 +++ src/kimchi/root.py | 28 +++++++++++++++++++--------- tests/test_rest.py | 1 - ui/js/src/kimchi.main.js | 5 +---- 5 files changed, 34 insertions(+), 14 deletions(-) diff --git a/src/kimchi/auth.py b/src/kimchi/auth.py index dc78ded..a38dbd3 100644 --- a/src/kimchi/auth.py +++ b/src/kimchi/auth.py @@ -28,6 +28,7 @@ import re import termios import time +import urllib2 from kimchi import template @@ -41,6 +42,12 @@ REFRESH = 'robot-refresh' +def redirect_login(): + next_url = urllib2.quote( + cherrypy.request.path_info.encode('utf-8'), safe="") + raise cherrypy.HTTPRedirect("/login.html?next=%s" % next_url, 303) + + def debug(msg): pass # cherrypy.log.error(msg) @@ -234,6 +241,10 @@ def kimchiauth(admin_methods=None): raise cherrypy.HTTPError(403) return + # not a REST full request, redirect login page directly + if not template.can_accept('application/json'): + redirect_login() + if not from_browser(): cherrypy.response.headers['WWW-Authenticate'] = 'Basic realm=kimchi' diff --git a/src/kimchi/config.py.in b/src/kimchi/config.py.in index 0206570..d4cbda0 100644 --- a/src/kimchi/config.py.in +++ b/src/kimchi/config.py.in @@ -187,6 +187,9 @@ class KimchiConfig(dict): '/spice.html': { 'tools.kimchiauth.on': True }, + '/kimchi-ui.html': { + 'tools.kimchiauth.on': True + }, '/data/screenshots': { 'tools.staticdir.on': True, 'tools.staticdir.dir': get_screenshot_path(), diff --git a/src/kimchi/root.py b/src/kimchi/root.py index 8b1d09b..181ab13 100644 --- a/src/kimchi/root.py +++ b/src/kimchi/root.py @@ -81,7 +81,7 @@ def get(self): @cherrypy.expose def default(self, page, **kwargs): if page.endswith('.html'): - return template.render(page, None) + return template.render(page, kwargs) raise cherrypy.HTTPError(404) @cherrypy.expose @@ -110,14 +110,24 @@ def __init__(self, model, dev_env): self.messages = messages @cherrypy.expose - def login(self, *args): - params = parse_request() - try: - username = params['username'] - password = params['password'] - except KeyError, item: - e = MissingParameter('KCHAUTH0003E', {'item': str(item)}) - raise cherrypy.HTTPError(400, e.message) + def login(self, *args, **kwargs): + username = kwargs.get('username') + password = kwargs.get('password') + # forms base authentication + if username is not None: + # UI can pass the redirect url by "next" query parameter + next_url = kwargs.get('next', "/") + next_url = type(next_url) is list and next_url[0] + auth.login(username, password) + raise cherrypy.HTTPRedirect(next_url, 303) + else: + try: + params = parse_request() + username = params['username'] + password = params['password'] + except KeyError, item: + e = MissingParameter('KCHAUTH0003E', {'item': str(item)}) + raise cherrypy.HTTPError(400, e.message) try: user_info = auth.login(username, password) diff --git a/tests/test_rest.py b/tests/test_rest.py index 7ed94cb..18ba66e 100644 --- a/tests/test_rest.py +++ b/tests/test_rest.py @@ -1431,7 +1431,6 @@ def test_auth_unprotected(self): '/css/theme-default.min.css', '/libs/jquery-1.10.0.min.js', '/images/icon-vm.png', - '/kimchi-ui.html', '/login-window.html', '/logout'] for uri in uris: diff --git a/ui/js/src/kimchi.main.js b/ui/js/src/kimchi.main.js index 184029d..2a8f461 100644 --- a/ui/js/src/kimchi.main.js +++ b/ui/js/src/kimchi.main.js @@ -227,10 +227,7 @@ kimchi.main = function() { kimchi.previousAjax = ajaxSettings; $(".empty-when-logged-off").empty(); $(".remove-when-logged-off").remove(); - kimchi.window.open({ - url: 'login-window.html', - id: 'login-window-wrapper' - }); + document.location.href='login.html'; return; } else if((jqXHR['status'] == 0) && ("error"==jqXHR.statusText)) { -- 1.9.3

From: ShaoHe Feng <shaohef@linux.vnet.ibm.com> let cookie remember the last page. Signed-off-by: ShaoHe Feng <shaohef@linux.vnet.ibm.com> Signed-off-by: Yu Xin Huo <huoyuxin@linux.vnet.ibm.com> --- src/kimchi/root.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/kimchi/root.py b/src/kimchi/root.py index 181ab13..651c847 100644 --- a/src/kimchi/root.py +++ b/src/kimchi/root.py @@ -93,7 +93,11 @@ def tabs(self, page, **kwargs): data['ui_dir'] = paths.ui_dir if page.endswith('.html'): - return template.render('tabs/' + page, data) + context = template.render('tabs/' + page, data) + cherrypy.response.cookie[ + "lastPage"] = "/#tabs/" + page.rstrip(".html") + cherrypy.response.cookie['lastPage']['path'] = '/' + return context raise cherrypy.HTTPError(404) @@ -115,9 +119,13 @@ def login(self, *args, **kwargs): password = kwargs.get('password') # forms base authentication if username is not None: - # UI can pass the redirect url by "next" query parameter - next_url = kwargs.get('next', "/") - next_url = type(next_url) is list and next_url[0] + next_url = cherrypy.request.cookie.get("lastPage") + if next_url is None: + # UI can pass the redirect url by "next" query parameter + next_url = kwargs.get('next', "/") + next_url = type(next_url) is list and next_url[0] + else: + next_url = next_url.value auth.login(username, password) raise cherrypy.HTTPRedirect(next_url, 303) else: -- 1.9.3

From: ShaoHe Feng <shaohef@linux.vnet.ibm.com> when username or password is wrong, come back to login page with an error message. Signed-off-by: ShaoHe Feng <shaohef@linux.vnet.ibm.com> --- src/kimchi/auth.py | 19 +++++++++++++------ ui/pages/login.html.tmpl | 6 ++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/kimchi/auth.py b/src/kimchi/auth.py index a38dbd3..9cb40d3 100644 --- a/src/kimchi/auth.py +++ b/src/kimchi/auth.py @@ -131,9 +131,8 @@ def _pam_conv(auth, query_list, userData=None): try: auth.authenticate() - except PAM.error, (resp, code): - msg_args = {'username': username, 'code': code} - raise OperationFailed("KCHAUTH0001E", msg_args) + except PAM.error: + raise return True @@ -196,9 +195,17 @@ def check_auth_httpba(): def login(username, password): - if not authenticate(username, password): - debug("User cannot be verified with the supplied password") - return None + try: + if not authenticate(username, password): + debug("User cannot be verified with the supplied password") + return None + except PAM.error, (resp, code): + if (cherrypy.request.path_info == "/login" and + not template.can_accept('application/json')): + raise cherrypy.HTTPRedirect("/login.html?error=userPassWrong", 303) + msg_args = {'username': username, 'code': code} + raise OperationFailed("KCHAUTH0001E", msg_args) + user = User(username) debug("User verified, establishing session") cherrypy.session.acquire_lock() diff --git a/ui/pages/login.html.tmpl b/ui/pages/login.html.tmpl index bf93732..583efd4 100644 --- a/ui/pages/login.html.tmpl +++ b/ui/pages/login.html.tmpl @@ -70,8 +70,14 @@ function updateBtnLabel() { document.getElementById("login").style.display = "none"; document.getElementById("logging").style.display = ""; } +function setMessage() { + var err = "$getVar('data.error', '')"; + if(err=="userPassWrong") + document.getElementById("messUserPass").style.display = ""; +} function init() { setLang(); + setMessage(); } </script> </head> -- 1.9.3

From: ShaoHe Feng <shaohef@linux.vnet.ibm.com> When session timeout, come back to login page with an error message. When session logout, close session directly. Signed-off-by: ShaoHe Feng <shaohef@linux.vnet.ibm.com> --- src/kimchi/auth.py | 25 ++++++++++++++++++++++--- src/kimchi/config.py.in | 4 ++++ src/kimchi/server.py | 2 ++ ui/js/src/kimchi.main.js | 11 +++++++++-- ui/pages/login.html.tmpl | 2 ++ 5 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/kimchi/auth.py b/src/kimchi/auth.py index 9cb40d3..59889ed 100644 --- a/src/kimchi/auth.py +++ b/src/kimchi/auth.py @@ -161,7 +161,7 @@ def check_auth_session(): cherrypy.session.timeout * 60): cherrypy.session[USER_NAME] = None cherrypy.lib.sessions.expire() - raise cherrypy.HTTPError(401) + raise cherrypy.HTTPError(401, "sessionTimeout") else: cherrypy.session[REFRESH] = time.time() return True @@ -223,7 +223,7 @@ def logout(): cherrypy.session[USER_NAME] = None cherrypy.session[REFRESH] = 0 cherrypy.session.release_lock() - cherrypy.lib.sessions.expire() + cherrypy.lib.sessions.close() def has_permission(admin_methods): @@ -238,6 +238,7 @@ def has_permission(admin_methods): def kimchiauth(admin_methods=None): debug("Entering kimchiauth...") + session_missing = cherrypy.session.missing if check_auth_session(): if not has_permission(admin_methods): raise cherrypy.HTTPError(403) @@ -249,11 +250,29 @@ def kimchiauth(admin_methods=None): return # not a REST full request, redirect login page directly - if not template.can_accept('application/json'): + if ("Accept" in cherrypy.request.headers and + not template.can_accept('application/json')): redirect_login() + # from browser, and it stays on one page. + if session_missing and cherrypy.request.cookie.get("lastPage") is not None: + raise cherrypy.HTTPError(401, "sessionTimeout") + if not from_browser(): cherrypy.response.headers['WWW-Authenticate'] = 'Basic realm=kimchi' e = InvalidOperation('KCHAUTH0002E') raise cherrypy.HTTPError(401, e.message.encode('utf-8')) + + +def kimchisession(admin_methods=None): + session = cherrypy.request.cookie.get("kimchi") + last_page = cherrypy.request.cookie.get("lastPage") + headers = cherrypy.request.headers + authheader = headers.get('AUTHORIZATION') + # when client browser first login in, both the session and lastPage cookie + # are None. + # when session timeout, only session cookie is None. + if (session is None and last_page is None and authheader is None and + ("Accept" in headers and not template.can_accept('application/json'))): + redirect_login() diff --git a/src/kimchi/config.py.in b/src/kimchi/config.py.in index d4cbda0..f557516 100644 --- a/src/kimchi/config.py.in +++ b/src/kimchi/config.py.in @@ -179,6 +179,7 @@ class KimchiConfig(dict): 'tools.sessions.locking': 'explicit', 'tools.sessions.storage_type': 'ram', 'tools.sessions.timeout': SESSIONSTIMEOUT, + 'tools.kimchisession.on': True, 'tools.kimchiauth.on': False }, '/vnc_auto.html': { @@ -190,6 +191,9 @@ class KimchiConfig(dict): '/kimchi-ui.html': { 'tools.kimchiauth.on': True }, + '/login.html': { + 'tools.kimchisession.on': False, + }, '/data/screenshots': { 'tools.staticdir.on': True, 'tools.staticdir.dir': get_screenshot_path(), diff --git a/src/kimchi/server.py b/src/kimchi/server.py index 7344349..30140ce 100644 --- a/src/kimchi/server.py +++ b/src/kimchi/server.py @@ -77,6 +77,8 @@ def __init__(self, options): cherrypy.tools.nocache = cherrypy.Tool('on_end_resource', set_no_cache) cherrypy.tools.kimchiauth = cherrypy.Tool('before_handler', auth.kimchiauth) + cherrypy.tools.kimchisession = cherrypy.Tool('before_request_body', + auth.kimchisession) # Setting host to 127.0.0.1. This makes kimchi runs # as a localhost app, inaccessible to the outside # directly. You must go through the proxy. diff --git a/ui/js/src/kimchi.main.js b/ui/js/src/kimchi.main.js index 2a8f461..4dc57e5 100644 --- a/ui/js/src/kimchi.main.js +++ b/ui/js/src/kimchi.main.js @@ -138,7 +138,13 @@ kimchi.main = function() { */ var loadPage = function(url) { // Get the page content through Ajax and render it. - url && $('#main').load(url, function(responseText, textStatus, jqXHR) {}); + url && $('#main').load(url, function(responseText, textStatus, jqXHR) { + if (jqXHR['status'] === 401 || jqXHR['status'] === 303) { + var isSessionTimeout = jqXHR['responseText'].indexOf("sessionTimeout")!=-1; + document.location.href= isSessionTimeout ? 'login.html?error=sessionTimeout' : 'login.html'; + return; + } + }); }; /* @@ -223,11 +229,12 @@ kimchi.main = function() { } if (jqXHR['status'] === 401) { + var isSessionTimeout = jqXHR['responseText'].indexOf("sessionTimeout")!=-1; kimchi.user.showUser(false); kimchi.previousAjax = ajaxSettings; $(".empty-when-logged-off").empty(); $(".remove-when-logged-off").remove(); - document.location.href='login.html'; + document.location.href= isSessionTimeout ? 'login.html?error=sessionTimeout' : 'login.html'; return; } else if((jqXHR['status'] == 0) && ("error"==jqXHR.statusText)) { diff --git a/ui/pages/login.html.tmpl b/ui/pages/login.html.tmpl index 583efd4..b3e69d4 100644 --- a/ui/pages/login.html.tmpl +++ b/ui/pages/login.html.tmpl @@ -74,6 +74,8 @@ function setMessage() { var err = "$getVar('data.error', '')"; if(err=="userPassWrong") document.getElementById("messUserPass").style.display = ""; + if(err=="sessionTimeout") + document.getElementById("messSession").style.display = ""; } function init() { setLang(); -- 1.9.3

Found 1 issue when testing input wrong user/pass, response error of 'incorrect user/pass', input correct user/pass again. got error below This only occurs when first time login after clearing brower cache. On 6/12/2014 12:24 AM, shaohef@linux.vnet.ibm.com wrote:
From: ShaoHe Feng <shaohef@linux.vnet.ibm.com>
V3 -> V4: improve the login page.
V2 -> V3: improve when to show timeout message
V1 -> V2: when username or password is wrong, back to login page with an error message. when session time out, back to login page with an error message.
ShaoHe Feng (5): create a new login page redirect the URL to login page when session timeout or first login when login successfully, redirect to the last page. login page prompts error when username or password is wrong login page prompts error when session timeout
src/kimchi/auth.py | 53 +++++++++++++++++---- src/kimchi/config.py.in | 7 +++ src/kimchi/root.py | 38 +++++++++++---- src/kimchi/server.py | 2 + tests/test_rest.py | 1 - ui/images/progressing.gif | Bin 0 -> 1152 bytes ui/js/src/kimchi.main.js | 14 ++++-- ui/pages/login.html.tmpl | 118 ++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 209 insertions(+), 24 deletions(-) create mode 100644 ui/images/progressing.gif create mode 100644 ui/pages/login.html.tmpl
participants (2)
-
shaohef@linux.vnet.ibm.com
-
Yu Xin Huo