Hi I have a patch for bug #6337, in this patch you have the possibility
to set in the configuration file the value MAX_LOGIN_ATTEMPTS which sets
the number of failed login attempts that are allowed. If this value is
exceeded the account is locked and can be reset by an administrator. By
setting the variable to the value zero this feature is deactivated this
is necessary if the account of the administrator was locked.
Comment:
Unfortunately the test cases fail because there seems to be a bug with
the migration, but unfortunately I was not able to locate this bug.
Unfortunately, in my opinion, the documentation does not sufficiently
explain how to correctly create the migrations.
I would be very happy if you could expand the documentation in the
future what this concerns and create a detailed guide to create a
migration. (This also concerns the instructions for the integration test)
With kind regards,
Florian Sabonchi
>From beb82a64075e7c52904669178804ae0835d0e924 Mon Sep 17 00:00:00 2001
From: Florian Sabonchi <sabon...@posteo.de>
Date: Sun, 11 Jul 2021 00:47:13 +0200
Subject: [PATCH 1/2] first draft for bruteforce_protection
---
web/migrations/versions/6650c52670c2_.py | 33 ++++++++++++++++
web/pgadmin/authenticate/__init__.py | 39 ++++++++++++++++---
web/pgadmin/model/__init__.py | 4 +-
web/pgadmin/tools/user_management/__init__.py | 16 ++++++--
.../static/js/user_management.js | 13 +++++++
5 files changed, 94 insertions(+), 11 deletions(-)
create mode 100644 web/migrations/versions/6650c52670c2_.py
diff --git a/web/migrations/versions/6650c52670c2_.py b/web/migrations/versions/6650c52670c2_.py
new file mode 100644
index 000000000..90cc17d36
--- /dev/null
+++ b/web/migrations/versions/6650c52670c2_.py
@@ -0,0 +1,33 @@
+
+"""empty message
+
+Revision ID: 6650c52670c2
+Revises: c465fee44968
+Create Date: 2021-07-10 18:12:38.821602
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+from pgadmin import db
+
+revision = '6650c52670c2'
+down_revision = 'c465fee44968'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ db.engine.execute(
+ 'ALTER TABLE user ADD COLUMN locked BOOLEAN DEFAULT FALSE'
+ )
+ db.engine.execute(
+ 'ALTER TABLE user ADD COLUMN login_attempts int DEFAULT 0'
+ )
+
+
+def downgrade():
+ # pgAdmin only upgrades, downgrade not implemented.
+ pass
diff --git a/web/pgadmin/authenticate/__init__.py b/web/pgadmin/authenticate/__init__.py
index db42d230a..5a21d3d9b 100644
--- a/web/pgadmin/authenticate/__init__.py
+++ b/web/pgadmin/authenticate/__init__.py
@@ -12,18 +12,18 @@
import config
import copy
-from flask import current_app, flash, Response, request, url_for,\
+from flask import current_app, flash, Response, request, url_for, \
session, redirect
from flask_babelex import gettext
from flask_security.views import _security
from flask_security.utils import get_post_logout_redirect, \
- get_post_login_redirect
+ get_post_login_redirect, logout_user
+from pgadmin import db, User
from pgadmin.utils import PgAdminModule
from pgadmin.utils.constants import KERBEROS, INTERNAL, OAUTH2, LDAP
from pgadmin.authenticate.registry import AuthSourceRegistry
-
MODULE_NAME = 'authenticate'
auth_obj = None
@@ -46,14 +46,29 @@ def login():
auth_obj = AuthSourceManager(form, copy.deepcopy(
config.AUTHENTICATION_SOURCES))
- if OAUTH2 in config.AUTHENTICATION_SOURCES\
- and 'oauth2_button' in request.form:
+ if OAUTH2 in config.AUTHENTICATION_SOURCES \
+ and 'oauth2_button' in request.form:
session['auth_obj'] = auth_obj
session['auth_source_manager'] = None
+
+ username = form.data['email']
+ user = User.query.filter_by(username=username).first()
+
+ if user:
+ if user.login_attempts >= config.MAX_LOGIN_ATTEMPTS > 0:
+ user.locked = True
+ else:
+ user.locked = False
+ db.session.commit()
+
# Validate the user
if not auth_obj.validate():
for field in form.errors:
+ if user:
+ if config.MAX_LOGIN_ATTEMPTS > 0:
+ user.login_attempts += 1
+ db.session.commit()
for error in form.errors[field]:
flash(error, 'warning')
return redirect(get_post_logout_redirect())
@@ -65,15 +80,27 @@ def login():
status, msg = auth_obj.login()
current_auth_obj = auth_obj.as_dict()
+ if status:
+ if user.login_attempts >= config.MAX_LOGIN_ATTEMPTS > 0:
+ flash(gettext('Account locked'),
+ 'warning')
+ logout_user()
+ return redirect(get_post_logout_redirect())
+
if not status:
- if current_auth_obj['current_source'] ==\
+ if current_auth_obj['current_source'] == \
KERBEROS:
return redirect('{0}?next={1}'.format(url_for(
'authenticate.kerberos_login'), url_for('browser.index')))
flash(msg, 'danger')
return redirect(get_post_logout_redirect())
+
session['auth_source_manager'] = current_auth_obj
+
+ user.login_attempts = 0
+ db.session.commit()
+
if 'auth_obj' in session:
session.pop('auth_obj')
return redirect(get_post_login_redirect())
diff --git a/web/pgadmin/model/__init__.py b/web/pgadmin/model/__init__.py
index 3afef96eb..aea32c1e1 100644
--- a/web/pgadmin/model/__init__.py
+++ b/web/pgadmin/model/__init__.py
@@ -30,7 +30,7 @@ import uuid
#
##########################################################################
-SCHEMA_VERSION = 30
+SCHEMA_VERSION = 31
##########################################################################
#
@@ -80,6 +80,8 @@ class User(db.Model, UserMixin):
# fs_uniquifier is required by flask-security-too >= 4.
fs_uniquifier = db.Column(db.String(255), unique=True, nullable=False,
default=(lambda _: uuid.uuid4().hex))
+ login_attempts = db.Column(db.Integer, default=0)
+ locked = db.Column(db.Boolean(), default=False)
class Setting(db.Model):
diff --git a/web/pgadmin/tools/user_management/__init__.py b/web/pgadmin/tools/user_management/__init__.py
index 3aabd7749..9ee3e26ae 100644
--- a/web/pgadmin/tools/user_management/__init__.py
+++ b/web/pgadmin/tools/user_management/__init__.py
@@ -129,6 +129,10 @@ def validate_user(data):
if 'auth_source' in data and data['auth_source'] != "":
new_data['auth_source'] = data['auth_source']
+ if 'locked' in data and not data['locked']:
+ new_data['locked'] = data['locked']
+ new_data['login_attempts'] = 0
+
return new_data
@@ -207,7 +211,8 @@ def user(uid):
'email': u.email,
'active': u.active,
'role': u.roles[0].id,
- 'auth_source': u.auth_source
+ 'auth_source': u.auth_source,
+ 'locked': u.locked
}
else:
users = User.query.all()
@@ -219,7 +224,8 @@ def user(uid):
'email': u.email,
'active': u.active,
'role': u.roles[0].id,
- 'auth_source': u.auth_source
+ 'auth_source': u.auth_source,
+ 'locked': u.locked
})
res = users_data
@@ -316,7 +322,8 @@ def create_user(data):
'username': usr.username,
'email': usr.email,
'active': usr.active,
- 'role': usr.roles[0].id
+ 'role': usr.roles[0].id,
+ 'locked': usr.locked
}
@@ -599,7 +606,8 @@ def update(uid):
'email': usr.email,
'active': usr.active,
'role': usr.roles[0].id,
- 'auth_source': usr.auth_source
+ 'auth_source': usr.auth_source,
+ 'locked': usr.locked
}
return ajax_response(
diff --git a/web/pgadmin/tools/user_management/static/js/user_management.js b/web/pgadmin/tools/user_management/static/js/user_management.js
index ffed1d44d..6fac754d2 100644
--- a/web/pgadmin/tools/user_management/static/js/user_management.js
+++ b/web/pgadmin/tools/user_management/static/js/user_management.js
@@ -436,6 +436,19 @@ define([
editable: function(m) {
return (m.get('auth_source') == DEFAULT_AUTH_SOURCE);
},
+ },{
+ id: 'locked',
+ label: gettext('Locked'),
+ type: 'switch',
+ cell: 'switch',
+ disabled: false,
+ sortable: false,
+ editable: function (m){
+ if (!m.get('locked')) {
+ return false;
+ }
+ return (m.get('id') != userInfo['id']);
+ },
}],
validate: function() {
var errmsg = null,
--
2.25.1
>From c01dea5759e0f18bb83517848c683e20a105b7c9 Mon Sep 17 00:00:00 2001
From: Florian Sabonchi <sabon...@posteo.de>
Date: Mon, 12 Jul 2021 17:53:21 +0200
Subject: [PATCH 2/2] updated documentation
---
docs/en_US/login.rst | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/docs/en_US/login.rst b/docs/en_US/login.rst
index d2b949910..d5c9dec20 100644
--- a/docs/en_US/login.rst
+++ b/docs/en_US/login.rst
@@ -56,3 +56,10 @@ Please note that your LDAP password cannot be recovered using this dialog. If
you enter your LDAP username in the *Email Address/Username* field, and then
enter your email to recover your password, an error message will be displayed
asking you to contact the LDAP administrator to recover your LDAP password.
+
+Avoiding a bruteforce attack
+**************************
+
+You have the possibility to lock an account by setting ``MAX_LOGIN_ATTEMPTS``
+once it has reached the maximum number of login attempts.
+You can disable this feature by setting the value to zero.
\ No newline at end of file
--
2.25.1