mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2024-11-21 16:27:39 -06:00
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:
parent
c2db647379
commit
a3d3c74e67
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
##########################################################################
|
||||
|
33
web/migrations/versions/6650c52670c2_.py
Normal file
33
web/migrations/versions/6650c52670c2_.py
Normal 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
|
@ -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())
|
||||
|
@ -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):
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user