diff --git a/.travis.yml b/.travis.yml index 855d402..205382f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ python: install: - cp config.default.py config.py - pip install -r requirements.txt + - pip install -r test-requirements.txt before_script: - ./pylint-check.py script: diff --git a/docs/ChangeLog.md b/docs/ChangeLog.md index d193533..4ab0583 100644 --- a/docs/ChangeLog.md +++ b/docs/ChangeLog.md @@ -1,11 +1,11 @@ -### Unreleased +### v1.2.0 - Rubber Soul BACKWARDS INCOMPATIBILITIES / NOTES: * The default MySQL Python connector of "mysqlclient" has been replaced with "pymysql" for a pure Python replacement. Upgrades will be unaffected by this, but please note that reinstalls will require you to either install "mysqlclient" ([Ubuntu Instructions](https://github.com/PyMySQL/mysqlclient-python#install) or change the start of your database URI -with "mysql+pymysql://" instead of "mysql://" [GH-170]. +with "mysql+pymysql://" instead of "mysql://" [GH-170] Features: @@ -13,8 +13,9 @@ Features: Improvements: -* Replaced GIF spinner with a pure CSS spinner using CSS3 animations [GH-177]. +* Replaced GIF spinner with a pure CSS spinner using CSS3 animations [GH-177] * Adds Python 3.5 support [GH-179] [GH-180] +* Update Python dependencies when updating PostMaster using the deb package (apt-get) [GH-185] Bug Fixes: diff --git a/ops/ansible/roles/postmaster_deploy/tasks/main.yml b/ops/ansible/roles/postmaster_deploy/tasks/main.yml index feb61c3..494184f 100644 --- a/ops/ansible/roles/postmaster_deploy/tasks/main.yml +++ b/ops/ansible/roles/postmaster_deploy/tasks/main.yml @@ -31,6 +31,10 @@ - name: Install python dependencies pip: requirements=/opt/postmaster/git/requirements.txt virtualenv=/opt/postmaster/env +- name: Install python test dependencies + pip: requirements=/opt/postmaster/git/test-requirements.txt virtualenv=/opt/postmaster/env + when: provision_type == "dev" + - name: Checking if config.py exists stat: path=/opt/postmaster/git/config.py register: config_file diff --git a/ops/ansible/roles/postmaster_upgrade/tasks/main.yml b/ops/ansible/roles/postmaster_upgrade/tasks/main.yml index d333aac..ddcfaee 100644 --- a/ops/ansible/roles/postmaster_upgrade/tasks/main.yml +++ b/ops/ansible/roles/postmaster_upgrade/tasks/main.yml @@ -5,6 +5,9 @@ args: chdir: /opt/postmaster/git +- name: Upgrade python dependencies + pip: requirements=/opt/postmaster/git/requirements.txt virtualenv=/opt/postmaster/env + - name: Ensure apache is reloaded become: yes service: name=apache2 state=reloaded diff --git a/postmaster/__init__.py b/postmaster/__init__.py index 5551f57..640df22 100644 --- a/postmaster/__init__.py +++ b/postmaster/__init__.py @@ -10,7 +10,7 @@ from flask_login import LoginManager from flask_bcrypt import Bcrypt -__version__ = 'v1.1.0.0' +__version__ = 'v1.2.0.0' app = Flask(__name__) if environ.get('POSTMASTER_DEV') == 'TRUE': diff --git a/postmaster/forms.py b/postmaster/forms.py index b61b7e0..4aac96b 100644 --- a/postmaster/forms.py +++ b/postmaster/forms.py @@ -4,14 +4,14 @@ Purpose: form definitions for the app """ -from flask_wtf import Form +from flask_wtf import FlaskForm from wtforms import StringField, PasswordField, SelectField, IntegerField from wtforms.validators import DataRequired, Optional from postmaster import models from postmaster.utils import validate_wtforms_password -class LoginForm(Form): +class LoginForm(FlaskForm): """ Class for login form on /login """ username = StringField(label='Username', validators=[DataRequired()]) diff --git a/postmaster/models.py b/postmaster/models.py index ac50ce3..47f14ff 100644 --- a/postmaster/models.py +++ b/postmaster/models.py @@ -9,8 +9,7 @@ from re import search, match from os import urandom import base64 -from passlib.hash import sha512_crypt as sha512 # pylint: disable=no-name-in-module -from hashlib import sha1 +import passlib.hash import onetimepass @@ -119,12 +118,7 @@ def from_json(self, json): @staticmethod def encrypt_password(password): - salt = (sha1(urandom(16)).hexdigest())[:16] - protectedPassword = sha512.encrypt(password, - rounds=5000, - salt=salt, - implicit_rounds=True) - return protectedPassword + return passlib.hash.sha512_crypt.hash(password, rounds=5000) class VirtualAliases(db.Model): diff --git a/requirements.in b/requirements.in index 8c17536..68f3b99 100644 --- a/requirements.in +++ b/requirements.in @@ -1,4 +1,3 @@ -coverage Flask Flask-Bcrypt Flask-Login @@ -7,11 +6,7 @@ Flask-Script Flask-SQLAlchemy Flask-WTF ldap3 -mock onetimepass passlib -pylint pymysql pyqrcode -pytest -pytest-cov diff --git a/requirements.txt b/requirements.txt index 34a8582..cd534b2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,45 +4,30 @@ # # pip-compile --output-file requirements.txt requirements.in # -alembic==0.8.7 # via flask-migrate -astroid==1.4.8 # via pylint -backports.functools-lru-cache==1.2.1 # via pylint -bcrypt==3.1.0 # via flask-bcrypt -cffi==1.7.0 # via bcrypt -click==6.6 # via flask -configparser==3.5.0 # via pylint -coverage==4.2 -Flask-Bcrypt==0.7.1 -Flask-Login==0.3.2 -Flask-Migrate==2.0.0 -Flask-Script==2.0.5 # via flask-migrate -Flask-SQLAlchemy==2.1 # via flask-migrate -Flask-WTF==0.12 -Flask==0.11.1 # via flask-bcrypt, flask-login, flask-migrate, flask-script, flask-sqlalchemy, flask-wtf -funcsigs==1.0.2 # via mock -isort==4.2.5 # via pylint +alembic==0.9.1 # via flask-migrate +bcrypt==3.1.3 # via flask-bcrypt +cffi==1.9.1 # via bcrypt +click==6.7 # via flask +flask-bcrypt==0.7.1 +flask-login==0.4.0 +flask-migrate==2.0.3 +flask-script==2.0.5 +flask-sqlalchemy==2.2 +flask-wtf==0.14.2 +flask==0.12 itsdangerous==0.24 # via flask -Jinja2==2.8 # via flask -lazy-object-proxy==1.2.2 # via astroid -ldap3==2.1.0 -Mako==1.0.4 # via alembic -MarkupSafe==0.23 # via jinja2, mako -mccabe==0.5.2 # via pylint -mock==2.0.0 +jinja2==2.9.5 # via flask +ldap3==2.2.1 +mako==1.0.6 # via alembic +markupsafe==1.0 # via jinja2, mako onetimepass==1.0.1 -passlib==1.6.5 -pbr==1.10.0 # via mock -py==1.4.31 # via pytest -pyasn1==0.1.9 # via ldap3 -pycparser==2.14 # via cffi -pylint==1.6.4 -pymysql==0.7.9 -PyQRCode==1.2.1 -pytest-cov==2.3.1 -pytest==2.9.2 -python-editor==1.0.1 # via alembic -six==1.10.0 # via astroid, bcrypt, mock, onetimepass, pylint -SQLAlchemy==1.0.14 # via alembic, flask-sqlalchemy -Werkzeug==0.11.10 # via flask, flask-wtf -wrapt==1.10.8 # via astroid -WTForms==2.1 # via flask-wtf +passlib==1.7.1 +pyasn1==0.2.3 # via ldap3 +pycparser==2.17 # via cffi +pymysql==0.7.10 +pyqrcode==1.2.1 +python-editor==1.0.3 # via alembic +six==1.10.0 # via bcrypt, onetimepass +sqlalchemy==1.1.6 # via alembic, flask-sqlalchemy +werkzeug==0.12 # via flask +wtforms==2.1 # via flask-wtf diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..c7f7b17 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,5 @@ +coverage +mock +pylint +pytest +pytest-cov diff --git a/tests/conftest.py b/tests/conftest.py index ea38554..a0885a6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,78 +4,54 @@ app.config.from_object('config.TestConfiguration') -def initialize(): - try: - db.session.remove() - db.drop_all() - db.create_all() - add_default_configuration_settings() - admin2 = models.Admins().from_json( - {'username': 'admin2', 'password': 'PostMaster2', 'name': 'Some Admin'}) - enable_ldap_auth = models.Configs.query.filter_by(setting='Enable LDAP Authentication').first() - enable_ldap_auth.value = 'True' - ldap_server = models.Configs.query.filter_by(setting='AD Server LDAP String').first() - ldap_server.value = 'LDAPS://postmaster.local:636' - domain = models.Configs.query.filter_by(setting='AD Domain').first() - domain.value = 'postmaster.local' - ldap_admin_group = models.Configs.query.filter_by(setting='AD PostMaster Group').first() - ldap_admin_group.value = 'PostMaster Admins' - ldap_auth_method = models.Configs.query.filter_by(setting='LDAP Authentication Method').first() - # Use SIMPLE auth because ldap3 testing only allows this authentication method - ldap_auth_method.value = 'SIMPLE' - - try: - db.session.add(admin2) - db.session.add(enable_ldap_auth) - db.session.add(ldap_server) - db.session.add(domain) - db.session.add(ldap_admin_group) - db.session.add(ldap_auth_method) - db.session.commit() - except: - return False - - domain = models.VirtualDomains().from_json({'name': 'postmaster.com'}) - domain2 = models.VirtualDomains().from_json({'name': 'postmaster.org'}) - - try: - db.session.add(domain) - db.session.add(domain2) - db.session.commit() - except: - return False - - emailUser = models.VirtualUsers().from_json({'email': 'email@postmaster.com', 'password': 'password'}) - emailUser2 = models.VirtualUsers().from_json({'email': 'email2@postmaster.com', 'password': 'password'}) - emailUser3 = models.VirtualUsers().from_json({'email': 'email@postmaster.org', 'password': 'password'}) - - try: - db.session.add(emailUser) - db.session.add(emailUser2) - db.session.add(emailUser3) - db.session.commit() - except: - return False - alias = models.VirtualAliases().from_json({'domain_id': 1, 'source': 'aliasemail@postmaster.com', 'destination': 'email@postmaster.com'}) - alias2 = models.VirtualAliases().from_json({'domain_id': 1, 'source': 'aliasemail2@postmaster.com', 'destination': 'email2@postmaster.com'}) - alias3 = models.VirtualAliases().from_json({'domain_id': 1, 'source': 'aliasemail3@postmaster.com', 'destination': 'email@postmaster.org'}) - - try: - db.session.add(alias) - db.session.add(alias2) - db.session.add(alias3) - db.session.commit() - except: - return False - - return True - - except Exception as e: - print("Unexpected error: {0}".format(e.message)) - return False - - return False +def initialize(): + db.session.remove() + db.drop_all() + db.create_all() + add_default_configuration_settings() + + admin2 = models.Admins().from_json( + {'username': 'admin2', 'password': 'PostMaster2', 'name': 'Some Admin'}) + db.session.add(admin2) + + enable_ldap_auth = models.Configs.query.filter_by(setting='Enable LDAP Authentication').first() + enable_ldap_auth.value = 'True' + ldap_server = models.Configs.query.filter_by(setting='AD Server LDAP String').first() + ldap_server.value = 'LDAPS://postmaster.local:636' + domain = models.Configs.query.filter_by(setting='AD Domain').first() + domain.value = 'postmaster.local' + ldap_admin_group = models.Configs.query.filter_by(setting='AD PostMaster Group').first() + ldap_admin_group.value = 'PostMaster Admins' + ldap_auth_method = models.Configs.query.filter_by(setting='LDAP Authentication Method').first() + # Use SIMPLE auth because ldap3 testing only allows this authentication method + ldap_auth_method.value = 'SIMPLE' + db.session.add(enable_ldap_auth) + db.session.add(ldap_server) + db.session.add(domain) + db.session.add(ldap_admin_group) + db.session.add(ldap_auth_method) + + domain = models.VirtualDomains().from_json({'name': 'postmaster.com'}) + domain2 = models.VirtualDomains().from_json({'name': 'postmaster.org'}) + db.session.add(domain) + db.session.add(domain2) + + emailUser = models.VirtualUsers().from_json({'email': 'email@postmaster.com', 'password': 'password'}) + emailUser2 = models.VirtualUsers().from_json({'email': 'email2@postmaster.com', 'password': 'password'}) + emailUser3 = models.VirtualUsers().from_json({'email': 'email@postmaster.org', 'password': 'password'}) + db.session.add(emailUser) + db.session.add(emailUser2) + db.session.add(emailUser3) + + alias = models.VirtualAliases().from_json({'domain_id': 1, 'source': 'aliasemail@postmaster.com', 'destination': 'email@postmaster.com'}) + alias2 = models.VirtualAliases().from_json({'domain_id': 1, 'source': 'aliasemail2@postmaster.com', 'destination': 'email2@postmaster.com'}) + alias3 = models.VirtualAliases().from_json({'domain_id': 1, 'source': 'aliasemail3@postmaster.com', 'destination': 'email@postmaster.org'}) + db.session.add(alias) + db.session.add(alias2) + db.session.add(alias3) + + db.session.commit() # Reinitialize the database before each test