From db9ac1f68ede4362d28c2a9dcbe17b3af83a6215 Mon Sep 17 00:00:00 2001 From: Dmytro Makovey Date: Thu, 24 Aug 2017 07:23:54 -0700 Subject: [PATCH 1/7] add encryption scheme manipulation --- 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 4097811f..bbb08b7b 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 aef89753323b6b388575903b34ac3e999ab15336 Mon Sep 17 00:00:00 2001 From: Dmytro Makovey Date: Thu, 24 Aug 2017 09:07:28 -0700 Subject: [PATCH 2/7] 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 bbb08b7b..bdfd30d6 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 e4b338c9a4c51dda106e558ba03c4ebd6131ec8c Mon Sep 17 00:00:00 2001 From: Dmytro Makovey Date: Fri, 25 Aug 2017 12:44:05 -0700 Subject: [PATCH 3/7] 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 bdfd30d6..16269272 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 d099e24f18b31b3ad12e25ff3323aed14d4aa5e7 Mon Sep 17 00:00:00 2001 From: Dmytro Makovey Date: Fri, 25 Aug 2017 13:07:07 -0700 Subject: [PATCH 4/7] 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 16269272..fdf84ee0 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 433da57015489d523b02183642f7694a87397885 Mon Sep 17 00:00:00 2001 From: Dmytro Makovey Date: Fri, 25 Aug 2017 14:38:16 -0700 Subject: [PATCH 5/7] 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 fdf84ee0..dfe3012f 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 856593990465588131d00a05f9842e2795faa5f3 Mon Sep 17 00:00:00 2001 From: Dmytro Makovey Date: Fri, 25 Aug 2017 14:48:36 -0700 Subject: [PATCH 6/7] 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 e8b62484a9df459ace9aa361a9c2718becef4b41 Mon Sep 17 00:00:00 2001 From: Dmytro Makovey Date: Fri, 25 Aug 2017 14:49:27 -0700 Subject: [PATCH 7/7] 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