Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Features:
* Added the ability to install PostMaster via a deb package [GH-111]
* Adds the ability to lockout local accounts after x number of failed login attempts [GH-142]
* Database upgrades/migrations are automatic during ugrades via the deb package and Docker [GH-138]
* Adds the ability to unlock administrators and reset administrator passwords via the CLI [GH-145]

Improvements:

Expand Down
4 changes: 4 additions & 0 deletions docs/Configuration/CommandLineConfiguration.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ Use the following commands to restore the proper permissions on the PostMaster f
**generatekey** replaces the secret key in config.py which is used by Flask (the Python framework used for PostMaster) for cryptographic functions.
After the initial installation, this command should not be run again as all current logins would become invalid upon the next restart of the PostMaster.

**unlockadmin username** unlocks a locked out administrator (replace username with the actual value).

**resetadminpassword username password** resets an administrator's password to the desired value (replace user and password with the actual values)

**runserver -d -h 0.0.0.0** runs PostMaster in debug mode on port 5000. This is useful if you are having issues as it bypasses the webserver
and displays failing errors in HTML.

Expand Down
15 changes: 14 additions & 1 deletion manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from re import sub
from flask_script import Manager
from postmaster import app, db, models
from postmaster.utils import add_default_configuration_settings
from postmaster.utils import add_default_configuration_settings, clear_lockout_fields_on_user, reset_admin_password

manager = Manager(app)
if os.environ.get('POSTMASTER_DEV'):
Expand Down Expand Up @@ -98,5 +98,18 @@ def setlogfile(filepath):
else:
print(line.rstrip())


@manager.command
def unlockadmin(username):
"""Unlocks a locked out administrator"""
clear_lockout_fields_on_user(username)


@manager.command
def resetadminpassword(username, new_password):
"""Resets an administrator's password with one supplied"""
reset_admin_password(username, new_password)


if __name__ == "__main__":
manager.run()
17 changes: 4 additions & 13 deletions postmaster/apiv1/admins.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from flask import request
from flask_login import login_required, current_user
from postmaster import db, bcrypt
from postmaster import db
from postmaster.models import Admins, Configs
from postmaster.utils import json_logger, clear_lockout_fields_on_user
from ..decorators import json_wrap, paginate
Expand Down Expand Up @@ -108,30 +108,21 @@ def update_admin(admin_id):
auditMessage = 'The administrator "{0}" had their username changed to "{1}"'.format(
admin.username, newUsername)
admin.username = newUsername
db.session.add(admin)
else:
ValidationError('The username supplied already exists')
elif 'password' in json:
minPwdLength = int(Configs.query.filter_by(
setting='Minimum Password Length').first().value)
if len(json['password']) < minPwdLength:
raise ValidationError(
'The password must be at least {0} characters long'.format(
minPwdLength))
admin.set_password(json['password'])
auditMessage = 'The administrator "{0}" had their password changed'.format(
admin.username)
admin.password = bcrypt.generate_password_hash(json['password'])
db.session.add(admin)
elif 'name' in json:
auditMessage = 'The administrator "{0}" had their name changed to "{1}"'.format(
admin.username, admin.name)
auditMessage = 'The administrator "{0}" had their name changed to "{1}"'.format(admin.username, admin.name)
admin.name = json['name']
db.session.add(admin)
else:
raise ValidationError(
'The username, password, or name was not supplied in the request')

try:
db.session.add(admin)
db.session.commit()
json_logger('audit', current_user.username, auditMessage)
except ValidationError as e:
Expand Down
11 changes: 11 additions & 0 deletions postmaster/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,17 @@ def increment_failed_login(self, account_lockout_threshold, reset_account_lockou

self.last_failed_date = now

def set_password(self, new_password):
""" Sets the password for an admin.
"""
if not self.id:
raise ValidationError('An admin is not associated with the object')

min_pwd_length = int(Configs.query.filter_by(setting='Minimum Password Length').first().value)
if len(new_password) < min_pwd_length:
raise ValidationError('The password must be at least {0} characters long'.format(min_pwd_length))

self.password = bcrypt.generate_password_hash(new_password)


class Configs(db.Model):
Expand Down
26 changes: 26 additions & 0 deletions postmaster/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,32 @@ def clear_lockout_fields_on_user(username):
db.session.close()


def reset_admin_password(username, new_password):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function should be on the model 💨

""" Resets an admin's password with one supplied
"""
admin = models.Admins.query.filter_by(username=username).first()

if not admin:
raise ValidationError('The admin does not exist in the database.')

admin.set_password(new_password)

try:
db.session.add(admin)
db.session.commit()
json_logger('audit', 'CLI', ('The administrator "{0}" had their password changed via the CLI'.format(username)))
except ValidationError as e:
raise e
except Exception as e:
db.session.rollback()
json_logger(
'error', username,
'The following error occurred when try to reset an admin\'s password: {0}'.format(str(e)))
ValidationError('A database error occurred. Please try again.', 'error')
finally:
db.session.close()


def add_ldap_user_to_db(username, display_name):
""" Adds an LDAP user stub in the Admins table of the database for flask_login
"""
Expand Down
10 changes: 9 additions & 1 deletion tests/utils/test_utils_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from mockldap import MockLdap
from mock import patch
from datetime import datetime, timedelta
from postmaster import app
from postmaster import app, bcrypt
from postmaster.utils import *
from postmaster.apiv1.utils import *

Expand Down Expand Up @@ -197,6 +197,14 @@ def test_clear_lockout_fields_on_user(self):
assert new_test_admin.unlock_date is None
assert new_test_admin.last_failed_date is None

def test_reset_admin_password(self):
test_admin = generate_test_admin()
db.session.add(test_admin)
db.session.commit()
reset_admin_password('test_admin', 'SomeNewPassword')
new_test_admin = models.Admins.query.filter_by(username='test_admin').first()
assert bcrypt.check_password_hash(new_test_admin.password, 'SomeNewPassword') is True

def test_get_wtforms_errors(self):
""" Tests the get_wtforms_errors function by posting to /login with missing parameters.
This also tests the new_line_to_break Jinja2 filter. The expected return value is an
Expand Down