From 320edb50c9dc1507a9315d9df7f464d7043515a2 Mon Sep 17 00:00:00 2001 From: Paz Date: Sun, 28 Mar 2021 01:21:42 +0800 Subject: [PATCH 01/17] Update certificates in dockerfile --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 93ff400..b78566c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,6 +2,7 @@ FROM python:3-alpine RUN apk --no-cache add build-base openldap-dev python2-dev python3-dev RUN pip3 install python-ldap sqlalchemy requests +RUN update-ca-certificates COPY templates ./templates COPY api.py filedb.py syncer.py ./ @@ -10,4 +11,4 @@ VOLUME [ "/db" ] VOLUME [ "/conf/dovecot" ] VOLUME [ "/conf/sogo" ] -ENTRYPOINT [ "python3", "syncer.py" ] \ No newline at end of file +ENTRYPOINT [ "python3", "syncer.py" ] From 10ee6f061a8a46609e09c84cc843291656790b0a Mon Sep 17 00:00:00 2001 From: Paz Date: Sun, 28 Mar 2021 01:29:14 +0800 Subject: [PATCH 02/17] Create entrypoint.sh --- entrypoint.sh | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 entrypoint.sh diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..95d694a --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,3 @@ +#!/bin/sh +update-ca-certificates +python3 syncer.py From 35ad2bc46229b056ce9365388924d3d41297e998 Mon Sep 17 00:00:00 2001 From: Paz Date: Sun, 28 Mar 2021 01:29:25 +0800 Subject: [PATCH 03/17] Update Dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index b78566c..d6baf05 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,4 +11,4 @@ VOLUME [ "/db" ] VOLUME [ "/conf/dovecot" ] VOLUME [ "/conf/sogo" ] -ENTRYPOINT [ "python3", "syncer.py" ] +ENTRYPOINT [ "entrypoint.sh" ] From 6628e1beef3e86f0a663a89fc2ada08b793f5108 Mon Sep 17 00:00:00 2001 From: Paz Date: Sun, 28 Mar 2021 01:35:17 +0800 Subject: [PATCH 04/17] Fix dockerfile - im an idiot --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index d6baf05..25319db 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,4 +11,5 @@ VOLUME [ "/db" ] VOLUME [ "/conf/dovecot" ] VOLUME [ "/conf/sogo" ] -ENTRYPOINT [ "entrypoint.sh" ] +COPY ./entrypoint.sh /entrypoint.sh +CMD ["/bin/ash", "/entrypoint.sh"] From 58b2ca62b056d54f4bdbbec333a906de8ce2f30b Mon Sep 17 00:00:00 2001 From: Paz Date: Sun, 28 Mar 2021 01:44:18 +0800 Subject: [PATCH 05/17] Update Dockerfile --- Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 25319db..f8d3c9c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM python:3-alpine -RUN apk --no-cache add build-base openldap-dev python2-dev python3-dev +RUN apk update && apk --no-cache add build-base openldap-dev python2-dev python3-dev ca-certificates && rm -rf /var/cache/apk/* mkdir /usr/local/share/ca-certificates/extra RUN pip3 install python-ldap sqlalchemy requests RUN update-ca-certificates @@ -11,5 +11,5 @@ VOLUME [ "/db" ] VOLUME [ "/conf/dovecot" ] VOLUME [ "/conf/sogo" ] -COPY ./entrypoint.sh /entrypoint.sh -CMD ["/bin/ash", "/entrypoint.sh"] +COPY ./entrypoint.sh /entrypoint.sh +CMD ["/bin/ash", "/entrypoint.sh"] From 84dd67da01622ef98f5d2f36a786acd7d2f6feda Mon Sep 17 00:00:00 2001 From: Paz Date: Sun, 28 Mar 2021 21:45:46 +0800 Subject: [PATCH 06/17] (1/2) Add option to replace domain --- api.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/api.py b/api.py index 8fd3dca..edb3375 100644 --- a/api.py +++ b/api.py @@ -18,7 +18,9 @@ def __post_request(url, json_data): if rsp['type'] != 'success': sys.exit(f"API {url}: {rsp['type']} - {rsp['msg']}") -def add_user(email, name, active): +def add_user(email, name, active, replaceDomain=None): + if replaceDomain is not None: + email = email.replace(email.split('@')[1], replaceDomain) password = ''.join(random.choices(string.ascii_letters + string.digits, k=20)) json_data = { 'local_part':email.split('@')[0], @@ -31,7 +33,9 @@ def add_user(email, name, active): __post_request('api/v1/add/mailbox', json_data) -def edit_user(email, active=None, name=None): +def edit_user(email, active=None, name=None, replaceDomain=None): + if replaceDomain is not None: + email = email.replace(email.split('@')[1], replaceDomain) attr = {} if (active is not None): attr['active'] = 1 if active else 0 @@ -45,12 +49,16 @@ def edit_user(email, active=None, name=None): __post_request('api/v1/edit/mailbox', json_data) -def __delete_user(email): +def __delete_user(email, replaceDomain=None): + if replaceDomain is not None: + email = email.replace(email.split('@')[1], replaceDomain) json_data = [email] __post_request('api/v1/delete/mailbox', json_data) -def check_user(email): +def check_user(email, replaceDomain=None): + if replaceDomain is not None: + email = email.replace(email.split('@')[1], replaceDomain) url = f"{api_host}/api/v1/get/mailbox/{email}" headers = {'X-API-Key': api_key, 'Content-type': 'application/json'} req = requests.get(url, headers=headers) From 7747b50da9158acc845f69e05919eceea3d5497d Mon Sep 17 00:00:00 2001 From: Paz Date: Sun, 28 Mar 2021 21:49:58 +0800 Subject: [PATCH 07/17] (2/2) Add option to replace domain --- syncer.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/syncer.py b/syncer.py index a1fd1e7..6f98940 100644 --- a/syncer.py +++ b/syncer.py @@ -49,9 +49,14 @@ def sync(): filedb.session_time = datetime.datetime.now() + if 'REPLACE_DOMAIN' in config: + replaceDomain = config['REPLACE_DOMAIN'] + else: + replaceDomain = None + for (email, ldap_name, ldap_active) in ldap_results: (db_user_exists, db_user_active) = filedb.check_user(email) - (api_user_exists, api_user_active, api_name) = api.check_user(email) + (api_user_exists, api_user_active, api_name) = api.check_user(email, replaceDomain) unchanged = True @@ -62,7 +67,7 @@ def sync(): unchanged = False if not api_user_exists: - api.add_user(email, ldap_name, ldap_active) + api.add_user(email, ldap_name, ldap_active, replaceDomain) (api_user_exists, api_user_active, api_name) = (True, ldap_active, ldap_name) logging.info (f"Added Mailcow user: {email} (Active: {ldap_active})") unchanged = False @@ -73,12 +78,12 @@ def sync(): unchanged = False if api_user_active != ldap_active: - api.edit_user(email, active=ldap_active) + api.edit_user(email, active=ldap_active, replaceDomain) logging.info (f"{'Activated' if ldap_active else 'Deactived'} {email} in Mailcow") unchanged = False if api_name != ldap_name: - api.edit_user(email, name=ldap_name) + api.edit_user(email, name=ldap_name, replaceDomain) logging.info (f"Changed name of {email} in Mailcow to {ldap_name}") unchanged = False @@ -86,10 +91,10 @@ def sync(): logging.info (f"Checked user {email}, unchanged") for email in filedb.get_unchecked_active_users(): - (api_user_exists, api_user_active, _) = api.check_user(email) + (api_user_exists, api_user_active, _) = api.check_user(email, replaceDomain) if (api_user_active and api_user_active): - api.edit_user(email, active=False) + api.edit_user(email, active=False, replaceDomain) logging.info (f"Deactivated user {email} in Mailcow, not found in LDAP") filedb.user_set_active_to(email, False) From 931024ba5e092c9d5e0d1a6d139738e4e52ae16b Mon Sep 17 00:00:00 2001 From: Paz Date: Sun, 28 Mar 2021 21:52:09 +0800 Subject: [PATCH 08/17] Env -> config --- syncer.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/syncer.py b/syncer.py index 6f98940..fb6649f 100644 --- a/syncer.py +++ b/syncer.py @@ -153,6 +153,9 @@ def read_config(): config['LDAP_FILTER'] = os.environ['LDAP-MAILCOW_LDAP_FILTER'] if 'LDAP-MAILCOW_LDAP_FILTER' in os.environ else '(&(objectClass=user)(objectCategory=person))' config['SOGO_LDAP_FILTER'] = os.environ['LDAP-MAILCOW_SOGO_LDAP_FILTER'] if 'LDAP-MAILCOW_SOGO_LDAP_FILTER' in os.environ else "objectClass='user' AND objectCategory='person'" + if 'LDAP-MAILCOW_REPLACE_DOMAIN' in os.environ: + config['SWAP_TLD'] = os.environ['LDAP-MAILCOW_REPLACE_DOMAIN'] + return config def read_dovecot_passdb_conf_template(): From adadd76e358e53df9b6b80fc03f45c7ea7b5c857 Mon Sep 17 00:00:00 2001 From: Paz Date: Sun, 28 Mar 2021 21:59:11 +0800 Subject: [PATCH 09/17] Update api.py --- api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api.py b/api.py index edb3375..f18eaf7 100644 --- a/api.py +++ b/api.py @@ -18,7 +18,7 @@ def __post_request(url, json_data): if rsp['type'] != 'success': sys.exit(f"API {url}: {rsp['type']} - {rsp['msg']}") -def add_user(email, name, active, replaceDomain=None): +def add_user(email, name, active, replaceDomain): if replaceDomain is not None: email = email.replace(email.split('@')[1], replaceDomain) password = ''.join(random.choices(string.ascii_letters + string.digits, k=20)) @@ -33,7 +33,7 @@ def add_user(email, name, active, replaceDomain=None): __post_request('api/v1/add/mailbox', json_data) -def edit_user(email, active=None, name=None, replaceDomain=None): +def edit_user(email, active=None, name=None, replaceDomain): if replaceDomain is not None: email = email.replace(email.split('@')[1], replaceDomain) attr = {} @@ -49,14 +49,14 @@ def edit_user(email, active=None, name=None, replaceDomain=None): __post_request('api/v1/edit/mailbox', json_data) -def __delete_user(email, replaceDomain=None): +def __delete_user(email, replaceDomain): if replaceDomain is not None: email = email.replace(email.split('@')[1], replaceDomain) json_data = [email] __post_request('api/v1/delete/mailbox', json_data) -def check_user(email, replaceDomain=None): +def check_user(email, replaceDomain): if replaceDomain is not None: email = email.replace(email.split('@')[1], replaceDomain) url = f"{api_host}/api/v1/get/mailbox/{email}" From 73887cfd4f1f6646634574bc225c4cbe40bcf150 Mon Sep 17 00:00:00 2001 From: Paz Date: Sun, 28 Mar 2021 22:09:32 +0800 Subject: [PATCH 10/17] Update syncer.py --- syncer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/syncer.py b/syncer.py index fb6649f..6595a2c 100644 --- a/syncer.py +++ b/syncer.py @@ -78,12 +78,12 @@ def sync(): unchanged = False if api_user_active != ldap_active: - api.edit_user(email, active=ldap_active, replaceDomain) + api.edit_user(email, replaceDomain, active=ldap_active) logging.info (f"{'Activated' if ldap_active else 'Deactived'} {email} in Mailcow") unchanged = False if api_name != ldap_name: - api.edit_user(email, name=ldap_name, replaceDomain) + api.edit_user(email, replaceDomain, name=ldap_name) logging.info (f"Changed name of {email} in Mailcow to {ldap_name}") unchanged = False @@ -94,7 +94,7 @@ def sync(): (api_user_exists, api_user_active, _) = api.check_user(email, replaceDomain) if (api_user_active and api_user_active): - api.edit_user(email, active=False, replaceDomain) + api.edit_user(email, replaceDomain, active=False) logging.info (f"Deactivated user {email} in Mailcow, not found in LDAP") filedb.user_set_active_to(email, False) From 5333276cab190e7435877871b4ddfec0bf3dcfb5 Mon Sep 17 00:00:00 2001 From: Paz Date: Sun, 28 Mar 2021 22:13:40 +0800 Subject: [PATCH 11/17] Update api.py --- api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api.py b/api.py index f18eaf7..48bc4cf 100644 --- a/api.py +++ b/api.py @@ -33,7 +33,7 @@ def add_user(email, name, active, replaceDomain): __post_request('api/v1/add/mailbox', json_data) -def edit_user(email, active=None, name=None, replaceDomain): +def edit_user(email, replaceDomain, active=None, name=None): if replaceDomain is not None: email = email.replace(email.split('@')[1], replaceDomain) attr = {} From 252307e3771fb61b5e47ba6212fc3ed48b2a0d7e Mon Sep 17 00:00:00 2001 From: Paz Date: Sun, 28 Mar 2021 22:33:08 +0800 Subject: [PATCH 12/17] Update syncer.py --- syncer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncer.py b/syncer.py index 6595a2c..5e17181 100644 --- a/syncer.py +++ b/syncer.py @@ -154,7 +154,7 @@ def read_config(): config['SOGO_LDAP_FILTER'] = os.environ['LDAP-MAILCOW_SOGO_LDAP_FILTER'] if 'LDAP-MAILCOW_SOGO_LDAP_FILTER' in os.environ else "objectClass='user' AND objectCategory='person'" if 'LDAP-MAILCOW_REPLACE_DOMAIN' in os.environ: - config['SWAP_TLD'] = os.environ['LDAP-MAILCOW_REPLACE_DOMAIN'] + config['REPLACE_DOMAIN'] = os.environ['LDAP-MAILCOW_REPLACE_DOMAIN'] return config From 1654bfd3dddab04f0544e2e705af841d507252f2 Mon Sep 17 00:00:00 2001 From: Paz Date: Sun, 28 Mar 2021 22:52:21 +0800 Subject: [PATCH 13/17] Revert some useless changes to dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index f8d3c9c..8a95dbb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM python:3-alpine -RUN apk update && apk --no-cache add build-base openldap-dev python2-dev python3-dev ca-certificates && rm -rf /var/cache/apk/* mkdir /usr/local/share/ca-certificates/extra +RUN apk --no-cache add build-base openldap-dev python2-dev python3-dev ca-certificates RUN pip3 install python-ldap sqlalchemy requests RUN update-ca-certificates From 095565ccd5d0f503d7ffee5eeed965a6e7381045 Mon Sep 17 00:00:00 2001 From: Paz Date: Sun, 28 Mar 2021 22:55:34 +0800 Subject: [PATCH 14/17] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index dc5d8f1..6fe8c68 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ A python script periodically checks and creates new LDAP accounts and deactivate - ./data/ldap:/db:rw - ./data/conf/dovecot:/conf/dovecot:rw - ./data/conf/sogo:/conf/sogo:rw + #- /usr/local/share/ca-certificates/:/usr/local/share/ca-certificates/ environment: - LDAP-MAILCOW_LDAP_URI=ldap(s)://dc.example.local - LDAP-MAILCOW_LDAP_BASE_DN=OU=Mail Users,DC=example,DC=local @@ -40,6 +41,7 @@ A python script periodically checks and creates new LDAP accounts and deactivate - LDAP-MAILCOW_SYNC_INTERVAL=300 - LDAP-MAILCOW_LDAP_FILTER=(&(objectClass=user)(objectCategory=person)(memberOf:1.2.840.113556.1.4.1941:=CN=Group,CN=Users,DC=example DC=local)) - LDAP-MAILCOW_SOGO_LDAP_FILTER=objectClass='user' AND objectCategory='person' AND memberOf:1.2.840.113556.1.4.1941:='CN=Group,CN=Users,DC=example DC=local' + #- LDAP-MAILCOW_REPLACE_DOMAIN=example.com ``` 3. Configure environmental variables: @@ -54,6 +56,7 @@ A python script periodically checks and creates new LDAP accounts and deactivate * **Optional** LDAP filters (see example above). SOGo uses special syntax, so you either have to **specify both or none**: * `LDAP-MAILCOW_LDAP_FILTER` - LDAP filter to apply, defaults to `(&(objectClass=user)(objectCategory=person))` * `LDAP-MAILCOW_SOGO_LDAP_FILTER` - LDAP filter to apply for SOGo ([special syntax](https://sogo.nu/files/docs/SOGoInstallationGuide.html#_authentication_using_ldap)), defaults to `objectClass='user' AND objectCategory='person'` + * LDAP-MAILCOW_REPLACE_DOMAIN - **Optional** Replace domain (eg you have public domain and internal AD domain) 4. Start additional container: `docker-compose up -d ldap-mailcow` 5. Check logs `docker-compose logs ldap-mailcow` From 793c8088ca305b1f6a6ac0597ed1bd59d5f3c277 Mon Sep 17 00:00:00 2001 From: Paz Date: Mon, 29 Mar 2021 10:36:02 +0800 Subject: [PATCH 15/17] Add configurable user attribute config['USER_ATTR'] --- syncer.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/syncer.py b/syncer.py index 5e17181..1cb7dbe 100644 --- a/syncer.py +++ b/syncer.py @@ -40,10 +40,10 @@ def sync(): ldap_results = ldap_connector.search_s(config['LDAP_BASE_DN'], ldap.SCOPE_SUBTREE, config['LDAP_FILTER'], - ['userPrincipalName', 'cn', 'userAccountControl']) + [config['USER_ATTR'], 'cn', 'userAccountControl']) ldap_results = map(lambda x: ( - x[1]['userPrincipalName'][0].decode(), + x[1][config['USER_ATTR']][0].decode(), x[1]['cn'][0].decode(), False if int(x[1]['userAccountControl'][0].decode()) & 0b10 else True), ldap_results) @@ -155,6 +155,9 @@ def read_config(): if 'LDAP-MAILCOW_REPLACE_DOMAIN' in os.environ: config['REPLACE_DOMAIN'] = os.environ['LDAP-MAILCOW_REPLACE_DOMAIN'] + + if 'LDAP-MAILCOW_USER_ATTR' in os.environ: + config['USER_ATTR'] = os.environ['LDAP-MAILCOW_USER_ATTR'] return config From 0459bb7d9969b910d1a1726cc05b7508d5e3d9eb Mon Sep 17 00:00:00 2001 From: Paz Date: Mon, 29 Mar 2021 10:37:46 +0800 Subject: [PATCH 16/17] $user_attr --- templates/sogo/plist_ldap | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/sogo/plist_ldap b/templates/sogo/plist_ldap index cf39ee5..011f003 100644 --- a/templates/sogo/plist_ldap +++ b/templates/sogo/plist_ldap @@ -10,7 +10,7 @@ IDFieldName cn UIDFieldName - userPrincipalName + $user_attr baseDN $ldap_base_dn @@ -21,7 +21,7 @@ $ldap_bind_dn_password bindFields - userPrincipalName + $user_attr bindAsCurrentUser @@ -42,4 +42,4 @@ scope SUB - \ No newline at end of file + From 4d9cba96d412e3c096a8119eb934a39bf8b70bfa Mon Sep 17 00:00:00 2001 From: Paz Date: Mon, 29 Mar 2021 10:40:56 +0800 Subject: [PATCH 17/17] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6fe8c68..74f8682 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ A python script periodically checks and creates new LDAP accounts and deactivate - LDAP-MAILCOW_SYNC_INTERVAL=300 - LDAP-MAILCOW_LDAP_FILTER=(&(objectClass=user)(objectCategory=person)(memberOf:1.2.840.113556.1.4.1941:=CN=Group,CN=Users,DC=example DC=local)) - LDAP-MAILCOW_SOGO_LDAP_FILTER=objectClass='user' AND objectCategory='person' AND memberOf:1.2.840.113556.1.4.1941:='CN=Group,CN=Users,DC=example DC=local' + - LDAP-MAILCOW_USER_ATTR=userPrincipalName #- LDAP-MAILCOW_REPLACE_DOMAIN=example.com ``` @@ -53,10 +54,11 @@ A python script periodically checks and creates new LDAP accounts and deactivate * `LDAP-MAILCOW_API_HOST` - mailcow API url. Make sure it's enabled and accessible from within the container for both reads and writes * `LDAP-MAILCOW_API_KEY` - mailcow API key (read/write) * `LDAP-MAILCOW_SYNC_INTERVAL` - interval in seconds between LDAP synchronizations + * `LDAP-MAILCOW_USER_ATTR` - user attribute to use * **Optional** LDAP filters (see example above). SOGo uses special syntax, so you either have to **specify both or none**: * `LDAP-MAILCOW_LDAP_FILTER` - LDAP filter to apply, defaults to `(&(objectClass=user)(objectCategory=person))` * `LDAP-MAILCOW_SOGO_LDAP_FILTER` - LDAP filter to apply for SOGo ([special syntax](https://sogo.nu/files/docs/SOGoInstallationGuide.html#_authentication_using_ldap)), defaults to `objectClass='user' AND objectCategory='person'` - * LDAP-MAILCOW_REPLACE_DOMAIN - **Optional** Replace domain (eg you have public domain and internal AD domain) + * `LDAP-MAILCOW_REPLACE_DOMAIN` - **Optional** Replace domain (eg you have public domain and internal AD domain) 4. Start additional container: `docker-compose up -d ldap-mailcow` 5. Check logs `docker-compose logs ldap-mailcow`