Ensure that the login account should be locked after N number of attempts. N is configurable using the 'MAX_LOGIN_ATTEMPTS' parameter. Fixes #6337

This commit is contained in:
Florian Sabonchi 2021-07-22 12:24:43 +05:30 committed by Akshay Joshi
parent c2db647379
commit a3d3c74e67
8 changed files with 113 additions and 14 deletions

View File

@ -10,16 +10,16 @@ Use the *Login* dialog to log in to pgAdmin:
:alt: pgAdmin login dialog
:align: center
Use the fields in the *Login* dialog to authenticate your connection. There are
Use the fields in the *Login* dialog to authenticate your connection. There are
two ways to authenticate your connection:
- From pgAdmin version 4.21 onwards, support for LDAP authentication
has been added. If LDAP authentication has been enabled for your pgAdmin
application, you can use your LDAP credentials to log in to pgAdmin:
* Provide the LDAP username in the *Email Address/Username* field.
* Provide the LDAP username in the *Email Address/Username* field.
* Provide your LDAP password in the Password field.
* Provide your LDAP password in the Password field.
- Alternatively, you can use the following information to log in to pgAdmin:
@ -53,6 +53,13 @@ If you have forgotten the email associated with your account, please contact
your administrator.
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
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.

View File

@ -17,6 +17,7 @@ Housekeeping
Bug fixes
*********
| `Issue #6337 <https://redmine.postgresql.org/issues/6337>`_ - Ensure that the login account should be locked after N number of attempts. N is configurable using the 'MAX_LOGIN_ATTEMPTS' parameter.
| `Issue #6369 <https://redmine.postgresql.org/issues/6369>`_ - Fixed CSRF errors for stale sessions by increasing the session expiration time for desktop mode.
| `Issue #6448 <https://redmine.postgresql.org/issues/6448>`_ - Fixed an issue in the search object when searching in 'all types' or 'subscription' if the user doesn't have access to the subscription.
| `Issue #6580 <https://redmine.postgresql.org/issues/6580>`_ - Fixed TypeError 'NoneType' object is not sub scriptable.

View File

@ -574,6 +574,14 @@ ENHANCED_COOKIE_PROTECTION = True
AUTHENTICATION_SOURCES = ['internal']
##########################################################################
# 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.
##########################################################################
MAX_LOGIN_ATTEMPTS = 3
##########################################################################
# LDAP Configuration
##########################################################################

View File

@ -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

View File

@ -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,36 @@ def login():
auth_obj = AuthSourceManager(form, copy.deepcopy(
config.AUTHENTICATION_SOURCES))
if OAUTH2 in config.AUTHENTICATION_SOURCES\
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()
if user.login_attempts >= config.MAX_LOGIN_ATTEMPTS > 0:
flash(gettext('Your account is locked. Please contact the '
'Administrator.'),
'warning')
logout_user()
return redirect(get_post_logout_redirect())
# 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())
@ -66,14 +88,19 @@ def login():
current_auth_obj = auth_obj.as_dict()
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())

View File

@ -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):

View File

@ -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(

View File

@ -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,