From 974f95f25ee9272b47a68e9a5f0fc73ff5da532a Mon Sep 17 00:00:00 2001 From: Dmytro Makovey Date: Thu, 24 Aug 2017 07:23:54 -0700 Subject: [PATCH 01/13] backport new bulk operations --- admin/mailu/admin/models.py | 18 +++++++++++++++--- admin/mailu/admin/views/__init__.py | 0 admin/manage.py | 21 +++++++++++++++++++-- 3 files changed, 34 insertions(+), 5 deletions(-) create mode 100644 admin/mailu/admin/views/__init__.py diff --git a/admin/mailu/admin/models.py b/admin/mailu/admin/models.py index 9e4bcd10..e8085d98 100644 --- a/admin/mailu/admin/models.py +++ b/admin/mailu/admin/models.py @@ -163,16 +163,28 @@ class User(Base, Email): def get_id(self): return self.email + scheme_dict = {'SHA512-CRYPT': "sha512_crypt", + 'SHA256-CRYPT': "sha256_crypt", + 'MD5-CRYPT': "md5_crypt", + 'CRYPT': "des_crypt"} pw_context = context.CryptContext( - ["sha512_crypt", "sha256_crypt", "md5_crypt"] + schemes = scheme_dict.values(), + default='sha512_crypt', ) def check_password(self, password): reference = re.match('({[^}]+})?(.*)', self.password).group(2) return User.pw_context.verify(password, reference) - def set_password(self, password): - self.password = '{SHA512-CRYPT}' + User.pw_context.encrypt(password) + def set_password(self, password, hash_scheme='SHA512-CRYPT', raw=False): + """Set password for user with specified encryption scheme + @password: plain text password to encrypt (if raw == True the hash itself) + """ + # for the list of hash schemes see https://wiki2.dovecot.org/Authentication/PasswordSchemes + if raw: + self.password = '{'+hash_scheme+'}' + password + else: + self.password = '{'+hash_scheme+'}' + User.pw_context.encrypt(password, self.scheme_dict[hash_scheme]) def get_managed_domains(self): if self.global_admin: diff --git a/admin/mailu/admin/views/__init__.py b/admin/mailu/admin/views/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/admin/manage.py b/admin/manage.py index a277d719..b57b2e2d 100644 --- a/admin/manage.py +++ b/admin/manage.py @@ -35,7 +35,7 @@ def admin(localpart, domain_name, password): @manager.command -def user(localpart, domain_name, password): +def user(localpart, domain_name, password, hash_scheme='SHA512-CRYPT'): """ Create an user """ domain = models.Domain.query.get(domain_name) @@ -47,7 +47,24 @@ def user(localpart, domain_name, password): domain=domain, global_admin=False ) - user.set_password(password) + user.set_password(password, hash_scheme=hash_scheme) + db.session.add(user) + db.session.commit() + +@manager.command +def user_raw(localpart, domain_name, password, hash_scheme='SHA512-CRYPT'): + """ Create an user + """ + domain = models.Domain.query.get(domain_name) + if not domain: + domain = models.Domain(name=domain_name) + db.session.add(domain) + user = models.User( + localpart=localpart, + domain=domain, + global_admin=False + ) + user.set_password(password, hash_scheme=hash_scheme) db.session.add(user) db.session.commit() From 4dc2f896a2f0052a0c4adf5af79ab2a38f608b6e Mon Sep 17 00:00:00 2001 From: Dmytro Makovey Date: Thu, 24 Aug 2017 09:07:28 -0700 Subject: [PATCH 02/13] rename user_raw to user_import for more clarity. Add proper docstring --- admin/manage.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/admin/manage.py b/admin/manage.py index b57b2e2d..8ec6ade8 100644 --- a/admin/manage.py +++ b/admin/manage.py @@ -36,7 +36,7 @@ def admin(localpart, domain_name, password): @manager.command def user(localpart, domain_name, password, hash_scheme='SHA512-CRYPT'): - """ Create an user + """ Create a user """ domain = models.Domain.query.get(domain_name) if not domain: @@ -52,8 +52,12 @@ def user(localpart, domain_name, password, hash_scheme='SHA512-CRYPT'): db.session.commit() @manager.command -def user_raw(localpart, domain_name, password, hash_scheme='SHA512-CRYPT'): - """ Create an user +def user_import(localpart, domain_name, password_hash, hash_scheme='SHA512-CRYPT'): + """ Import a user along with password hash. Available hashes: + 'SHA512-CRYPT' + 'SHA256-CRYPT' + 'MD5-CRYPT' + 'CRYPT' """ domain = models.Domain.query.get(domain_name) if not domain: @@ -64,7 +68,7 @@ def user_raw(localpart, domain_name, password, hash_scheme='SHA512-CRYPT'): domain=domain, global_admin=False ) - user.set_password(password, hash_scheme=hash_scheme) + user.set_password(password_hash, hash_scheme=hash_scheme, raw=True) db.session.add(user) db.session.commit() From 59bc07cde515cdf9104d21c5cde99db462e9343e Mon Sep 17 00:00:00 2001 From: Dmytro Makovey Date: Fri, 25 Aug 2017 12:44:05 -0700 Subject: [PATCH 03/13] add config sync for bulk operations on users and aliases driven by config management systems etc. --- admin/manage.py | 48 ++++++++++++++++++++++++++++++++++++++++++ admin/requirements.txt | 1 + 2 files changed, 49 insertions(+) diff --git a/admin/manage.py b/admin/manage.py index 8ec6ade8..bb3c4a5e 100644 --- a/admin/manage.py +++ b/admin/manage.py @@ -72,6 +72,54 @@ def user_import(localpart, domain_name, password_hash, hash_scheme='SHA512-CRYPT db.session.add(user) db.session.commit() +@manager.command +def config_update(): + """sync configuration with data from stdin""" + import yaml, sys + new_config=yaml.load(sys.stdin) + # print new_config + users=new_config['users'] + for user_config in users: + localpart=user_config['localpart'] + domain_name=user_config['domain'] + password_hash=user_config['password_hash'] + hash_scheme=user_config['hash_scheme'] + domain = models.Domain.query.get(domain_name) + if not domain: + domain = models.Domain(name=domain_name) + db.session.add(domain) + user = models.User.query.get('{0}@{1}'.format(localpart,domain_name)) + if not user: + user = models.User( + localpart=localpart, + domain=domain, + global_admin=False + ) + user.set_password(password_hash, hash_scheme=hash_scheme, raw=True) + db.session.add(user) + + aliases=new_config['aliases'] + for alias_config in aliases: + localpart=alias_config['localpart'] + domain_name=alias_config['domain'] + destination=alias_config['destination'] + domain = models.Domain.query.get(domain_name) + if not domain: + domain = models.Domain(name=domain_name) + db.session.add(domain) + alias = models.Alias.query.get('{0}@{1}'.format(localpart, domain_name)) + if not alias: + alias = models.Alias( + localpart=localpart, + domain=domain, + destination=destination.split(','), + email="%s@%s" % (localpart, domain_name) + ) + else: + alias.destination = destination.split(',') + db.session.add(alias) + + db.session.commit() @manager.command def alias(localpart, domain_name, destination): diff --git a/admin/requirements.txt b/admin/requirements.txt index f5337d48..3d0ce119 100644 --- a/admin/requirements.txt +++ b/admin/requirements.txt @@ -14,3 +14,4 @@ docker-py tabulate apscheduler certbot +PyYAML From 28f490ddee6c05056c18664196f25265a04f5224 Mon Sep 17 00:00:00 2001 From: Dmytro Makovey Date: Fri, 25 Aug 2017 13:07:07 -0700 Subject: [PATCH 04/13] added object deletion to config update --- admin/manage.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/admin/manage.py b/admin/manage.py index bb3c4a5e..53d5c7f1 100644 --- a/admin/manage.py +++ b/admin/manage.py @@ -73,22 +73,25 @@ def user_import(localpart, domain_name, password_hash, hash_scheme='SHA512-CRYPT db.session.commit() @manager.command -def config_update(): - """sync configuration with data from stdin""" +def config_update(delete_objects=False): + """sync configuration with data from YAML-formatted stdin""" import yaml, sys new_config=yaml.load(sys.stdin) # print new_config users=new_config['users'] + tracked_users=set() for user_config in users: localpart=user_config['localpart'] domain_name=user_config['domain'] password_hash=user_config['password_hash'] hash_scheme=user_config['hash_scheme'] domain = models.Domain.query.get(domain_name) + email='{0}@{1}'.format(localpart,domain_name) if not domain: domain = models.Domain(name=domain_name) db.session.add(domain) - user = models.User.query.get('{0}@{1}'.format(localpart,domain_name)) + user = models.User.query.get(email) + tracked_users.add(email) if not user: user = models.User( localpart=localpart, @@ -99,26 +102,36 @@ def config_update(): db.session.add(user) aliases=new_config['aliases'] + tracked_aliases=set() for alias_config in aliases: localpart=alias_config['localpart'] domain_name=alias_config['domain'] destination=alias_config['destination'] domain = models.Domain.query.get(domain_name) + email='{0}@{1}'.format(localpart,domain_name) if not domain: domain = models.Domain(name=domain_name) db.session.add(domain) - alias = models.Alias.query.get('{0}@{1}'.format(localpart, domain_name)) + alias = models.Alias.query.get(email) + tracked_aliases.add(email) if not alias: alias = models.Alias( localpart=localpart, domain=domain, destination=destination.split(','), - email="%s@%s" % (localpart, domain_name) + email=email ) else: alias.destination = destination.split(',') db.session.add(alias) + if delete_objects: + for user in db.session.query(models.User).all(): + if not ( user.email in tracked_users ): + db.session.delete(user) + for alias in db.session.query(models.Alias).all(): + if not ( alias.email in tracked_aliases ): + db.session.delete(alias) db.session.commit() @manager.command From 95fd89808c5224e01abf3f5aabde5fe91d1a7306 Mon Sep 17 00:00:00 2001 From: Dmytro Makovey Date: Fri, 25 Aug 2017 14:38:16 -0700 Subject: [PATCH 05/13] add more CLI operations: deletions of users and aliases --- admin/manage.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/admin/manage.py b/admin/manage.py index 53d5c7f1..02dc0f11 100644 --- a/admin/manage.py +++ b/admin/manage.py @@ -134,6 +134,22 @@ def config_update(delete_objects=False): db.session.delete(alias) db.session.commit() +@manager.command +def user_delete(email): + """delete user""" + user = models.User.query.get(email) + if user: + db.session.delete(user) + db.session.commit() + +@manager.command +def alias_delete(email): + """delete alias""" + alias = models.Alias.query.get(email) + if alias: + db.session.delete(alias) + db.session.commit() + @manager.command def alias(localpart, domain_name, destination): """ Create an alias From 6525969a56e726461439372ed0297bd8814e5c4b Mon Sep 17 00:00:00 2001 From: Dmytro Makovey Date: Fri, 25 Aug 2017 14:48:36 -0700 Subject: [PATCH 06/13] ignoring virtualenv artefacts etc. --- admin/.gitignore | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 admin/.gitignore diff --git a/admin/.gitignore b/admin/.gitignore new file mode 100644 index 00000000..5bb3bd8e --- /dev/null +++ b/admin/.gitignore @@ -0,0 +1,4 @@ +.fg/ +lib64 +.vscode +tags From fcf37e6d5e506668930901e89f377ce9e95966e8 Mon Sep 17 00:00:00 2001 From: Dmytro Makovey Date: Fri, 25 Aug 2017 14:49:27 -0700 Subject: [PATCH 07/13] ignore vscode artefacts --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b5f73c9f..eb84ec09 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ pip-selfcheck.json /data /docker-compose.mac.yml /docker-compose.yml -/.idea \ No newline at end of file +/.idea +/.vscode From e28285155ef5dd5ebdfd9376550a2acdd31df88a Mon Sep 17 00:00:00 2001 From: Dmytro Makovey Date: Mon, 11 Sep 2017 09:11:12 -0700 Subject: [PATCH 08/13] Expect list instead of string for destination --- admin/manage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin/manage.py b/admin/manage.py index 02dc0f11..717d1d80 100644 --- a/admin/manage.py +++ b/admin/manage.py @@ -118,7 +118,7 @@ def config_update(delete_objects=False): alias = models.Alias( localpart=localpart, domain=domain, - destination=destination.split(','), + # destination=destination.split(','), email=email ) else: From e6a92af806d2ab17e6b4ff4c39aacd80fcb72f10 Mon Sep 17 00:00:00 2001 From: Dmytro Makovey Date: Mon, 11 Sep 2017 09:48:35 -0700 Subject: [PATCH 09/13] add verbosity level configuration option --- admin/manage.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/admin/manage.py b/admin/manage.py index 717d1d80..76bfe9a4 100644 --- a/admin/manage.py +++ b/admin/manage.py @@ -73,7 +73,7 @@ def user_import(localpart, domain_name, password_hash, hash_scheme='SHA512-CRYPT db.session.commit() @manager.command -def config_update(delete_objects=False): +def config_update(verbose=False, delete_objects=False): """sync configuration with data from YAML-formatted stdin""" import yaml, sys new_config=yaml.load(sys.stdin) @@ -81,6 +81,8 @@ def config_update(delete_objects=False): users=new_config['users'] tracked_users=set() for user_config in users: + if verbose: + print user_config localpart=user_config['localpart'] domain_name=user_config['domain'] password_hash=user_config['password_hash'] @@ -104,6 +106,8 @@ def config_update(delete_objects=False): aliases=new_config['aliases'] tracked_aliases=set() for alias_config in aliases: + if verbose: + print alias_config localpart=alias_config['localpart'] domain_name=alias_config['domain'] destination=alias_config['destination'] From e6bedabef07bca4b40bc3cd318886b4e7498f3a1 Mon Sep 17 00:00:00 2001 From: Dmytro Makovey Date: Mon, 11 Sep 2017 14:06:00 -0700 Subject: [PATCH 10/13] fix print call --- admin/manage.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/admin/manage.py b/admin/manage.py index 76bfe9a4..9ac3079a 100644 --- a/admin/manage.py +++ b/admin/manage.py @@ -82,7 +82,7 @@ def config_update(verbose=False, delete_objects=False): tracked_users=set() for user_config in users: if verbose: - print user_config + print(str(user_config)) localpart=user_config['localpart'] domain_name=user_config['domain'] password_hash=user_config['password_hash'] @@ -107,7 +107,7 @@ def config_update(verbose=False, delete_objects=False): tracked_aliases=set() for alias_config in aliases: if verbose: - print alias_config + print(str(alias_config)) localpart=alias_config['localpart'] domain_name=alias_config['domain'] destination=alias_config['destination'] From 9ddfa0a63399000e6bbf3a9a0d9ae93ef358cac6 Mon Sep 17 00:00:00 2001 From: Dmytro Makovey Date: Tue, 19 Sep 2017 08:05:10 -0700 Subject: [PATCH 11/13] adjust ciphers to a more secure set --- dovecot/conf/dovecot.conf | 3 ++- nginx/nginx.conf.default | 7 ++++--- nginx/nginx.conf.fallback | 3 ++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/dovecot/conf/dovecot.conf b/dovecot/conf/dovecot.conf index 0f4b04d8..36471272 100644 --- a/dovecot/conf/dovecot.conf +++ b/dovecot/conf/dovecot.conf @@ -61,7 +61,8 @@ ssl_key = Date: Tue, 19 Sep 2017 08:27:19 -0700 Subject: [PATCH 12/13] be smarted about destination field --- admin/manage.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/admin/manage.py b/admin/manage.py index 9ac3079a..0ba3501f 100644 --- a/admin/manage.py +++ b/admin/manage.py @@ -126,7 +126,10 @@ def config_update(verbose=False, delete_objects=False): email=email ) else: - alias.destination = destination.split(',') + if type(destination) == type(""): + alias.destination = destination.split(',') + else: + alias.destination = destination db.session.add(alias) if delete_objects: From b3d961a3dc07a7aa6e5946dd300cd5ddddef612c Mon Sep 17 00:00:00 2001 From: Dmytro Makovey Date: Thu, 21 Sep 2017 23:56:15 -0700 Subject: [PATCH 13/13] adding nginx modularity --- nginx/Dockerfile | 2 ++ nginx/extra.d/.keep | 0 nginx/http.d/.keep | 0 nginx/nginx.conf.default | 3 +++ nginx/nginx.conf.fallback | 4 ++++ 5 files changed, 9 insertions(+) create mode 100644 nginx/extra.d/.keep create mode 100644 nginx/http.d/.keep diff --git a/nginx/Dockerfile b/nginx/Dockerfile index 15d00972..2a8308f9 100644 --- a/nginx/Dockerfile +++ b/nginx/Dockerfile @@ -4,6 +4,8 @@ RUN apk add --no-cache nginx-mod-http-lua openssl COPY nginx.conf.default /etc/nginx/nginx.conf.default COPY nginx.conf.fallback /etc/nginx/nginx.conf.fallback +COPY http.d /etc/nginx/ +COPY extra.d /etc/nginx/ COPY start.sh /start.sh diff --git a/nginx/extra.d/.keep b/nginx/extra.d/.keep new file mode 100644 index 00000000..e69de29b diff --git a/nginx/http.d/.keep b/nginx/http.d/.keep new file mode 100644 index 00000000..e69de29b diff --git a/nginx/nginx.conf.default b/nginx/nginx.conf.default index 2dd4729c..87982712 100644 --- a/nginx/nginx.conf.default +++ b/nginx/nginx.conf.default @@ -50,6 +50,7 @@ http { set_by_lua $webdav 'return os.getenv("WEBDAV")'; set_by_lua $expose_admin 'return os.getenv("EXPOSE_ADMIN")'; + include http.d/*.conf; # Actual logic location / { @@ -97,3 +98,5 @@ http { } } } + +include extra.d/*.conf; diff --git a/nginx/nginx.conf.fallback b/nginx/nginx.conf.fallback index 985c6189..50bb77cb 100644 --- a/nginx/nginx.conf.fallback +++ b/nginx/nginx.conf.fallback @@ -36,6 +36,8 @@ http { add_header Strict-Transport-Security max-age=15768000; + include http.d/*.conf; + if ($scheme = http) { return 301 https://$host$request_uri; } @@ -45,3 +47,5 @@ http { } } } + +include extra.d/*.conf;