[Kimchi-devel] [PATCH v4] [Wok] Bug fix #147: Block authentication request after too many failures
Aline Manera
alinefm at linux.vnet.ibm.com
Fri Feb 3 18:54:26 UTC 2017
Also, after logging, the user logs were not loaded and in the logs there
are:
11; Fedora; Linux x86_64; rv:51.0) Gecko/20100101 Firefox/51.0"
[03/Feb/2017:16:52:43] HTTP
Request Headers:
COOKIE: wok=1780a4b70362519eb0a809690949fef39712e6cb; username=admin;
user_role=admin; lastPage="/#/tabs/settings"
Remote-Addr: 127.0.0.1
X-REAL-IP: 127.0.0.1
USER-AGENT: Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:51.0)
Gecko/20100101 Firefox/51.0
CONNECTION: close
REFERER: https://localhost:8001/
X-REQUESTED-WITH: XMLHttpRequest
DNT: 1
HOST: localhost
ACCEPT: application/json, text/javascript, */*; q=0.01
ACCEPT-LANGUAGE: en-US,en;q=0.5
X-FORWARDED-FOR: 127.0.0.1
Content-Type: application/json
ACCEPT-ENCODING: gzip, deflate, br
INFO:cherrypy.error.140313708401360:[03/Feb/2017:16:52:43] HTTP
Request Headers:
COOKIE: wok=1780a4b70362519eb0a809690949fef39712e6cb; username=admin;
user_role=admin; lastPage="/#/tabs/settings"
Remote-Addr: 127.0.0.1
X-REAL-IP: 127.0.0.1
USER-AGENT: Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:51.0)
Gecko/20100101 Firefox/51.0
CONNECTION: close
REFERER: https://localhost:8001/
X-REQUESTED-WITH: XMLHttpRequest
DNT: 1
HOST: localhost
ACCEPT: application/json, text/javascript, */*; q=0.01
ACCEPT-LANGUAGE: en-US,en;q=0.5
X-FORWARDED-FOR: 127.0.0.1
Content-Type: application/json
ACCEPT-ENCODING: gzip, deflate, br
[03/Feb/2017:16:52:43] HTTP Traceback (most recent call last):
File "/usr/lib/python2.7/site-packages/cherrypy/_cprequest.py", line
670, in respond
response.body = self.handler()
File "/usr/lib/python2.7/site-packages/cherrypy/lib/encoding.py",
line 217, in __call__
self.body = self.oldhandler(*args, **kwargs)
File "/usr/lib/python2.7/site-packages/cherrypy/_cpdispatch.py", line
61, in __call__
return self.callable(*self.args, **self.kwargs)
File "/home/alinefm/wok/src/wok/control/base.py", line 438, in index
return self.get(params)
File "/home/alinefm/wok/src/wok/control/logs.py", line 38, in get
res_list = get_list(filter_params)
File "/home/alinefm/wok/src/wok/model/logs.py", line 31, in get_list
return RequestParser().getRecords()
File "/home/alinefm/wok/src/wok/reqlogger.py", line 206, in getRecords
text = self.getTranslatedMessage(message, error, uri)
File "/home/alinefm/wok/src/wok/reqlogger.py", line 175, in
getTranslatedMessage
text = msg.get_text(prepend_code=False, translate=True)
File "/home/alinefm/wok/src/wok/message.py", line 89, in get_text
msg = decode_value(msg) % self.args
KeyError: u'username'
ERROR:cherrypy.error.140313708401360:[03/Feb/2017:16:52:43] HTTP
Traceback (most recent call last):
File "/usr/lib/python2.7/site-packages/cherrypy/_cprequest.py", line
670, in respond
response.body = self.handler()
File "/usr/lib/python2.7/site-packages/cherrypy/lib/encoding.py",
line 217, in __call__
self.body = self.oldhandler(*args, **kwargs)
File "/usr/lib/python2.7/site-packages/cherrypy/_cpdispatch.py", line
61, in __call__
return self.callable(*self.args, **self.kwargs)
File "/home/alinefm/wok/src/wok/control/base.py", line 438, in index
return self.get(params)
File "/home/alinefm/wok/src/wok/control/logs.py", line 38, in get
res_list = get_list(filter_params)
File "/home/alinefm/wok/src/wok/model/logs.py", line 31, in get_list
return RequestParser().getRecords()
File "/home/alinefm/wok/src/wok/reqlogger.py", line 206, in getRecords
text = self.getTranslatedMessage(message, error, uri)
File "/home/alinefm/wok/src/wok/reqlogger.py", line 175, in
getTranslatedMessage
text = msg.get_text(prepend_code=False, translate=True)
File "/home/alinefm/wok/src/wok/message.py", line 89, in get_text
msg = decode_value(msg) % self.args
KeyError: u'username'
On 02/03/2017 04:44 PM, Aline Manera wrote:
> Ramon,
>
> There is a test failing:
>
> ======================================================================
> FAIL: test_user_log (test_api.APITests)
> ----------------------------------------------------------------------
> Traceback (most recent call last):
> File "test_api.py", line 73, in test_user_log
> self.assertIn('records', conf)
> AssertionError: 'records' not found in {u'reason': u'The server
> encountered an unexpected condition which prevented it from fulfilling
> the request.', u'code': u'500 Internal Server Error', u'call_stack':
> u'Traceback (most recent call last):\n File
> "/usr/lib/python2.7/site-packages/cherrypy/_cprequest.py", line 670,
> in respond\n response.body = self.handler()\n File
> "/usr/lib/python2.7/site-packages/cherrypy/lib/encoding.py", line 217,
> in __call__\n self.body = self.oldhandler(*args, **kwargs)\n File
> "/usr/lib/python2.7/site-packages/cherrypy/_cpdispatch.py", line 61,
> in __call__\n return self.callable(*self.args, **self.kwargs)\n
> File "/home/alinefm/wok/src/wok/control/base.py", line 438, in
> index\n return self.get(params)\n File
> "/home/alinefm/wok/src/wok/control/logs.py", line 38, in get\n
> res_list = get_list(filter_params)\n File
> "/home/alinefm/wok/src/wok/model/logs.py", line 29, in get_list\n
> return RequestParser().getFilteredRecords(filter_params)\n File
> "/home/alinefm/wok/src/wok/reqlogger.py", line 264, in
> getFilteredRecords\n records = self.getRecords()\n File
> "/home/alinefm/wok/src/wok/reqlogger.py", line 206, in getRecords\n
> text = self.getTranslatedMessage(message, error, uri)\n File
> "/home/alinefm/wok/src/wok/reqlogger.py", line 175, in
> getTranslatedMessage\n text = msg.get_text(prepend_code=False,
> translate=True)\n File "/home/alinefm/wok/src/wok/message.py", line
> 89, in get_text\n msg = decode_value(msg) % self.args\nKeyError:
> u\'username\'\n'}
>
>
>
> On 02/03/2017 03:34 PM, ramonn at linux.vnet.ibm.com wrote:
>> From: Ramon Medeiros <ramonn at linux.vnet.ibm.com>
>>
>> To prevent brute force attack, creates a mechanism to allow 3 tries
>> first. After that, a timeout will start and will be added 30 seconds for
>> each failed try in a row.
>>
>> Signed-off-by: Ramon Medeiros <ramonn at linux.vnet.ibm.com>
>> ---
>> Changes:
>>
>> v4:
>> Use API.json for input validation
>>
>> v3:
>> Improve error handling on login page
>>
>> v2:
>> Set timeout by user, ip and session id. This will avoid trouble with
>> users using the same ip, like NAT.
>>
>> src/wok/API.json | 25 +++++++++++++++++-
>> src/wok/i18n.py | 5 +++-
>> src/wok/root.py | 69
>> +++++++++++++++++++++++++++++++++++++++++-------
>> ui/js/src/wok.login.js | 19 ++++++++-----
>> ui/pages/i18n.json.tmpl | 5 +++-
>> ui/pages/login.html.tmpl | 6 ++---
>> 6 files changed, 106 insertions(+), 23 deletions(-)
>>
>> diff --git a/src/wok/API.json b/src/wok/API.json
>> index 8965db9..3f7bfd7 100644
>> --- a/src/wok/API.json
>> +++ b/src/wok/API.json
>> @@ -2,5 +2,28 @@
>> "$schema": "http://json-schema.org/draft-03/schema#",
>> "title": "Wok API",
>> "description": "Json schema for Wok API",
>> - "type": "object"
>> + "type": "object",
>> + "properties": {
>> + "wokroot_login": {
>> + "type": "object",
>> + "properties": {
>> + "username": {
>> + "description": "Username",
>> + "required": true,
>> + "type": "string",
>> + "minLength": 1,
>> + "error": "WOKAUTH0003E"
>> + },
>> + "password": {
>> + "description": "Password",
>> + "required": true,
>> + "type": "string",
>> + "minLength": 1,
>> + "error": "WOKAUTH0006E"
>> + }
>> + },
>> + "additionalProperties": false,
>> + "error": "WOKAUTH0007E"
>> + }
>> + }
>> }
>> diff --git a/src/wok/i18n.py b/src/wok/i18n.py
>> index 935c9c1..5ad5e57 100644
>> --- a/src/wok/i18n.py
>> +++ b/src/wok/i18n.py
>> @@ -40,8 +40,11 @@ messages = {
>>
>> "WOKAUTH0001E": _("Authentication failed for user
>> '%(username)s'. [Error code: %(code)s]"),
>> "WOKAUTH0002E": _("You are not authorized to access Wok.
>> Please, login first."),
>> - "WOKAUTH0003E": _("Specify %(item)s to login into Wok."),
>> + "WOKAUTH0003E": _("Specify username to login into Wok."),
>> + "WOKAUTH0004E": _("You have failed to login in too much
>> attempts. Please, wait for %(seconds)s seconds to try again."),
>> "WOKAUTH0005E": _("Invalid LDAP configuration: %(item)s :
>> %(value)s"),
>> + "WOKAUTH0006E": _("Specify password to login into Wok."),
>> + "WOKAUTH0007E": _("You need to specify username and password to
>> login into Wok."),
>>
>> "WOKLOG0001E": _("Invalid filter parameter. Filter parameters
>> allowed: %(filters)s"),
>> "WOKLOG0002E": _("Creation of log file failed: %(err)s"),
>> diff --git a/src/wok/root.py b/src/wok/root.py
>> index 080b7f0..9f6b7b3 100644
>> --- a/src/wok/root.py
>> +++ b/src/wok/root.py
>> @@ -1,7 +1,7 @@
>> #
>> # Project Wok
>> #
>> -# Copyright IBM Corp, 2015-2016
>> +# Copyright IBM Corp, 2015-2017
>> #
>> # Code derived from Project Kimchi
>> #
>> @@ -21,7 +21,9 @@
>>
>> import cherrypy
>> import json
>> +import re
>> import os
>> +import time
>> from distutils.version import LooseVersion
>>
>> from wok import auth
>> @@ -30,8 +32,8 @@ from wok.i18n import messages
>> from wok.config import paths as wok_paths
>> from wok.control import sub_nodes
>> from wok.control.base import Resource
>> -from wok.control.utils import parse_request
>> -from wok.exception import MissingParameter
>> +from wok.control.utils import parse_request, validate_params
>> +from wok.exception import UnauthorizedError, WokException
>> from wok.reqlogger import log_request
>>
>>
>> @@ -48,7 +50,8 @@ class Root(Resource):
>> super(Root, self).__init__(model)
>> self._handled_error = ['error_page.400', 'error_page.404',
>> 'error_page.405', 'error_page.406',
>> - 'error_page.415', 'error_page.500']
>> + 'error_page.415', 'error_page.500',
>> + 'error_page.403', 'error_page.401']
>>
>> if not dev_env:
>> self._cp_config = dict([(key,
>> self.error_production_handler)
>> @@ -146,6 +149,7 @@ class WokRoot(Root):
>> self.domain = 'wok'
>> self.messages = messages
>> self.extends = None
>> + self.failed_logins = {}
>>
>> # set user log messages and make sure all parameters are
>> present
>> self.log_map = ROOT_REQUESTS
>> @@ -153,24 +157,71 @@ class WokRoot(Root):
>>
>> @cherrypy.expose
>> def login(self, *args):
>> + def _raise_timeout(user_id):
>> + length = self.failed_logins[user_ip_sid]["count"]
>> + timeout = (length - 3) * 30
>> + details = e = UnauthorizedError("WOKAUTH0004E",
>> + {"seconds": timeout})
>> + log_request(code, params, details, method, 403)
>> + raise cherrypy.HTTPError(403, e.message)
>> details = None
>> method = 'POST'
>> code = self.getRequestMessage(method, 'login')
>>
>> try:
>> params = parse_request()
>> + validate_params(params, self, "login")
>> username = params['username']
>> password = params['password']
>> - except KeyError, item:
>> - details = e = MissingParameter('WOKAUTH0003E', {'item':
>> str(item)})
>> - log_request(code, params, details, method, 400)
>> - raise cherrypy.HTTPError(400, e.message)
>> + except WokException, e:
>> + details = e
>> + status = e.getHttpStatusCode()
>> + raise cherrypy.HTTPError(status, e.message)
>> +
>> + # get authentication info
>> + remote_ip = cherrypy.request.remote.ip
>> + session_id = str(cherrypy.session.originalid)
>> + user_ip_sid = re.escape(username + remote_ip + session_id)
>> +
>> + # check for repetly
>> + count = self.failed_logins.get(user_ip_sid, {"count":
>> 0}).get("count")
>> + if count > 3:
>> +
>> + # verify if timeout is still valid
>> + last_try = self.failed_logins[user_ip_sid]["time"]
>> + if time.time() < (last_try + ((count - 3) * 30)):
>> + _raise_timeout(user_ip_sid)
>> + else:
>> + self.failed_logins.pop(user_ip_sid)
>>
>> try:
>> status = 200
>> user_info = auth.login(username, password)
>> +
>> + # user logged sucessfuly: reset counters
>> + if self.failed_logins.get(user_ip_sid) != None:
>> + self.failed_logins.pop(user_ip_sid)
>> except cherrypy.HTTPError, e:
>> - status = e.status
>> +
>> + # store time and prevent too much tries
>> + if self.failed_logins.get(user_ip_sid) == None:
>> + self.failed_logins[user_ip_sid] = {"time": time.time(),
>> + "ip": remote_ip,
>> + "session_id": session_id,
>> + "username":
>> username,
>> + "count": 1}
>> + else:
>> + # tries take more than 30 seconds between each one:
>> do not
>> + # increase count
>> + if (time.time() -
>> + self.failed_logins[user_ip_sid]["time"]) < 30:
>> +
>> + self.failed_logins[user_ip_sid]["time"] =
>> time.time()
>> + self.failed_logins[user_ip_sid]["count"] += 1
>> +
>> + # more than 3 fails: raise error
>> + if self.failed_logins[user_ip_sid]["count"] > 3:
>> + _raise_timeout(user_ip_sid)
>> raise
>> finally:
>> log_request(code, params, details, method, status)
>> diff --git a/ui/js/src/wok.login.js b/ui/js/src/wok.login.js
>> index 666a339..9e2a392 100644
>> --- a/ui/js/src/wok.login.js
>> +++ b/ui/js/src/wok.login.js
>> @@ -19,6 +19,10 @@
>> */
>> wok.login_main = function() {
>> "use strict";
>> + var i18n;
>> + wok.getI18n(function(i18nObj){
>> + i18n = i18nObj;
>> + }, false, "i18n.json", true);
>>
>> // verify if language is available
>> var selectedLanguage = wok.lang.get();
>> @@ -50,7 +54,8 @@ wok.login_main = function() {
>> var query = window.location.search;
>> var error = /.*error=(.*?)(&|$)/g.exec(query);
>> if (error && error[1] === "sessionTimeout") {
>> - $("#messSession").show();
>> + $("#errorArea").html(i18n["WOKAUT0001E"]);
>> + $("#errorArea").show();
>> }
>>
>> var userNameBox = $('#username');
>> @@ -82,13 +87,13 @@ wok.login_main = function() {
>> window.location.replace(window.location.pathname.replace(/\/+login.html/,
>> '') + next_url);
>> }, function(jqXHR, textStatus, errorThrown) {
>> if (jqXHR.responseText == "") {
>> - $("#messUserPass").hide();
>> - $("#missServer").show();
>> - } else {
>> - $("#missServer").hide();
>> - $("#messUserPass").show();
>> + $("#errorArea").html(i18n["WOKAUT0002E"]);
>> + $("#errorArea").show();
>> + } else if ((jqXHR.responseJSON != undefined) &&
>> + ! (jqXHR.responseJSON["reason"] == undefined)) {
>> + $("#errorArea").html(jqXHR.responseJSON["reason"]);
>> + $("#errorArea").show();
>> }
>> - $("#messSession").hide();
>> $("#logging").hide();
>> $("#login").show();
>> });
>> diff --git a/ui/pages/i18n.json.tmpl b/ui/pages/i18n.json.tmpl
>> index ba29532..4329ad0 100644
>> --- a/ui/pages/i18n.json.tmpl
>> +++ b/ui/pages/i18n.json.tmpl
>> @@ -1,7 +1,7 @@
>> #*
>> * Project Wok
>> *
>> - * Copyright IBM Corp, 2014-2016
>> + * Copyright IBM Corp, 2014-2017
>> *
>> * Code derived from Project Kimchi
>> *
>> @@ -39,6 +39,9 @@
>>
>> "WOKHOST6001M": "$_("Max:")",
>>
>> + "WOKAUT0001E": "$_("Session timeout, please re-login.")",
>> + "WOKAUT0002E": "$_("Server unreachable")",
>> +
>> "WOKSETT0001M": "$_("Application")",
>> "WOKSETT0002M": "$_("User")",
>> "WOKSETT0003M": "$_("Request")",
>> diff --git a/ui/pages/login.html.tmpl b/ui/pages/login.html.tmpl
>> index f5a4b2d..6f967cf 100644
>> --- a/ui/pages/login.html.tmpl
>> +++ b/ui/pages/login.html.tmpl
>> @@ -1,7 +1,7 @@
>> #*
>> * Project Wok
>> *
>> - * Copyright IBM Corp, 2014-2016
>> + * Copyright IBM Corp, 2014-2017
>> *
>> * Code derived from Project Kimchi
>> *
>> @@ -104,9 +104,7 @@
>> <div class="container">
>> <div id="login-window" class="login-area row">
>> <div class="err-area">
>> - <div id="messUserPass" class="alert
>> alert-danger" style="display: none;">$_("The username or password you
>> entered is incorrect. Please try again.")</div>
>> - <div id="messSession" class="alert alert-danger"
>> style="display: none;">$_("Session timeout, please re-login.")</div>
>> - <div id="missServer" class="alert alert-danger"
>> style="display: none;">$_("Server unreachable.")</div>
>> + <div id="errorArea" class="alert alert-danger"
>> style="display: none;"></div>
>> </div>
>> <form id="form-login" class="form-horizontal"
>> method="post">
>> <div class="form-group">
>
> _______________________________________________
> Kimchi-devel mailing list
> Kimchi-devel at ovirt.org
> http://lists.ovirt.org/mailman/listinfo/kimchi-devel
>
More information about the Kimchi-devel
mailing list