diff --git a/Dockerfile b/Dockerfile index 93ff400..8a95dbb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,8 @@ FROM python:3-alpine -RUN apk --no-cache add build-base openldap-dev python2-dev python3-dev +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 COPY templates ./templates COPY api.py filedb.py syncer.py ./ @@ -10,4 +11,5 @@ VOLUME [ "/db" ] VOLUME [ "/conf/dovecot" ] VOLUME [ "/conf/sogo" ] -ENTRYPOINT [ "python3", "syncer.py" ] \ No newline at end of file +COPY ./entrypoint.sh /entrypoint.sh +CMD ["/bin/ash", "/entrypoint.sh"] diff --git a/README.md b/README.md index dc5d8f1..74f8682 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,8 @@ 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 ``` 3. Configure environmental variables: @@ -51,9 +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) 4. Start additional container: `docker-compose up -d ldap-mailcow` 5. Check logs `docker-compose logs ldap-mailcow` diff --git a/api.py b/api.py index 8fd3dca..48bc4cf 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): + 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, replaceDomain, active=None, name=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): + 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): + 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) 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 diff --git a/syncer.py b/syncer.py index a1fd1e7..1cb7dbe 100644 --- a/syncer.py +++ b/syncer.py @@ -40,18 +40,23 @@ 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) 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, 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) + api.edit_user(email, replaceDomain, name=ldap_name) 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, replaceDomain, active=False) logging.info (f"Deactivated user {email} in Mailcow, not found in LDAP") filedb.user_set_active_to(email, False) @@ -148,6 +153,12 @@ 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['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 def read_dovecot_passdb_conf_template(): 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 +