[Kimchi-devel] [PATCH v4] [Wok] Bug fix #147: Block authentication request after too many failures

ramonn@linux.vnet.ibm.com posted 1 patch 7 years, 2 months ago
Patches applied successfully (tree, apply log)
git fetch https://github.com/patchew-project/kimchi-wok tags/patchew/20170203173449.28223-1-ramonn@linux.vnet.ibm.com
There is a newer version of this series
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(-)
[Kimchi-devel] [PATCH v4] [Wok] Bug fix #147: Block authentication request after too many failures
Posted by ramonn@linux.vnet.ibm.com 7 years, 2 months ago
From: Ramon Medeiros <ramonn@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@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">
-- 
2.10.1 (Apple Git-78)

_______________________________________________
Kimchi-devel mailing list
Kimchi-devel@ovirt.org
http://lists.ovirt.org/mailman/listinfo/kimchi-devel
Re: [Kimchi-devel] [PATCH v4] [Wok] Bug fix #147: Block authentication request after too many failures
Posted by Aline Manera 7 years, 2 months ago
Also, when I enter invalid credentials, I got the error:

Authentication failed for user '%(username)s'. [Error code: %(code)s]

I know it is correct, but it seems not a friendly message.
Maybe we could log that message (to use when debugging) and show on UI:

The username or password you entered is incorrect. Please try again.

(the same message we had before this patch)

On 02/03/2017 03:34 PM, ramonn@linux.vnet.ibm.com wrote:
> From: Ramon Medeiros <ramonn@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@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@ovirt.org
http://lists.ovirt.org/mailman/listinfo/kimchi-devel
Re: [Kimchi-devel] [PATCH v4] [Wok] Bug fix #147: Block authentication request after too many failures
Posted by Aline Manera 7 years, 2 months ago
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@linux.vnet.ibm.com wrote:
> From: Ramon Medeiros <ramonn@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@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@ovirt.org
http://lists.ovirt.org/mailman/listinfo/kimchi-devel
Re: [Kimchi-devel] [PATCH v4] [Wok] Bug fix #147: Block authentication request after too many failures
Posted by Aline Manera 7 years, 2 months ago
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@linux.vnet.ibm.com wrote:
>> From: Ramon Medeiros <ramonn@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@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@ovirt.org
> http://lists.ovirt.org/mailman/listinfo/kimchi-devel
>

_______________________________________________
Kimchi-devel mailing list
Kimchi-devel@ovirt.org
http://lists.ovirt.org/mailman/listinfo/kimchi-devel