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 .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
7 changes: 4 additions & 3 deletions docs/ChangeLog.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
### 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:

* Supports Two-Factor Authentication on the backend [GH-169]

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:

Expand Down
4 changes: 4 additions & 0 deletions ops/ansible/roles/postmaster_deploy/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions ops/ansible/roles/postmaster_upgrade/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion postmaster/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down
4 changes: 2 additions & 2 deletions postmaster/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
Purpose: form definitions for the app
"""

from flask_wtf import Form
from flask_wtf import FlaskForm
Copy link
Member

Choose a reason for hiding this comment

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

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()])
Expand Down
10 changes: 2 additions & 8 deletions postmaster/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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):
Expand Down
5 changes: 0 additions & 5 deletions requirements.in
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
coverage
Flask
Flask-Bcrypt
Flask-Login
Expand All @@ -7,11 +6,7 @@ Flask-Script
Flask-SQLAlchemy
Flask-WTF
ldap3
mock
onetimepass
passlib
pylint
pymysql
pyqrcode
pytest
pytest-cov
65 changes: 25 additions & 40 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 5 additions & 0 deletions test-requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
coverage
mock
pylint
pytest
pytest-cov
118 changes: 47 additions & 71 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down