From a699c03ba9ee489b060c80821b49299203045f75 Mon Sep 17 00:00:00 2001 From: polarathene <5098581+polarathene@users.noreply.github.com> Date: Sun, 3 Sep 2023 16:40:53 +1200 Subject: [PATCH 01/15] refactor: Generate `saslauthd.conf` via Config Template feature From a inline config via HereDoc to using a `.tmpl` file that has of all supported keys for SASLAuthd LDAP config. This additionally supports layering the ENV `.tmpl` generated config over a user-provided config. With a utility method that will ensure earlier duplicate keys are removed. The two new utilities are documented well enough to grok. `Dockerfile` and `packages.sh` updated to bring in new dependencies and provide the `.tmpl` file. `log_level` is not documented as a LDAP config key. Original PR did not explain why this key and value chosen were added. --- Dockerfile | 3 ++ target/features/ldap/saslauthd.tmpl | 36 +++++++++++++++++++++ target/scripts/build/packages.sh | 13 ++++++++ target/scripts/helpers/utils.sh | 36 +++++++++++++++++++++ target/scripts/startup/setup.d/saslauthd.sh | 36 ++++++++++----------- 5 files changed, 106 insertions(+), 18 deletions(-) create mode 100644 target/features/ldap/saslauthd.tmpl diff --git a/Dockerfile b/Dockerfile index 4d0e3568..f6b40124 100644 --- a/Dockerfile +++ b/Dockerfile @@ -120,6 +120,9 @@ COPY \ target/postfix/ldap-senders.cf \ /etc/postfix/ +# LDAP config support: +COPY --link target/features/ldap/ /etc/dms/ldap/ + # hadolint ignore=SC2016 RUN < Removes lines where keys have no value assigned. +# - tac + sort => Remove any duplicate keys (keeps the last instance found). +# +# @param ${1} = A delimiter between key and value columns +# @param ${2} = Input filepath to clean +# @output = The transformed file content +function _cleanse_config() { + local KV_DELIMITER=${1:?KV Delimiter is required} + local INPUT_FILE=${2?:Input file is required} + + sed "/^[^${KV_DELIMITER}]*${KV_DELIMITER}\s*$/d" ${INPUT_FILE} \ + | tac | sort -u -t"${KV_DELIMITER}" -k1,1 +} diff --git a/target/scripts/startup/setup.d/saslauthd.sh b/target/scripts/startup/setup.d/saslauthd.sh index eb33a243..daddb852 100644 --- a/target/scripts/startup/setup.d/saslauthd.sh +++ b/target/scripts/startup/setup.d/saslauthd.sh @@ -9,24 +9,7 @@ function _setup_saslauthd() { if [[ ${ACCOUNT_PROVISIONER} == 'LDAP' ]] \ && [[ ! -f /etc/saslauthd.conf ]]; then _log 'trace' 'Creating /etc/saslauthd.conf' - - # Create a config based on ENV - sed '/^.*: $/d'> /etc/saslauthd.conf << EOF -ldap_servers: ${SASLAUTHD_LDAP_SERVER:=${LDAP_SERVER_HOST}} -ldap_auth_method: ${SASLAUTHD_LDAP_AUTH_METHOD:=bind} -ldap_bind_dn: ${SASLAUTHD_LDAP_BIND_DN:=${LDAP_BIND_DN}} -ldap_bind_pw: ${SASLAUTHD_LDAP_PASSWORD:=${LDAP_BIND_PW}} -ldap_search_base: ${SASLAUTHD_LDAP_SEARCH_BASE:=${LDAP_SEARCH_BASE}} -ldap_filter: ${SASLAUTHD_LDAP_FILTER:=(&(uniqueIdentifier=%u)(mailEnabled=TRUE))} -ldap_start_tls: ${SASLAUTHD_LDAP_START_TLS:=no} -ldap_tls_check_peer: ${SASLAUTHD_LDAP_TLS_CHECK_PEER:=no} -ldap_tls_cacert_file: ${SASLAUTHD_LDAP_TLS_CACERT_FILE} -ldap_tls_cacert_dir: ${SASLAUTHD_LDAP_TLS_CACERT_DIR} -ldap_password_attr: ${SASLAUTHD_LDAP_PASSWORD_ATTR} -ldap_mech: ${SASLAUTHD_LDAP_MECH} -ldap_referrals: yes -log_level: 10 -EOF + _create_config_saslauthd fi sed -i \ @@ -42,3 +25,20 @@ EOF gpasswd -a postfix sasl >/dev/null } + +function _create_config_saslauthd() { + local SASLAUTHD_LDAP_SERVER=${SASLAUTHD_LDAP_SERVER:=${LDAP_SERVER_HOST}} + local SASLAUTHD_LDAP_BIND_DN=${SASLAUTHD_LDAP_BIND_DN:=${LDAP_BIND_DN}} + local SASLAUTHD_LDAP_PASSWORD=${SASLAUTHD_LDAP_PASSWORD:=${LDAP_BIND_PW}} + local SASLAUTHD_LDAP_SEARCH_BASE=${SASLAUTHD_LDAP_SEARCH_BASE:=${LDAP_SEARCH_BASE}} + local SASLAUTHD_LDAP_FILTER=${SASLAUTHD_LDAP_FILTER:=(&(uniqueIdentifier=%u)(mailEnabled=TRUE))} + local SASLAUTHD_LDAP_REFERRALS=${SASLAUTHD_LDAP_REFERRALS:=yes} + + # Generates a config from an ENV template while layering several other sources + # into a single temporary file, used as input into `_cleanse_config` which + # prepares the final output config. + _cleanse_config ':' <(cat 2>/dev/null \ + /tmp/docker-mailserver/ldap/saslauthd.conf \ + <(_template_with_env 'SASLAUTHD_' /etc/dms/ldap/saslauthd.tmpl) \ + ) > /etc/saslauthd.conf +} From 54eebb9129f326a2bf7979bc4953b5dd8d33fb4d Mon Sep 17 00:00:00 2001 From: polarathene <5098581+polarathene@users.noreply.github.com> Date: Sun, 3 Sep 2023 16:52:52 +1200 Subject: [PATCH 02/15] chore: `saslauthd.conf` generation - Use a base config for defaults This avoids the ENV overriding user-provided settings due to the ENV having hard-coded fallback defaults when not explicitly set. --- target/features/ldap/saslauthd.base | 2 ++ target/scripts/startup/setup.d/saslauthd.sh | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 target/features/ldap/saslauthd.base diff --git a/target/features/ldap/saslauthd.base b/target/features/ldap/saslauthd.base new file mode 100644 index 00000000..4dddd75f --- /dev/null +++ b/target/features/ldap/saslauthd.base @@ -0,0 +1,2 @@ +ldap_filter: (&(uniqueIdentifier=%u)(mailEnabled=TRUE)) +ldap_referrals: yes diff --git a/target/scripts/startup/setup.d/saslauthd.sh b/target/scripts/startup/setup.d/saslauthd.sh index daddb852..524a8bb5 100644 --- a/target/scripts/startup/setup.d/saslauthd.sh +++ b/target/scripts/startup/setup.d/saslauthd.sh @@ -31,13 +31,12 @@ function _create_config_saslauthd() { local SASLAUTHD_LDAP_BIND_DN=${SASLAUTHD_LDAP_BIND_DN:=${LDAP_BIND_DN}} local SASLAUTHD_LDAP_PASSWORD=${SASLAUTHD_LDAP_PASSWORD:=${LDAP_BIND_PW}} local SASLAUTHD_LDAP_SEARCH_BASE=${SASLAUTHD_LDAP_SEARCH_BASE:=${LDAP_SEARCH_BASE}} - local SASLAUTHD_LDAP_FILTER=${SASLAUTHD_LDAP_FILTER:=(&(uniqueIdentifier=%u)(mailEnabled=TRUE))} - local SASLAUTHD_LDAP_REFERRALS=${SASLAUTHD_LDAP_REFERRALS:=yes} # Generates a config from an ENV template while layering several other sources # into a single temporary file, used as input into `_cleanse_config` which # prepares the final output config. _cleanse_config ':' <(cat 2>/dev/null \ + /etc/dms/ldap/saslauthd.base \ /tmp/docker-mailserver/ldap/saslauthd.conf \ <(_template_with_env 'SASLAUTHD_' /etc/dms/ldap/saslauthd.tmpl) \ ) > /etc/saslauthd.conf From 4cc300f9b1d502b50b781aab1cbb274d89106cae Mon Sep 17 00:00:00 2001 From: polarathene <5098581+polarathene@users.noreply.github.com> Date: Sun, 3 Sep 2023 17:01:22 +1200 Subject: [PATCH 03/15] chore: `saslauthd.conf` generation - Use common `LDAP_` ENV in base Similar to the prior commit, this ensures default ENV fallback doesn't accidentally override explicit user-provided config settings. --- target/features/ldap/saslauthd.base | 4 ++++ target/scripts/startup/setup.d/saslauthd.sh | 13 ++++--------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/target/features/ldap/saslauthd.base b/target/features/ldap/saslauthd.base index 4dddd75f..4d92c33e 100644 --- a/target/features/ldap/saslauthd.base +++ b/target/features/ldap/saslauthd.base @@ -1,2 +1,6 @@ +ldap_bind_dn: ${BIND_DN} +ldap_bind_pw: ${BIND_PW} +ldap_servers: ${SERVER_HOST} +ldap_search_base: ${SEARCH_BASE} ldap_filter: (&(uniqueIdentifier=%u)(mailEnabled=TRUE)) ldap_referrals: yes diff --git a/target/scripts/startup/setup.d/saslauthd.sh b/target/scripts/startup/setup.d/saslauthd.sh index 524a8bb5..c1c23e4c 100644 --- a/target/scripts/startup/setup.d/saslauthd.sh +++ b/target/scripts/startup/setup.d/saslauthd.sh @@ -26,17 +26,12 @@ function _setup_saslauthd() { gpasswd -a postfix sasl >/dev/null } +# Generates a config from an ENV template while layering several other sources +# into a single temporary file, used as input into `_cleanse_config` which +# prepares the final output config. function _create_config_saslauthd() { - local SASLAUTHD_LDAP_SERVER=${SASLAUTHD_LDAP_SERVER:=${LDAP_SERVER_HOST}} - local SASLAUTHD_LDAP_BIND_DN=${SASLAUTHD_LDAP_BIND_DN:=${LDAP_BIND_DN}} - local SASLAUTHD_LDAP_PASSWORD=${SASLAUTHD_LDAP_PASSWORD:=${LDAP_BIND_PW}} - local SASLAUTHD_LDAP_SEARCH_BASE=${SASLAUTHD_LDAP_SEARCH_BASE:=${LDAP_SEARCH_BASE}} - - # Generates a config from an ENV template while layering several other sources - # into a single temporary file, used as input into `_cleanse_config` which - # prepares the final output config. _cleanse_config ':' <(cat 2>/dev/null \ - /etc/dms/ldap/saslauthd.base \ + <(_template_with_env 'LDAP_' /etc/dms/ldap/saslauthd.base) \ /tmp/docker-mailserver/ldap/saslauthd.conf \ <(_template_with_env 'SASLAUTHD_' /etc/dms/ldap/saslauthd.tmpl) \ ) > /etc/saslauthd.conf From b5edba69ad37cf61ef8235d9ec2a6ae5d9bdd4c8 Mon Sep 17 00:00:00 2001 From: polarathene <5098581+polarathene@users.noreply.github.com> Date: Sun, 3 Sep 2023 17:20:00 +1200 Subject: [PATCH 04/15] chore: Adapt Dovecot LDAP config generation to use Config Template Same process as described by earlier commits for SASLAuthd. To avoid introducing potential breakage, the ENV fallback convenience for `DOVECOT_PASS_FILTER` is retained. --- target/features/ldap/dovecot.base | 9 ++++++++ target/features/ldap/dovecot.tmpl | 32 ++++++++++++++++++++++++++ target/scripts/startup/setup.d/ldap.sh | 30 +++++++++++------------- 3 files changed, 55 insertions(+), 16 deletions(-) create mode 100644 target/features/ldap/dovecot.base create mode 100644 target/features/ldap/dovecot.tmpl diff --git a/target/features/ldap/dovecot.base b/target/features/ldap/dovecot.base new file mode 100644 index 00000000..3ef4e15c --- /dev/null +++ b/target/features/ldap/dovecot.base @@ -0,0 +1,9 @@ +dn = ${BIND_DN} +dnpass = ${BIND_PW} +uris = ${SERVER_HOST} +base = ${SEARCH_BASE} +default_pass_scheme = SSHA +pass_attrs = uniqueIdentifier=user,userPassword=password +pass_filter = (&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n)) +user_attrs = mailHomeDirectory=home,mailUidNumber=uid,mailGidNumber=gid,mailStorageDirectory=mail +user_filter = (&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n)) diff --git a/target/features/ldap/dovecot.tmpl b/target/features/ldap/dovecot.tmpl new file mode 100644 index 00000000..c95c399f --- /dev/null +++ b/target/features/ldap/dovecot.tmpl @@ -0,0 +1,32 @@ +# Dovecot LDAP config docs: https://github.com/dovecot/core/blob/bbb600e46ca650a3a5ef812ea3a1e8c45a6ea0ba/doc/example-config/dovecot-ldap.conf.ext +hosts = ${HOSTS} +uris = ${URIS} +dn = ${DN} +dnpass = ${DNPASS} +sasl_bind = ${SASL_BIND} +sasl_mech = ${SASL_MECH} +sasl_realm = ${SASL_REALM} +sasl_authz_id = ${SASL_AUTHZ_ID} +tls = ${TLS} +tls_ca_cert_file = ${TLS_CA_CERT_FILE} +tls_ca_cert_dir = ${TLS_CA_CERT_DIR} +tls_cipher_suite = ${TLS_CIPHER_SUITE} +tls_cert_file = ${TLS_CERT_FILE} +tls_key_file = ${TLS_KEY_FILE} +tls_require_cert = ${TLS_REQUIRE_CERT} +ldaprc_path = ${LDAPRC_PATH} +debug_level = ${DEBUG_LEVEL} +auth_bind = ${AUTH_BIND} +auth_bind_userdn = ${AUTH_BIND_USERDN} +ldap_version = ${LDAP_VERSION} +base = ${BASE} +deref = ${DEREF} +scope = ${SCOPE} +user_attrs = ${USER_ATTRS} +user_filter = ${USER_FILTER} +pass_attrs = ${PASS_ATTRS} +pass_filter = ${PASS_FILTER} +iterate_attrs = ${ITERATE_ATTRS} +iterate_filter = ${ITERATE_FILTER} +default_pass_scheme = ${DEFAULT_PASS_SCHEME} +blocking = ${BLOCKING} diff --git a/target/scripts/startup/setup.d/ldap.sh b/target/scripts/startup/setup.d/ldap.sh index 1451ec32..42115bbd 100644 --- a/target/scripts/startup/setup.d/ldap.sh +++ b/target/scripts/startup/setup.d/ldap.sh @@ -31,23 +31,10 @@ function _setup_ldap() { [[ -f ${FILE} ]] && _replace_by_env_in_file 'LDAP_' "${FILE}" done - _log 'trace' "Configuring Dovecot LDAP" - - declare -A DOVECOT_LDAP_MAPPING - - DOVECOT_LDAP_MAPPING['DOVECOT_BASE']="${DOVECOT_BASE:="${LDAP_SEARCH_BASE}"}" - DOVECOT_LDAP_MAPPING['DOVECOT_DN']="${DOVECOT_DN:="${LDAP_BIND_DN}"}" - DOVECOT_LDAP_MAPPING['DOVECOT_DNPASS']="${DOVECOT_DNPASS:="${LDAP_BIND_PW}"}" - DOVECOT_LDAP_MAPPING['DOVECOT_URIS']="${DOVECOT_URIS:="${LDAP_SERVER_HOST}"}" - + _log 'trace' "Configuring Dovecot for LDAP" # Default DOVECOT_PASS_FILTER to the same value as DOVECOT_USER_FILTER - DOVECOT_LDAP_MAPPING['DOVECOT_PASS_FILTER']="${DOVECOT_PASS_FILTER:="${DOVECOT_USER_FILTER}"}" - - for VAR in "${!DOVECOT_LDAP_MAPPING[@]}"; do - export "${VAR}=${DOVECOT_LDAP_MAPPING[${VAR}]}" - done - - _replace_by_env_in_file 'DOVECOT_' '/etc/dovecot/dovecot-ldap.conf.ext' + local DOVECOT_PASS_FILTER="${DOVECOT_PASS_FILTER:="${DOVECOT_USER_FILTER}"}" + _create_config_dovecot _log 'trace' 'Enabling Dovecot LDAP authentication' @@ -79,3 +66,14 @@ function _setup_ldap() { return 0 } + +# Generates a config from an ENV template while layering several other sources +# into a single temporary file, used as input into `_cleanse_config` which +# prepares the final output config. +function _create_config_dovecot() { + _cleanse_config '=' <(cat 2>/dev/null \ + <(_template_with_env 'LDAP_' /etc/dms/ldap/dovecot.base) \ + /tmp/docker-mailserver/ldap/dovecot.conf \ + <(_template_with_env 'DOVECOT_' /etc/dms/ldap/dovecot.tmpl) \ + ) > /etc/dovecot/dovecot-ldap.conf.ext +} From ea61a21259c404b42c20256b29312c0a2ac1d38a Mon Sep 17 00:00:00 2001 From: polarathene <5098581+polarathene@users.noreply.github.com> Date: Sun, 3 Sep 2023 18:49:24 +1200 Subject: [PATCH 05/15] refactor: Adapt Postfix LDAP config generation to use Config Template - `postfix.base` defaults are now potentially breaking: - The `query_filter` default is common between `ldap-users.cf` and `ldap-senders.cf`, but the `mailEnabled` attribute locks it in to requiring the `postfix-book` OpenLDAP schema. Like the `result_attribute` setting, this is only set as a convenient default but not as broadly useful like the `bind` + `version` settings. - `version = 3` is required as unlike SASLAuthd and Dovecot, the default for Postfix is `2`. - `bind = yes` because we only support configuring for this in DMS? - `ldap-senders.cf` originally differed with it's `result_attribute` setting, but that default chosen looks to be more of a workaround introduced and should be more explicit? - The Postfix `.base` template does not include the four common attributes (_that Dovecot and SASLAuthd base configs do_), as the `LDAP_` prefix is presently the same (no`POSTFIX_` prefix), thus would override user config regardless.. - `sender_login_maps.ldap` doesn't exist and isn't relevant to LDAP queries (seems to be accidentally included here). `ldap-senders.cf` provides this functionality. - `ldap-senders.cf` was not supporting copying over a user-provided config, it does now. - Internal location for these Postfix generated configs is now `/etc/postfix/ldap/`. --- target/features/ldap/postfix.base | 4 ++ target/features/ldap/postfix.tmpl | 35 +++++++++++++++ target/scripts/startup/setup.d/ldap.sh | 60 ++++++++++++++++---------- 3 files changed, 76 insertions(+), 23 deletions(-) create mode 100644 target/features/ldap/postfix.base create mode 100644 target/features/ldap/postfix.tmpl diff --git a/target/features/ldap/postfix.base b/target/features/ldap/postfix.base new file mode 100644 index 00000000..6623427b --- /dev/null +++ b/target/features/ldap/postfix.base @@ -0,0 +1,4 @@ +bind = yes +query_filter = (&(mail=%s)(mailEnabled=TRUE)) +result_attribute = mail +version = 3 diff --git a/target/features/ldap/postfix.tmpl b/target/features/ldap/postfix.tmpl new file mode 100644 index 00000000..4d48f31d --- /dev/null +++ b/target/features/ldap/postfix.tmpl @@ -0,0 +1,35 @@ +# Postfix LDAP table docs: http://www.postfix.org/ldap_table.5.html +server_host = ${SERVER_HOST} +server_port = ${SERVER_PORT} +timeout = ${TIMEOUT} +search_base = ${SEARCH_BASE} +query_filter = ${QUERY_FILTER} +result_format = ${RESULT_FORMAT} +domain = ${DOMAIN} +result_attribute = ${RESULT_ATTRIBUTE} +special_result_attribute = ${SPECIAL_RESULT_ATTRIBUTE} +terminal_result_attribute = ${TERMINAL_RESULT_ATTRIBUTE} +leaf_result_attribute = ${LEAF_RESULT_ATTRIBUTE} +scope = ${SCOPE} +bind = ${BIND} +bind_dn = ${BIND_DN} +bind_pw = ${BIND_PW} +recursion_limit = ${RECURSION_LIMIT} +expansion_limit = ${EXPANSION_LIMIT} +size_limit = ${SIZE_LIMIT} +dereference = ${DEREFERENCE} +chase_referrals = ${CHASE_REFERRALS} +version = ${VERSION} +debuglevel = ${DEBUGLEVEL} +sasl_mechs = ${SASL_MECHS} +sasl_realm = ${SASL_REALM} +sasl_authz_id = ${SASL_AUTHZ_ID} +sasl_minssf = ${SASL_MINSSF} +start_tls = ${START_TLS} +tls_ca_cert_dir = ${TLS_CA_CERT_DIR} +tls_ca_cert_file = ${TLS_CA_CERT_FILE} +tls_cert = ${TLS_CERT} +tls_key = ${TLS_KEY} +tls_require_cert = ${TLS_REQUIRE_CERT} +tls_random_file = ${TLS_RANDOM_FILE} +tls_cipher_suite = ${TLS_CIPHER_SUITE} diff --git a/target/scripts/startup/setup.d/ldap.sh b/target/scripts/startup/setup.d/ldap.sh index 42115bbd..9b037f3b 100644 --- a/target/scripts/startup/setup.d/ldap.sh +++ b/target/scripts/startup/setup.d/ldap.sh @@ -2,33 +2,36 @@ function _setup_ldap() { _log 'debug' 'Setting up LDAP' - _log 'trace' 'Checking for custom configs' - for i in 'users' 'groups' 'aliases' 'domains'; do - local FPATH="/tmp/docker-mailserver/ldap-${i}.cf" - if [[ -f ${FPATH} ]]; then - cp "${FPATH}" "/etc/postfix/ldap-${i}.cf" - fi - done + _log 'trace' "Configuring Postfix for LDAP" + mkdir -p /etc/postfix/ldap - _log 'trace' 'Starting to override configs' + # Generate Postfix LDAP configs: + for QUERY_KIND in 'users' 'groups' 'aliases' 'domains' 'senders'; do + # NOTE: Presently, only `query_filter` is supported for individually targeting: + case "${QUERY_KIND}" in + ( 'users' ) + export LDAP_QUERY_FILTER="${LDAP_QUERY_FILTER_USER}" + ;; - local FILES=( - /etc/postfix/ldap-users.cf - /etc/postfix/ldap-groups.cf - /etc/postfix/ldap-aliases.cf - /etc/postfix/ldap-domains.cf - /etc/postfix/ldap-senders.cf - /etc/postfix/maps/sender_login_maps.ldap - ) + ( 'groups' ) + export LDAP_QUERY_FILTER="${LDAP_QUERY_FILTER_GROUP}" + ;; - for FILE in "${FILES[@]}"; do - [[ ${FILE} =~ ldap-user ]] && export LDAP_QUERY_FILTER="${LDAP_QUERY_FILTER_USER}" - [[ ${FILE} =~ ldap-group ]] && export LDAP_QUERY_FILTER="${LDAP_QUERY_FILTER_GROUP}" - [[ ${FILE} =~ ldap-aliases ]] && export LDAP_QUERY_FILTER="${LDAP_QUERY_FILTER_ALIAS}" - [[ ${FILE} =~ ldap-domains ]] && export LDAP_QUERY_FILTER="${LDAP_QUERY_FILTER_DOMAIN}" - [[ ${FILE} =~ ldap-senders ]] && export LDAP_QUERY_FILTER="${LDAP_QUERY_FILTER_SENDERS}" - [[ -f ${FILE} ]] && _replace_by_env_in_file 'LDAP_' "${FILE}" + ( 'aliases' ) + export LDAP_QUERY_FILTER="${LDAP_QUERY_FILTER_ALIAS}" + ;; + + ( 'domains' ) + export LDAP_QUERY_FILTER="${LDAP_QUERY_FILTER_DOMAIN}" + ;; + + ( 'senders' ) + export LDAP_QUERY_FILTER="${LDAP_QUERY_FILTER_SENDERS}" + ;; + esac + + _create_config_postfix "${QUERY_KIND}" done _log 'trace' "Configuring Dovecot for LDAP" @@ -77,3 +80,14 @@ function _create_config_dovecot() { <(_template_with_env 'DOVECOT_' /etc/dms/ldap/dovecot.tmpl) \ ) > /etc/dovecot/dovecot-ldap.conf.ext } + +# NOTE: Only relies on the `LDAP_` prefix, presently assigned a `POSTFIX_` prefix. +function _create_config_postfix() { + local QUERY_KIND=${1} + + _cleanse_config '=' <(cat 2>/dev/null \ + /etc/dms/ldap/postfix.base \ + "/tmp/docker-mailserver/ldap-${QUERY_KIND}.cf" \ + <(_template_with_env 'LDAP_' /etc/dms/ldap/postfix.tmpl) \ + ) > "/etc/postfix/ldap-${QUERY_KIND}.cf" +} From 067341ab5532513751eb967da1c3d2f5aef0dfba Mon Sep 17 00:00:00 2001 From: polarathene <5098581+polarathene@users.noreply.github.com> Date: Sun, 3 Sep 2023 19:18:34 +1200 Subject: [PATCH 06/15] chore: `ldap-*cf` to `ldap/*.cf` Remove the prefix in favor of moving these files into a subdirectory: `/etc/postfix/ldap/`. Original LDAP config files provided via `Dockerfile` are now removed as the new `.tmpl` generation makes them redundant. --- Dockerfile | 11 +-------- target/dovecot/dovecot-ldap.conf.ext | 12 ---------- target/postfix/ldap-aliases.cf | 9 ------- target/postfix/ldap-domains.cf | 9 ------- target/postfix/ldap-groups.cf | 9 ------- target/postfix/ldap-senders.cf | 9 ------- target/postfix/ldap-users.cf | 9 ------- target/scripts/helpers/postfix.sh | 2 +- target/scripts/startup/setup.d/ldap.sh | 20 ++++++++-------- .../startup/setup.d/security/spoofing.sh | 4 ++-- test/tests/serial/mail_with_ldap.bats | 24 +++++++++---------- 11 files changed, 26 insertions(+), 92 deletions(-) delete mode 100644 target/dovecot/dovecot-ldap.conf.ext delete mode 100644 target/postfix/ldap-aliases.cf delete mode 100644 target/postfix/ldap-domains.cf delete mode 100644 target/postfix/ldap-groups.cf delete mode 100644 target/postfix/ldap-senders.cf delete mode 100644 target/postfix/ldap-users.cf diff --git a/Dockerfile b/Dockerfile index f6b40124..e28a58f0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -111,16 +111,7 @@ COPY target/rspamd/local.d/ /etc/rspamd/local.d/ # --- LDAP & SpamAssassin's Cron ---------------- # ----------------------------------------------- -COPY target/dovecot/dovecot-ldap.conf.ext /etc/dovecot -COPY \ - target/postfix/ldap-users.cf \ - target/postfix/ldap-groups.cf \ - target/postfix/ldap-aliases.cf \ - target/postfix/ldap-domains.cf \ - target/postfix/ldap-senders.cf \ - /etc/postfix/ - -# LDAP config support: +# LDAP config template support: COPY --link target/features/ldap/ /etc/dms/ldap/ # hadolint ignore=SC2016 diff --git a/target/dovecot/dovecot-ldap.conf.ext b/target/dovecot/dovecot-ldap.conf.ext deleted file mode 100644 index 66311106..00000000 --- a/target/dovecot/dovecot-ldap.conf.ext +++ /dev/null @@ -1,12 +0,0 @@ -base = ou=people,dc=example,dc=com -default_pass_scheme = SSHA -dn = cn=admin,dc=example,dc=com -dnpass = admin -uris = ldap://mail.example.com -tls = no -ldap_version = 3 -pass_attrs = uniqueIdentifier=user,userPassword=password -pass_filter = (&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n)) -user_attrs = mailHomeDirectory=home,mailUidNumber=uid,mailGidNumber=gid,mailStorageDirectory=mail -user_filter = (&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n)) -auth_bind = no diff --git a/target/postfix/ldap-aliases.cf b/target/postfix/ldap-aliases.cf deleted file mode 100644 index 5c8d443a..00000000 --- a/target/postfix/ldap-aliases.cf +++ /dev/null @@ -1,9 +0,0 @@ -bind = yes -bind_dn = cn=admin,dc=example,dc=com -bind_pw = admin -query_filter = (&(mailAlias=%s)(mailEnabled=TRUE)) -result_attribute = mail -search_base = ou=people,dc=example,dc=com -server_host = mail.example.com -start_tls = no -version = 3 diff --git a/target/postfix/ldap-domains.cf b/target/postfix/ldap-domains.cf deleted file mode 100644 index c118ebf6..00000000 --- a/target/postfix/ldap-domains.cf +++ /dev/null @@ -1,9 +0,0 @@ -bind = yes -bind_dn = cn=admin,dc=example,dc=com -bind_pw = admin -query_filter = (&(|(mail=*@%s)(mailalias=*@%s))(mailEnabled=TRUE)) -result_attribute = mail -search_base = ou=people,dc=example,dc=com -server_host = mail.example.com -start_tls = no -version = 3 diff --git a/target/postfix/ldap-groups.cf b/target/postfix/ldap-groups.cf deleted file mode 100644 index dc7fa14f..00000000 --- a/target/postfix/ldap-groups.cf +++ /dev/null @@ -1,9 +0,0 @@ -bind = yes -bind_dn = cn=admin,dc=example,dc=com -bind_pw = admin -query_filter = (&(mailGroupMember=%s)(mailEnabled=TRUE)) -result_attribute = mail -search_base = ou=people,dc=example,dc=com -server_host = mail.example.com -start_tls = no -version = 3 diff --git a/target/postfix/ldap-senders.cf b/target/postfix/ldap-senders.cf deleted file mode 100644 index 88a6cd87..00000000 --- a/target/postfix/ldap-senders.cf +++ /dev/null @@ -1,9 +0,0 @@ -bind = yes -bind_dn = cn=admin,dc=example,dc=com -bind_pw = admin -query_filter = (mail=%s) -result_attribute = mail, uid -search_base = ou=people,dc=example,dc=com -server_host = mail.example.com -start_tls = no -version = 3 diff --git a/target/postfix/ldap-users.cf b/target/postfix/ldap-users.cf deleted file mode 100644 index 943801f4..00000000 --- a/target/postfix/ldap-users.cf +++ /dev/null @@ -1,9 +0,0 @@ -bind = yes -bind_dn = cn=admin,dc=example,dc=com -bind_pw = admin -query_filter = (&(mail=%s)(mailEnabled=TRUE)) -result_attribute = mail -search_base = ou=people,dc=example,dc=com -server_host = mail.example.com -start_tls = no -version = 3 diff --git a/target/scripts/helpers/postfix.sh b/target/scripts/helpers/postfix.sh index 5fa4fa83..92758a0a 100644 --- a/target/scripts/helpers/postfix.sh +++ b/target/scripts/helpers/postfix.sh @@ -69,7 +69,7 @@ function _vhost_collect_postfix_domains() { # NOTE: `setup-stack.sh:_setup_ldap` has related logic: # - `main.cf:mydestination` setting removes `$mydestination` as an LDAP bugfix. # - `main.cf:virtual_mailbox_domains` uses `/etc/postfix/vhost`, but may -# conditionally include a 2nd table (ldap:/etc/postfix/ldap-domains.cf). +# conditionally include a 2nd table (ldap:/etc/postfix/ldap/domains.cf). function _vhost_ldap_support() { [[ ${ACCOUNT_PROVISIONER} == 'LDAP' ]] && echo "${DOMAINNAME}" >>"${TMP_VHOST}" } diff --git a/target/scripts/startup/setup.d/ldap.sh b/target/scripts/startup/setup.d/ldap.sh index 9b037f3b..2776aa5d 100644 --- a/target/scripts/startup/setup.d/ldap.sh +++ b/target/scripts/startup/setup.d/ldap.sh @@ -46,22 +46,22 @@ function _setup_ldap() { _log 'trace' "Configuring LDAP" - if [[ -f /etc/postfix/ldap-users.cf ]]; then - postconf 'virtual_mailbox_maps = ldap:/etc/postfix/ldap-users.cf' + if [[ -f /etc/postfix/ldap/users.cf ]]; then + postconf 'virtual_mailbox_maps = ldap:/etc/postfix/ldap/users.cf' else - _log 'warn' "'/etc/postfix/ldap-users.cf' not found" + _log 'warn' "'/etc/postfix/ldap/users.cf' not found" fi - if [[ -f /etc/postfix/ldap-domains.cf ]]; then - postconf 'virtual_mailbox_domains = /etc/postfix/vhost, ldap:/etc/postfix/ldap-domains.cf' + if [[ -f /etc/postfix/ldap/domains.cf ]]; then + postconf 'virtual_mailbox_domains = /etc/postfix/vhost, ldap:/etc/postfix/ldap/domains.cf' else - _log 'warn' "'/etc/postfix/ldap-domains.cf' not found" + _log 'warn' "'/etc/postfix/ldap/domains.cf' not found" fi - if [[ -f /etc/postfix/ldap-aliases.cf ]] && [[ -f /etc/postfix/ldap-groups.cf ]]; then - postconf 'virtual_alias_maps = ldap:/etc/postfix/ldap-aliases.cf, ldap:/etc/postfix/ldap-groups.cf' + if [[ -f /etc/postfix/ldap/aliases.cf ]] && [[ -f /etc/postfix/ldap/groups.cf ]]; then + postconf 'virtual_alias_maps = ldap:/etc/postfix/ldap/aliases.cf, ldap:/etc/postfix/ldap/groups.cf' else - _log 'warn' "'/etc/postfix/ldap-aliases.cf' and / or '/etc/postfix/ldap-groups.cf' not found" + _log 'warn' "'/etc/postfix/ldap/aliases.cf' and / or '/etc/postfix/ldap/groups.cf' not found" fi # shellcheck disable=SC2016 @@ -89,5 +89,5 @@ function _create_config_postfix() { /etc/dms/ldap/postfix.base \ "/tmp/docker-mailserver/ldap-${QUERY_KIND}.cf" \ <(_template_with_env 'LDAP_' /etc/dms/ldap/postfix.tmpl) \ - ) > "/etc/postfix/ldap-${QUERY_KIND}.cf" + ) > "/etc/postfix/ldap/${QUERY_KIND}.cf" } diff --git a/target/scripts/startup/setup.d/security/spoofing.sh b/target/scripts/startup/setup.d/security/spoofing.sh index ffefb279..1afe1d0c 100644 --- a/target/scripts/startup/setup.d/security/spoofing.sh +++ b/target/scripts/startup/setup.d/security/spoofing.sh @@ -6,9 +6,9 @@ function _setup_spoof_protection() { if [[ ${ACCOUNT_PROVISIONER} == 'LDAP' ]]; then if [[ -z ${LDAP_QUERY_FILTER_SENDERS} ]]; then - postconf 'smtpd_sender_login_maps = ldap:/etc/postfix/ldap-users.cf ldap:/etc/postfix/ldap-aliases.cf ldap:/etc/postfix/ldap-groups.cf' + postconf 'smtpd_sender_login_maps = ldap:/etc/postfix/ldap/users.cf ldap:/etc/postfix/ldap/aliases.cf ldap:/etc/postfix/ldap/groups.cf' else - postconf 'smtpd_sender_login_maps = ldap:/etc/postfix/ldap-senders.cf' + postconf 'smtpd_sender_login_maps = ldap:/etc/postfix/ldap/senders.cf' fi else # NOTE: This file is always created at startup, it potentially has content added. diff --git a/test/tests/serial/mail_with_ldap.bats b/test/tests/serial/mail_with_ldap.bats index f2011d22..aad18582 100644 --- a/test/tests/serial/mail_with_ldap.bats +++ b/test/tests/serial/mail_with_ldap.bats @@ -39,7 +39,7 @@ function setup_file() { # # LDAP filter queries explained. - # NOTE: All LDAP configs for Postfix (with the exception of `ldap-senders.cf`), return the `mail` attribute value of matched results. + # NOTE: All LDAP configs for Postfix (with the exception of `ldap/senders.cf`), return the `mail` attribute value of matched results. # This is through the config key `result_attribute`, which the ENV substitution feature can only replace across all configs, not selectively like `query_filter`. # NOTE: The queries below rely specifically upon attributes and classes defined by the schema `postfix-book.ldif`. These are not compatible with all LDAP setups. @@ -199,7 +199,7 @@ function teardown() { # REF: https://github.com/docker-mailserver/docker-mailserver/pull/642#issuecomment-313916384 # NOTE: This account has no `mailAlias` or `mailGroupMember` defined in it's `.ldif`. local MAIL_ACCOUNT="some.user.email@${FQDN_LOCALHOST_A}" - _run_in_container postmap -q "${MAIL_ACCOUNT}" ldap:/etc/postfix/ldap-users.cf + _run_in_container postmap -q "${MAIL_ACCOUNT}" ldap:/etc/postfix/ldap/users.cf assert_success assert_output "${MAIL_ACCOUNT}" } @@ -210,9 +210,9 @@ function teardown() { export CONTAINER_NAME=${CONTAINER3_NAME} local LDAP_CONFIGS_POSTFIX=( - /etc/postfix/ldap-users.cf - /etc/postfix/ldap-groups.cf - /etc/postfix/ldap-aliases.cf + /etc/postfix/ldap/users.cf + /etc/postfix/ldap/groups.cf + /etc/postfix/ldap/aliases.cf ) for LDAP_CONFIG in "${LDAP_CONFIGS_POSTFIX[@]}"; do @@ -232,15 +232,15 @@ function teardown() { for LDAP_SETTING in "${LDAP_SETTINGS_POSTFIX[@]}"; do # "${LDAP_SETTING%=*}" is to match only the key portion of the var (helpful for assert_output error messages) # NOTE: `start_tls = no` is a default setting, but the white-space differs when ENV `LDAP_START_TLS` is not set explicitly. - _run_in_container grep "${LDAP_SETTING%=*}" /etc/postfix/ldap-users.cf + _run_in_container grep "${LDAP_SETTING%=*}" /etc/postfix/ldap/users.cf assert_output "${LDAP_SETTING}" assert_success - _run_in_container grep "${LDAP_SETTING%=*}" /etc/postfix/ldap-groups.cf + _run_in_container grep "${LDAP_SETTING%=*}" /etc/postfix/ldap/groups.cf assert_output "${LDAP_SETTING}" assert_success - _run_in_container grep "${LDAP_SETTING%=*}" /etc/postfix/ldap-aliases.cf + _run_in_container grep "${LDAP_SETTING%=*}" /etc/postfix/ldap/aliases.cf assert_output "${LDAP_SETTING}" assert_success done @@ -401,20 +401,20 @@ function _should_exist_in_ldap_tables() { local DOMAIN_PART="${MAIL_ACCOUNT#*@}" # Each LDAP config file sets `query_filter` to lookup a key in LDAP (values defined in `.ldif` test files) - # `mail` (ldap-users), `mailAlias` (ldap-aliases), `mailGroupMember` (ldap-groups) + # `mail` (ldap/users.cf), `mailAlias` (ldap/aliases.cf), `mailGroupMember` (ldap/groups.cf) # `postmap` is queried with the mail account address, and the LDAP service should respond with # `result_attribute` which is the LDAP `mail` value (should match what we'r'e quering `postmap` with) - _run_in_container postmap -q "${MAIL_ACCOUNT}" ldap:/etc/postfix/ldap-users.cf + _run_in_container postmap -q "${MAIL_ACCOUNT}" ldap:/etc/postfix/ldap/users.cf assert_success assert_output "${MAIL_ACCOUNT}" # Check which account has the `postmaster` virtual alias: - _run_in_container postmap -q "postmaster@${DOMAIN_PART}" ldap:/etc/postfix/ldap-aliases.cf + _run_in_container postmap -q "postmaster@${DOMAIN_PART}" ldap:/etc/postfix/ldap/aliases.cf assert_success assert_output "${MAIL_ACCOUNT}" - _run_in_container postmap -q "employees@${DOMAIN_PART}" ldap:/etc/postfix/ldap-groups.cf + _run_in_container postmap -q "employees@${DOMAIN_PART}" ldap:/etc/postfix/ldap/groups.cf assert_success assert_output "${MAIL_ACCOUNT}" } From 8fe744ffd1565524f230d3201639d57f6eb19890 Mon Sep 17 00:00:00 2001 From: polarathene <5098581+polarathene@users.noreply.github.com> Date: Sun, 3 Sep 2023 21:24:45 +1200 Subject: [PATCH 07/15] fix: Postfix LDAP configs `main.cf` support opt-out instead Generic approach to configuring Postfix with the LDAP tables in `main.cf`. Instead of opt-in when file exists (always did), opt-out when the `query_filter` is missing. This fixes a bug reported when deployment does not require LDAP queries for a lookup type, like groups. Avoids introducing misconfiguration by default. --- target/scripts/startup/setup.d/ldap.sh | 38 +++++++++++--------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/target/scripts/startup/setup.d/ldap.sh b/target/scripts/startup/setup.d/ldap.sh index 2776aa5d..39db49fa 100644 --- a/target/scripts/startup/setup.d/ldap.sh +++ b/target/scripts/startup/setup.d/ldap.sh @@ -4,9 +4,15 @@ function _setup_ldap() { _log 'debug' 'Setting up LDAP' _log 'trace' "Configuring Postfix for LDAP" - mkdir -p /etc/postfix/ldap + + # Configure Postfix settings for LDAP configs in advance: + postconf \ + 'virtual_mailbox_maps = ldap:/etc/postfix/ldap/users.cf' \ + 'virtual_mailbox_domains = /etc/postfix/vhost ldap:/etc/postfix/ldap/domains.cf' \ + 'virtual_alias_maps = ldap:/etc/postfix/ldap/aliases.cf ldap:/etc/postfix/ldap/groups.cf' # Generate Postfix LDAP configs: + mkdir -p /etc/postfix/ldap for QUERY_KIND in 'users' 'groups' 'aliases' 'domains' 'senders'; do # NOTE: Presently, only `query_filter` is supported for individually targeting: case "${QUERY_KIND}" in @@ -44,26 +50,6 @@ function _setup_ldap() { sed -i -e '/\!include auth-ldap\.conf\.ext/s/^#//' /etc/dovecot/conf.d/10-auth.conf sed -i -e '/\!include auth-passwdfile\.inc/s/^/#/' /etc/dovecot/conf.d/10-auth.conf - _log 'trace' "Configuring LDAP" - - if [[ -f /etc/postfix/ldap/users.cf ]]; then - postconf 'virtual_mailbox_maps = ldap:/etc/postfix/ldap/users.cf' - else - _log 'warn' "'/etc/postfix/ldap/users.cf' not found" - fi - - if [[ -f /etc/postfix/ldap/domains.cf ]]; then - postconf 'virtual_mailbox_domains = /etc/postfix/vhost, ldap:/etc/postfix/ldap/domains.cf' - else - _log 'warn' "'/etc/postfix/ldap/domains.cf' not found" - fi - - if [[ -f /etc/postfix/ldap/aliases.cf ]] && [[ -f /etc/postfix/ldap/groups.cf ]]; then - postconf 'virtual_alias_maps = ldap:/etc/postfix/ldap/aliases.cf, ldap:/etc/postfix/ldap/groups.cf' - else - _log 'warn' "'/etc/postfix/ldap/aliases.cf' and / or '/etc/postfix/ldap/groups.cf' not found" - fi - # shellcheck disable=SC2016 sed -i 's|mydestination = \$myhostname, |mydestination = |' /etc/postfix/main.cf @@ -84,10 +70,18 @@ function _create_config_dovecot() { # NOTE: Only relies on the `LDAP_` prefix, presently assigned a `POSTFIX_` prefix. function _create_config_postfix() { local QUERY_KIND=${1} + local LDAP_CONFIG_FILE="/etc/postfix/ldap/${QUERY_KIND}.cf" _cleanse_config '=' <(cat 2>/dev/null \ /etc/dms/ldap/postfix.base \ "/tmp/docker-mailserver/ldap-${QUERY_KIND}.cf" \ <(_template_with_env 'LDAP_' /etc/dms/ldap/postfix.tmpl) \ - ) > "/etc/postfix/ldap/${QUERY_KIND}.cf" + ) > "${LDAP_CONFIG_FILE}" + + # Opt-out of generated config if `query_filter` was not configured: + if ! grep --silent '^query_filter =' "${LDAP_CONFIG_FILE}"; then + _log 'warn' "'${LDAP_CONFIG_FILE}' is missing the 'query_filter' setting - disabling" + + sed -i "s/$(_escape_for_sed <<< ${LDAP_CONFIG_FILE})//" /etc/postfix/main.cf + fi } From 1ec18535285ff3fda3526a0b075ce76e1c0d90bd Mon Sep 17 00:00:00 2001 From: polarathene <5098581+polarathene@users.noreply.github.com> Date: Thu, 5 Oct 2023 12:31:59 +1300 Subject: [PATCH 08/15] feat: Support ENV override on individual Postfix LDAP config files Previously only `query_filter` had this support via an inconsistent `_${QUERY_KIND}` ENV suffix. This has been shifted to the left under the new `POSTFIX_` prefix, so that it can easily leverage the ENV prefix with config templates, layering after the generic `POSTFIX_` template. Naming is now consistent with `${QUERY_KIND}` (upper-cased). This also enables using the common `LDAP_` prefix in the Postfix `.base` template. As a part of the previous commit toggling based on presence of `query_filter`, this is now dropped from the Postfix `.base` template. --- target/features/ldap/postfix.base | 5 ++++- target/scripts/startup/setup.d/ldap.sh | 29 +++----------------------- test/tests/serial/mail_with_ldap.bats | 27 ++++++++++++------------ 3 files changed, 20 insertions(+), 41 deletions(-) diff --git a/target/features/ldap/postfix.base b/target/features/ldap/postfix.base index 6623427b..72bbee08 100644 --- a/target/features/ldap/postfix.base +++ b/target/features/ldap/postfix.base @@ -1,4 +1,7 @@ +bind_dn = ${BIND_DN} +bind_pw = ${BIND_PW} +server_host = ${SERVER_HOST} +search_base = ${SEARCH_BASE} bind = yes -query_filter = (&(mail=%s)(mailEnabled=TRUE)) result_attribute = mail version = 3 diff --git a/target/scripts/startup/setup.d/ldap.sh b/target/scripts/startup/setup.d/ldap.sh index 39db49fa..f4ffb36e 100644 --- a/target/scripts/startup/setup.d/ldap.sh +++ b/target/scripts/startup/setup.d/ldap.sh @@ -14,29 +14,6 @@ function _setup_ldap() { # Generate Postfix LDAP configs: mkdir -p /etc/postfix/ldap for QUERY_KIND in 'users' 'groups' 'aliases' 'domains' 'senders'; do - # NOTE: Presently, only `query_filter` is supported for individually targeting: - case "${QUERY_KIND}" in - ( 'users' ) - export LDAP_QUERY_FILTER="${LDAP_QUERY_FILTER_USER}" - ;; - - ( 'groups' ) - export LDAP_QUERY_FILTER="${LDAP_QUERY_FILTER_GROUP}" - ;; - - ( 'aliases' ) - export LDAP_QUERY_FILTER="${LDAP_QUERY_FILTER_ALIAS}" - ;; - - ( 'domains' ) - export LDAP_QUERY_FILTER="${LDAP_QUERY_FILTER_DOMAIN}" - ;; - - ( 'senders' ) - export LDAP_QUERY_FILTER="${LDAP_QUERY_FILTER_SENDERS}" - ;; - esac - _create_config_postfix "${QUERY_KIND}" done @@ -67,15 +44,15 @@ function _create_config_dovecot() { ) > /etc/dovecot/dovecot-ldap.conf.ext } -# NOTE: Only relies on the `LDAP_` prefix, presently assigned a `POSTFIX_` prefix. function _create_config_postfix() { local QUERY_KIND=${1} local LDAP_CONFIG_FILE="/etc/postfix/ldap/${QUERY_KIND}.cf" _cleanse_config '=' <(cat 2>/dev/null \ - /etc/dms/ldap/postfix.base \ + <(_template_with_env 'LDAP_' /etc/dms/ldap/postfix.base) \ "/tmp/docker-mailserver/ldap-${QUERY_KIND}.cf" \ - <(_template_with_env 'LDAP_' /etc/dms/ldap/postfix.tmpl) \ + <(_template_with_env 'POSTFIX_' /etc/dms/ldap/postfix.tmpl) \ + <(_template_with_env "POSTFIX_${QUERY_KIND^^}_" /etc/dms/ldap/postfix.tmpl) \ ) > "${LDAP_CONFIG_FILE}" # Opt-out of generated config if `query_filter` was not configured: diff --git a/test/tests/serial/mail_with_ldap.bats b/test/tests/serial/mail_with_ldap.bats index aad18582..2116613d 100644 --- a/test/tests/serial/mail_with_ldap.bats +++ b/test/tests/serial/mail_with_ldap.bats @@ -39,25 +39,24 @@ function setup_file() { # # LDAP filter queries explained. - # NOTE: All LDAP configs for Postfix (with the exception of `ldap/senders.cf`), return the `mail` attribute value of matched results. - # This is through the config key `result_attribute`, which the ENV substitution feature can only replace across all configs, not selectively like `query_filter`. + # NOTE: All LDAP configs use `result_attribute = mail` for Postfix to return the `mail` attribute value from query matched results. # NOTE: The queries below rely specifically upon attributes and classes defined by the schema `postfix-book.ldif`. These are not compatible with all LDAP setups. - # `mailAlias`` is supported by both classes provided from the schema `postfix-book.ldif`, but `mailEnabled` is only available to `PostfixBookMailAccount` class: - local QUERY_ALIAS='(&(mailAlias=%s) (| (objectClass=PostfixBookMailForward) (&(objectClass=PostfixBookMailAccount)(mailEnabled=TRUE)) ))' + # `mailAlias` is supported by both classes provided from the schema `postfix-book.ldif`, but `mailEnabled` is only available to `PostfixBookMailAccount` class: + local QUERY_ALIASES='(&(mailAlias=%s) (| (objectClass=PostfixBookMailForward) (&(objectClass=PostfixBookMailAccount)(mailEnabled=TRUE)) ))' # Postfix does domain lookups with the domain of the recipient to check if DMS manages the mail domain. # For this lookup `%s` only represents the domain, not a full email address. Hence the match pattern using a wildcard prefix `*@`. - # For a breakdown, see QUERY_SENDERS comment. + # For a breakdown, see the `QUERY_SENDERS` comment. # NOTE: Although `result_attribute = mail` will return each accounts full email address, Postfix will only compare to domain-part. - local QUERY_DOMAIN='(| (& (|(mail=*@%s) (mailAlias=*@%s) (mailGroupMember=*@%s)) (&(objectClass=PostfixBookMailAccount)(mailEnabled=TRUE)) ) (&(mailAlias=*@%s)(objectClass=PostfixBookMailForward)) )' + local QUERY_DOMAINS='(| (& (|(mail=*@%s) (mailAlias=*@%s) (mailGroupMember=*@%s)) (&(objectClass=PostfixBookMailAccount)(mailEnabled=TRUE)) ) (&(mailAlias=*@%s)(objectClass=PostfixBookMailForward)) )' # Simple queries for a single attribute that additionally requires `mailEnabled=TRUE` from the `PostfixBookMailAccount` class: # NOTE: `mail` attribute is not unique to `PostfixBookMailAccount`. The `mailEnabled` attribute is to further control valid mail accounts. # TODO: For tests, since `mailEnabled` is not relevant (always configured as TRUE currently), # a simpler query like `mail=%s` or `mailGroupMember=%s` would be sufficient. The additional constraints could be covered in our docs instead. - local QUERY_GROUP='(&(mailGroupMember=%s) (&(objectClass=PostfixBookMailAccount)(mailEnabled=TRUE)) )' - local QUERY_USER='(&(mail=%s) (&(objectClass=PostfixBookMailAccount)(mailEnabled=TRUE)) )' + local QUERY_GROUPS='(&(mailGroupMember=%s) (&(objectClass=PostfixBookMailAccount)(mailEnabled=TRUE)) )' + local QUERY_USERS='(&(mail=%s) (&(objectClass=PostfixBookMailAccount)(mailEnabled=TRUE)) )' # Given the sender address `%s` from Postfix, query LDAP for accounts that meet the search filter, # the `result_attribute` is `mail` + `uid` (`userID`) attributes for login names that are authorized to use that sender address. @@ -93,7 +92,7 @@ function setup_file() { --env ACCOUNT_PROVISIONER=LDAP # Common LDAP ENV: - # NOTE: `scripts/startup/setup.d/ldap.sh:_setup_ldap()` uses `_replace_by_env_in_file()` to configure settings (stripping `DOVECOT_` / `LDAP_` prefixes): + # NOTE: `scripts/startup/setup.d/ldap.sh:_setup_ldap()` uses helper methods to generate / override LDAP configs (grouped by common ENV prefixes): --env LDAP_SERVER_HOST="ldap://${FQDN_LDAP}" --env LDAP_SEARCH_BASE='ou=users,dc=example,dc=test' --env LDAP_START_TLS=no @@ -113,11 +112,11 @@ function setup_file() { --env DOVECOT_TLS=no # Postfix: - --env LDAP_QUERY_FILTER_ALIAS="${QUERY_ALIAS}" - --env LDAP_QUERY_FILTER_DOMAIN="${QUERY_DOMAIN}" - --env LDAP_QUERY_FILTER_GROUP="${QUERY_GROUP}" - --env LDAP_QUERY_FILTER_SENDERS="${QUERY_SENDERS}" - --env LDAP_QUERY_FILTER_USER="${QUERY_USER}" + --env POSTFIX_ALIASES_QUERY_FILTER="${QUERY_ALIASES}" + --env POSTFIX_DOMAINS_QUERY_FILTER="${QUERY_DOMAINS}" + --env POSTFIX_GROUPS_QUERY_FILTER="${QUERY_GROUPS}" + --env POSTFIX_SENDERS_QUERY_FILTER="${QUERY_SENDERS}" + --env POSTFIX_USERS_QUERY_FILTER="${QUERY_USERS}" ) # Extra ENV needed to support specific test-cases: From 4968f4a51dd0d584c78f05e84530b314508e4790 Mon Sep 17 00:00:00 2001 From: polarathene <5098581+polarathene@users.noreply.github.com> Date: Mon, 4 Sep 2023 18:11:08 +1200 Subject: [PATCH 09/15] tests: Fix white-space difference - `packages.sh` + `utils.sh` lint fix. - `.tmpl` + `.base` files column aligned. - `mail_with_ldap.bats` updated to ignore white-space between key/value entries being checked. --- target/features/ldap/dovecot.tmpl | 60 +++++++++++------------ target/features/ldap/postfix.tmpl | 66 +++++++++++++------------- target/features/ldap/saslauthd.base | 10 ++-- target/features/ldap/saslauthd.tmpl | 68 +++++++++++++-------------- target/scripts/build/packages.sh | 3 +- target/scripts/helpers/utils.sh | 2 +- test/tests/serial/mail_with_ldap.bats | 38 ++++++++------- 7 files changed, 127 insertions(+), 120 deletions(-) diff --git a/target/features/ldap/dovecot.tmpl b/target/features/ldap/dovecot.tmpl index c95c399f..a44ee80e 100644 --- a/target/features/ldap/dovecot.tmpl +++ b/target/features/ldap/dovecot.tmpl @@ -1,32 +1,32 @@ # Dovecot LDAP config docs: https://github.com/dovecot/core/blob/bbb600e46ca650a3a5ef812ea3a1e8c45a6ea0ba/doc/example-config/dovecot-ldap.conf.ext -hosts = ${HOSTS} -uris = ${URIS} -dn = ${DN} -dnpass = ${DNPASS} -sasl_bind = ${SASL_BIND} -sasl_mech = ${SASL_MECH} -sasl_realm = ${SASL_REALM} -sasl_authz_id = ${SASL_AUTHZ_ID} -tls = ${TLS} -tls_ca_cert_file = ${TLS_CA_CERT_FILE} -tls_ca_cert_dir = ${TLS_CA_CERT_DIR} -tls_cipher_suite = ${TLS_CIPHER_SUITE} -tls_cert_file = ${TLS_CERT_FILE} -tls_key_file = ${TLS_KEY_FILE} -tls_require_cert = ${TLS_REQUIRE_CERT} -ldaprc_path = ${LDAPRC_PATH} -debug_level = ${DEBUG_LEVEL} -auth_bind = ${AUTH_BIND} -auth_bind_userdn = ${AUTH_BIND_USERDN} -ldap_version = ${LDAP_VERSION} -base = ${BASE} -deref = ${DEREF} -scope = ${SCOPE} -user_attrs = ${USER_ATTRS} -user_filter = ${USER_FILTER} -pass_attrs = ${PASS_ATTRS} -pass_filter = ${PASS_FILTER} -iterate_attrs = ${ITERATE_ATTRS} -iterate_filter = ${ITERATE_FILTER} +hosts = ${HOSTS} +uris = ${URIS} +dn = ${DN} +dnpass = ${DNPASS} +sasl_bind = ${SASL_BIND} +sasl_mech = ${SASL_MECH} +sasl_realm = ${SASL_REALM} +sasl_authz_id = ${SASL_AUTHZ_ID} +tls = ${TLS} +tls_ca_cert_file = ${TLS_CA_CERT_FILE} +tls_ca_cert_dir = ${TLS_CA_CERT_DIR} +tls_cipher_suite = ${TLS_CIPHER_SUITE} +tls_cert_file = ${TLS_CERT_FILE} +tls_key_file = ${TLS_KEY_FILE} +tls_require_cert = ${TLS_REQUIRE_CERT} +ldaprc_path = ${LDAPRC_PATH} +debug_level = ${DEBUG_LEVEL} +auth_bind = ${AUTH_BIND} +auth_bind_userdn = ${AUTH_BIND_USERDN} +ldap_version = ${LDAP_VERSION} +base = ${BASE} +deref = ${DEREF} +scope = ${SCOPE} +user_attrs = ${USER_ATTRS} +user_filter = ${USER_FILTER} +pass_attrs = ${PASS_ATTRS} +pass_filter = ${PASS_FILTER} +iterate_attrs = ${ITERATE_ATTRS} +iterate_filter = ${ITERATE_FILTER} default_pass_scheme = ${DEFAULT_PASS_SCHEME} -blocking = ${BLOCKING} +blocking = ${BLOCKING} diff --git a/target/features/ldap/postfix.tmpl b/target/features/ldap/postfix.tmpl index 4d48f31d..682d8e27 100644 --- a/target/features/ldap/postfix.tmpl +++ b/target/features/ldap/postfix.tmpl @@ -1,35 +1,35 @@ # Postfix LDAP table docs: http://www.postfix.org/ldap_table.5.html -server_host = ${SERVER_HOST} -server_port = ${SERVER_PORT} -timeout = ${TIMEOUT} -search_base = ${SEARCH_BASE} -query_filter = ${QUERY_FILTER} -result_format = ${RESULT_FORMAT} -domain = ${DOMAIN} -result_attribute = ${RESULT_ATTRIBUTE} -special_result_attribute = ${SPECIAL_RESULT_ATTRIBUTE} +server_host = ${SERVER_HOST} +server_port = ${SERVER_PORT} +timeout = ${TIMEOUT} +search_base = ${SEARCH_BASE} +query_filter = ${QUERY_FILTER} +result_format = ${RESULT_FORMAT} +domain = ${DOMAIN} +result_attribute = ${RESULT_ATTRIBUTE} +special_result_attribute = ${SPECIAL_RESULT_ATTRIBUTE} terminal_result_attribute = ${TERMINAL_RESULT_ATTRIBUTE} -leaf_result_attribute = ${LEAF_RESULT_ATTRIBUTE} -scope = ${SCOPE} -bind = ${BIND} -bind_dn = ${BIND_DN} -bind_pw = ${BIND_PW} -recursion_limit = ${RECURSION_LIMIT} -expansion_limit = ${EXPANSION_LIMIT} -size_limit = ${SIZE_LIMIT} -dereference = ${DEREFERENCE} -chase_referrals = ${CHASE_REFERRALS} -version = ${VERSION} -debuglevel = ${DEBUGLEVEL} -sasl_mechs = ${SASL_MECHS} -sasl_realm = ${SASL_REALM} -sasl_authz_id = ${SASL_AUTHZ_ID} -sasl_minssf = ${SASL_MINSSF} -start_tls = ${START_TLS} -tls_ca_cert_dir = ${TLS_CA_CERT_DIR} -tls_ca_cert_file = ${TLS_CA_CERT_FILE} -tls_cert = ${TLS_CERT} -tls_key = ${TLS_KEY} -tls_require_cert = ${TLS_REQUIRE_CERT} -tls_random_file = ${TLS_RANDOM_FILE} -tls_cipher_suite = ${TLS_CIPHER_SUITE} +leaf_result_attribute = ${LEAF_RESULT_ATTRIBUTE} +scope = ${SCOPE} +bind = ${BIND} +bind_dn = ${BIND_DN} +bind_pw = ${BIND_PW} +recursion_limit = ${RECURSION_LIMIT} +expansion_limit = ${EXPANSION_LIMIT} +size_limit = ${SIZE_LIMIT} +dereference = ${DEREFERENCE} +chase_referrals = ${CHASE_REFERRALS} +version = ${VERSION} +debuglevel = ${DEBUGLEVEL} +sasl_mechs = ${SASL_MECHS} +sasl_realm = ${SASL_REALM} +sasl_authz_id = ${SASL_AUTHZ_ID} +sasl_minssf = ${SASL_MINSSF} +start_tls = ${START_TLS} +tls_ca_cert_dir = ${TLS_CA_CERT_DIR} +tls_ca_cert_file = ${TLS_CA_CERT_FILE} +tls_cert = ${TLS_CERT} +tls_key = ${TLS_KEY} +tls_require_cert = ${TLS_REQUIRE_CERT} +tls_random_file = ${TLS_RANDOM_FILE} +tls_cipher_suite = ${TLS_CIPHER_SUITE} diff --git a/target/features/ldap/saslauthd.base b/target/features/ldap/saslauthd.base index 4d92c33e..508c6e24 100644 --- a/target/features/ldap/saslauthd.base +++ b/target/features/ldap/saslauthd.base @@ -1,6 +1,6 @@ -ldap_bind_dn: ${BIND_DN} -ldap_bind_pw: ${BIND_PW} -ldap_servers: ${SERVER_HOST} +ldap_bind_dn: ${BIND_DN} +ldap_bind_pw: ${BIND_PW} +ldap_servers: ${SERVER_HOST} ldap_search_base: ${SEARCH_BASE} -ldap_filter: (&(uniqueIdentifier=%u)(mailEnabled=TRUE)) -ldap_referrals: yes +ldap_filter: (&(uniqueIdentifier=%u)(mailEnabled=TRUE)) +ldap_referrals: yes diff --git a/target/features/ldap/saslauthd.tmpl b/target/features/ldap/saslauthd.tmpl index 016bf1a0..f07c985d 100644 --- a/target/features/ldap/saslauthd.tmpl +++ b/target/features/ldap/saslauthd.tmpl @@ -1,36 +1,36 @@ # Parameter docs: https://github.com/cyrusimap/cyrus-sasl/blob/3959d45aa187d906d5fb3e8edf7e3661780967a5/saslauthd/LDAP_SASLAUTHD#L85-L242 -ldap_auth_method: ${LDAP_AUTH_METHOD} -ldap_bind_dn: ${LDAP_BIND_DN} -ldap_bind_pw: ${LDAP_BIND_PW} -ldap_default_domain: ${LDAP_DEFAULT_DOMAIN} -ldap_default_realm: ${LDAP_DEFAULT_REALM} -ldap_deref: ${LDAP_DEREF} -ldap_filter: ${LDAP_FILTER} -ldap_group_attr: ${LDAP_GROUP_ATTR} -ldap_group_dn: ${LDAP_GROUP_DN} -ldap_group_filter: ${LDAP_GROUP_FILTER} +ldap_auth_method: ${LDAP_AUTH_METHOD} +ldap_bind_dn: ${LDAP_BIND_DN} +ldap_bind_pw: ${LDAP_BIND_PW} +ldap_default_domain: ${LDAP_DEFAULT_DOMAIN} +ldap_default_realm: ${LDAP_DEFAULT_REALM} +ldap_deref: ${LDAP_DEREF} +ldap_filter: ${LDAP_FILTER} +ldap_group_attr: ${LDAP_GROUP_ATTR} +ldap_group_dn: ${LDAP_GROUP_DN} +ldap_group_filter: ${LDAP_GROUP_FILTER} ldap_group_match_method: ${LDAP_GROUP_MATCH_METHOD} -ldap_group_search_base: ${LDAP_GROUP_SEARCH_BASE} -ldap_group_scope: ${LDAP_GROUP_SCOPE} -ldap_password: ${LDAP_PASSWORD} -ldap_password_attr: ${LDAP_PASSWORD_ATTR} -ldap_referrals: ${LDAP_REFERRALS} -ldap_restart: ${LDAP_RESTART} -ldap_id: ${LDAP_ID} -ldap_authz_id: ${LDAP_AUTHZ_ID} -ldap_mech: ${LDAP_MECH} -ldap_realm: ${LDAP_REALM} -ldap_scope: ${LDAP_SCOPE} -ldap_search_base: ${LDAP_SEARCH_BASE} -ldap_servers: ${LDAP_SERVERS} -ldap_start_tls: ${LDAP_START_TLS} -ldap_time_limit: ${LDAP_TIME_LIMIT} -ldap_timeout: ${LDAP_TIMEOUT} -ldap_tls_check_peer: ${LDAP_TLS_CHECK_PEER} -ldap_tls_cacert_file: ${LDAP_TLS_CACERT_FILE} -ldap_tls_cacert_dir: ${LDAP_TLS_CACERT_DIR} -ldap_tls_ciphers: ${LDAP_TLS_CIPHERS} -ldap_tls_cert: ${LDAP_TLS_CERT} -ldap_tls_key: ${LDAP_TLS_KEY} -ldap_use_sasl: ${LDAP_USE_SASL} -ldap_version: ${LDAP_VERSION} +ldap_group_search_base: ${LDAP_GROUP_SEARCH_BASE} +ldap_group_scope: ${LDAP_GROUP_SCOPE} +ldap_password: ${LDAP_PASSWORD} +ldap_password_attr: ${LDAP_PASSWORD_ATTR} +ldap_referrals: ${LDAP_REFERRALS} +ldap_restart: ${LDAP_RESTART} +ldap_id: ${LDAP_ID} +ldap_authz_id: ${LDAP_AUTHZ_ID} +ldap_mech: ${LDAP_MECH} +ldap_realm: ${LDAP_REALM} +ldap_scope: ${LDAP_SCOPE} +ldap_search_base: ${LDAP_SEARCH_BASE} +ldap_servers: ${LDAP_SERVERS} +ldap_start_tls: ${LDAP_START_TLS} +ldap_time_limit: ${LDAP_TIME_LIMIT} +ldap_timeout: ${LDAP_TIMEOUT} +ldap_tls_check_peer: ${LDAP_TLS_CHECK_PEER} +ldap_tls_cacert_file: ${LDAP_TLS_CACERT_FILE} +ldap_tls_cacert_dir: ${LDAP_TLS_CACERT_DIR} +ldap_tls_ciphers: ${LDAP_TLS_CIPHERS} +ldap_tls_cert: ${LDAP_TLS_CERT} +ldap_tls_key: ${LDAP_TLS_KEY} +ldap_use_sasl: ${LDAP_USE_SASL} +ldap_version: ${LDAP_VERSION} diff --git a/target/scripts/build/packages.sh b/target/scripts/build/packages.sh index f9726fb2..f6f9c638 100644 --- a/target/scripts/build/packages.sh +++ b/target/scripts/build/packages.sh @@ -99,7 +99,8 @@ function _install_feature_config_templates() { apt-get "${QUIET}" --no-install-recommends install gettext-base # zenv: - local URL_ZENV="https://github.com/numToStr/zenv/releases/download/0.8.0/zenv-0.8.0-$(uname --machine)-unknown-linux-gnu.tar.gz" + local URL_ZENV + URL_ZENV="https://github.com/numToStr/zenv/releases/download/0.8.0/zenv-0.8.0-$(uname --machine)-unknown-linux-gnu.tar.gz" # Download from GH releases to stdout, then extract the zenv file to make available via PATH: curl -L "${URL_ZENV}" -o - | tar --gzip --extract --directory /usr/local/bin --file - zenv } diff --git a/target/scripts/helpers/utils.sh b/target/scripts/helpers/utils.sh index 29afd135..91900fc6 100644 --- a/target/scripts/helpers/utils.sh +++ b/target/scripts/helpers/utils.sh @@ -186,6 +186,6 @@ function _cleanse_config() { local KV_DELIMITER=${1:?KV Delimiter is required} local INPUT_FILE=${2?:Input file is required} - sed "/^[^${KV_DELIMITER}]*${KV_DELIMITER}\s*$/d" ${INPUT_FILE} \ + sed "/^[^${KV_DELIMITER}]*${KV_DELIMITER}\s*$/d" "${INPUT_FILE}" \ | tac | sort -u -t"${KV_DELIMITER}" -k1,1 } diff --git a/test/tests/serial/mail_with_ldap.bats b/test/tests/serial/mail_with_ldap.bats index 2116613d..771a18ae 100644 --- a/test/tests/serial/mail_with_ldap.bats +++ b/test/tests/serial/mail_with_ldap.bats @@ -229,19 +229,9 @@ function teardown() { ) for LDAP_SETTING in "${LDAP_SETTINGS_POSTFIX[@]}"; do - # "${LDAP_SETTING%=*}" is to match only the key portion of the var (helpful for assert_output error messages) - # NOTE: `start_tls = no` is a default setting, but the white-space differs when ENV `LDAP_START_TLS` is not set explicitly. - _run_in_container grep "${LDAP_SETTING%=*}" /etc/postfix/ldap/users.cf - assert_output "${LDAP_SETTING}" - assert_success - - _run_in_container grep "${LDAP_SETTING%=*}" /etc/postfix/ldap/groups.cf - assert_output "${LDAP_SETTING}" - assert_success - - _run_in_container grep "${LDAP_SETTING%=*}" /etc/postfix/ldap/aliases.cf - assert_output "${LDAP_SETTING}" - assert_success + _should_have_matching_setting "${LDAP_SETTING}" /etc/postfix/ldap/users.cf + _should_have_matching_setting "${LDAP_SETTING}" /etc/postfix/ldap/groups.cf + _should_have_matching_setting "${LDAP_SETTING}" /etc/postfix/ldap/aliases.cf done } @@ -269,9 +259,7 @@ function teardown() { ) for LDAP_SETTING in "${LDAP_SETTINGS_DOVECOT[@]}"; do - _run_in_container grep "${LDAP_SETTING%=*}" /etc/dovecot/dovecot-ldap.conf.ext - assert_output "${LDAP_SETTING}" - assert_success + _should_have_matching_setting "${LDAP_SETTING}" /etc/dovecot/dovecot-ldap.conf.ext done } @@ -437,3 +425,21 @@ function _should_successfully_deliver_mail_to() { # NOTE: Prevents compatibility for running testcases in parallel (for same container) when the count could become racey: _count_files_in_directory_in_container "${MAIL_STORAGE_RECIPIENT}" 1 } + +function _should_have_matching_setting() { + local KEY_VALUE=${1} + local CONFIG_FILE=${2} + + function __trim_whitespace() { + sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' <<< "${1}" + } + + local KEY VALUE + # Split string into key/value vars and trim white-space: + KEY=$(__trim_whitespace "${KEY_VALUE%=*}") + VALUE=$(__trim_whitespace "${KEY_VALUE#*=}") + + _run_in_container grep "${KEY}" "${CONFIG_FILE}" + assert_output --regexp "^${KEY}\s*=\s*${VALUE}$" + assert_success +} From 6bc4a45c979f4b9b34cb7aff60194ab5dd4005ad Mon Sep 17 00:00:00 2001 From: polarathene <5098581+polarathene@users.noreply.github.com> Date: Mon, 4 Sep 2023 19:22:52 +1200 Subject: [PATCH 10/15] fix: New template files accidentally committed with `CRLF` line endings These files were created on Windows, linter caught the discrepancy. Now they're `LF`. --- target/features/ldap/dovecot.base | 18 ++++---- target/features/ldap/dovecot.tmpl | 64 ++++++++++++------------- target/features/ldap/postfix.base | 14 +++--- target/features/ldap/postfix.tmpl | 70 ++++++++++++++-------------- target/features/ldap/saslauthd.base | 12 ++--- target/features/ldap/saslauthd.tmpl | 72 ++++++++++++++--------------- 6 files changed, 125 insertions(+), 125 deletions(-) diff --git a/target/features/ldap/dovecot.base b/target/features/ldap/dovecot.base index 3ef4e15c..5d904cde 100644 --- a/target/features/ldap/dovecot.base +++ b/target/features/ldap/dovecot.base @@ -1,9 +1,9 @@ -dn = ${BIND_DN} -dnpass = ${BIND_PW} -uris = ${SERVER_HOST} -base = ${SEARCH_BASE} -default_pass_scheme = SSHA -pass_attrs = uniqueIdentifier=user,userPassword=password -pass_filter = (&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n)) -user_attrs = mailHomeDirectory=home,mailUidNumber=uid,mailGidNumber=gid,mailStorageDirectory=mail -user_filter = (&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n)) +dn = ${BIND_DN} +dnpass = ${BIND_PW} +uris = ${SERVER_HOST} +base = ${SEARCH_BASE} +default_pass_scheme = SSHA +pass_attrs = uniqueIdentifier=user,userPassword=password +pass_filter = (&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n)) +user_attrs = mailHomeDirectory=home,mailUidNumber=uid,mailGidNumber=gid,mailStorageDirectory=mail +user_filter = (&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n)) diff --git a/target/features/ldap/dovecot.tmpl b/target/features/ldap/dovecot.tmpl index a44ee80e..0be048b8 100644 --- a/target/features/ldap/dovecot.tmpl +++ b/target/features/ldap/dovecot.tmpl @@ -1,32 +1,32 @@ -# Dovecot LDAP config docs: https://github.com/dovecot/core/blob/bbb600e46ca650a3a5ef812ea3a1e8c45a6ea0ba/doc/example-config/dovecot-ldap.conf.ext -hosts = ${HOSTS} -uris = ${URIS} -dn = ${DN} -dnpass = ${DNPASS} -sasl_bind = ${SASL_BIND} -sasl_mech = ${SASL_MECH} -sasl_realm = ${SASL_REALM} -sasl_authz_id = ${SASL_AUTHZ_ID} -tls = ${TLS} -tls_ca_cert_file = ${TLS_CA_CERT_FILE} -tls_ca_cert_dir = ${TLS_CA_CERT_DIR} -tls_cipher_suite = ${TLS_CIPHER_SUITE} -tls_cert_file = ${TLS_CERT_FILE} -tls_key_file = ${TLS_KEY_FILE} -tls_require_cert = ${TLS_REQUIRE_CERT} -ldaprc_path = ${LDAPRC_PATH} -debug_level = ${DEBUG_LEVEL} -auth_bind = ${AUTH_BIND} -auth_bind_userdn = ${AUTH_BIND_USERDN} -ldap_version = ${LDAP_VERSION} -base = ${BASE} -deref = ${DEREF} -scope = ${SCOPE} -user_attrs = ${USER_ATTRS} -user_filter = ${USER_FILTER} -pass_attrs = ${PASS_ATTRS} -pass_filter = ${PASS_FILTER} -iterate_attrs = ${ITERATE_ATTRS} -iterate_filter = ${ITERATE_FILTER} -default_pass_scheme = ${DEFAULT_PASS_SCHEME} -blocking = ${BLOCKING} +# Dovecot LDAP config docs: https://github.com/dovecot/core/blob/bbb600e46ca650a3a5ef812ea3a1e8c45a6ea0ba/doc/example-config/dovecot-ldap.conf.ext +hosts = ${HOSTS} +uris = ${URIS} +dn = ${DN} +dnpass = ${DNPASS} +sasl_bind = ${SASL_BIND} +sasl_mech = ${SASL_MECH} +sasl_realm = ${SASL_REALM} +sasl_authz_id = ${SASL_AUTHZ_ID} +tls = ${TLS} +tls_ca_cert_file = ${TLS_CA_CERT_FILE} +tls_ca_cert_dir = ${TLS_CA_CERT_DIR} +tls_cipher_suite = ${TLS_CIPHER_SUITE} +tls_cert_file = ${TLS_CERT_FILE} +tls_key_file = ${TLS_KEY_FILE} +tls_require_cert = ${TLS_REQUIRE_CERT} +ldaprc_path = ${LDAPRC_PATH} +debug_level = ${DEBUG_LEVEL} +auth_bind = ${AUTH_BIND} +auth_bind_userdn = ${AUTH_BIND_USERDN} +ldap_version = ${LDAP_VERSION} +base = ${BASE} +deref = ${DEREF} +scope = ${SCOPE} +user_attrs = ${USER_ATTRS} +user_filter = ${USER_FILTER} +pass_attrs = ${PASS_ATTRS} +pass_filter = ${PASS_FILTER} +iterate_attrs = ${ITERATE_ATTRS} +iterate_filter = ${ITERATE_FILTER} +default_pass_scheme = ${DEFAULT_PASS_SCHEME} +blocking = ${BLOCKING} diff --git a/target/features/ldap/postfix.base b/target/features/ldap/postfix.base index 72bbee08..68e203ef 100644 --- a/target/features/ldap/postfix.base +++ b/target/features/ldap/postfix.base @@ -1,7 +1,7 @@ -bind_dn = ${BIND_DN} -bind_pw = ${BIND_PW} -server_host = ${SERVER_HOST} -search_base = ${SEARCH_BASE} -bind = yes -result_attribute = mail -version = 3 +bind_dn = ${BIND_DN} +bind_pw = ${BIND_PW} +server_host = ${SERVER_HOST} +search_base = ${SEARCH_BASE} +bind = yes +result_attribute = mail +version = 3 diff --git a/target/features/ldap/postfix.tmpl b/target/features/ldap/postfix.tmpl index 682d8e27..86de2f33 100644 --- a/target/features/ldap/postfix.tmpl +++ b/target/features/ldap/postfix.tmpl @@ -1,35 +1,35 @@ -# Postfix LDAP table docs: http://www.postfix.org/ldap_table.5.html -server_host = ${SERVER_HOST} -server_port = ${SERVER_PORT} -timeout = ${TIMEOUT} -search_base = ${SEARCH_BASE} -query_filter = ${QUERY_FILTER} -result_format = ${RESULT_FORMAT} -domain = ${DOMAIN} -result_attribute = ${RESULT_ATTRIBUTE} -special_result_attribute = ${SPECIAL_RESULT_ATTRIBUTE} -terminal_result_attribute = ${TERMINAL_RESULT_ATTRIBUTE} -leaf_result_attribute = ${LEAF_RESULT_ATTRIBUTE} -scope = ${SCOPE} -bind = ${BIND} -bind_dn = ${BIND_DN} -bind_pw = ${BIND_PW} -recursion_limit = ${RECURSION_LIMIT} -expansion_limit = ${EXPANSION_LIMIT} -size_limit = ${SIZE_LIMIT} -dereference = ${DEREFERENCE} -chase_referrals = ${CHASE_REFERRALS} -version = ${VERSION} -debuglevel = ${DEBUGLEVEL} -sasl_mechs = ${SASL_MECHS} -sasl_realm = ${SASL_REALM} -sasl_authz_id = ${SASL_AUTHZ_ID} -sasl_minssf = ${SASL_MINSSF} -start_tls = ${START_TLS} -tls_ca_cert_dir = ${TLS_CA_CERT_DIR} -tls_ca_cert_file = ${TLS_CA_CERT_FILE} -tls_cert = ${TLS_CERT} -tls_key = ${TLS_KEY} -tls_require_cert = ${TLS_REQUIRE_CERT} -tls_random_file = ${TLS_RANDOM_FILE} -tls_cipher_suite = ${TLS_CIPHER_SUITE} +# Postfix LDAP table docs: http://www.postfix.org/ldap_table.5.html +server_host = ${SERVER_HOST} +server_port = ${SERVER_PORT} +timeout = ${TIMEOUT} +search_base = ${SEARCH_BASE} +query_filter = ${QUERY_FILTER} +result_format = ${RESULT_FORMAT} +domain = ${DOMAIN} +result_attribute = ${RESULT_ATTRIBUTE} +special_result_attribute = ${SPECIAL_RESULT_ATTRIBUTE} +terminal_result_attribute = ${TERMINAL_RESULT_ATTRIBUTE} +leaf_result_attribute = ${LEAF_RESULT_ATTRIBUTE} +scope = ${SCOPE} +bind = ${BIND} +bind_dn = ${BIND_DN} +bind_pw = ${BIND_PW} +recursion_limit = ${RECURSION_LIMIT} +expansion_limit = ${EXPANSION_LIMIT} +size_limit = ${SIZE_LIMIT} +dereference = ${DEREFERENCE} +chase_referrals = ${CHASE_REFERRALS} +version = ${VERSION} +debuglevel = ${DEBUGLEVEL} +sasl_mechs = ${SASL_MECHS} +sasl_realm = ${SASL_REALM} +sasl_authz_id = ${SASL_AUTHZ_ID} +sasl_minssf = ${SASL_MINSSF} +start_tls = ${START_TLS} +tls_ca_cert_dir = ${TLS_CA_CERT_DIR} +tls_ca_cert_file = ${TLS_CA_CERT_FILE} +tls_cert = ${TLS_CERT} +tls_key = ${TLS_KEY} +tls_require_cert = ${TLS_REQUIRE_CERT} +tls_random_file = ${TLS_RANDOM_FILE} +tls_cipher_suite = ${TLS_CIPHER_SUITE} diff --git a/target/features/ldap/saslauthd.base b/target/features/ldap/saslauthd.base index 508c6e24..f93cf07a 100644 --- a/target/features/ldap/saslauthd.base +++ b/target/features/ldap/saslauthd.base @@ -1,6 +1,6 @@ -ldap_bind_dn: ${BIND_DN} -ldap_bind_pw: ${BIND_PW} -ldap_servers: ${SERVER_HOST} -ldap_search_base: ${SEARCH_BASE} -ldap_filter: (&(uniqueIdentifier=%u)(mailEnabled=TRUE)) -ldap_referrals: yes +ldap_bind_dn: ${BIND_DN} +ldap_bind_pw: ${BIND_PW} +ldap_servers: ${SERVER_HOST} +ldap_search_base: ${SEARCH_BASE} +ldap_filter: (&(uniqueIdentifier=%u)(mailEnabled=TRUE)) +ldap_referrals: yes diff --git a/target/features/ldap/saslauthd.tmpl b/target/features/ldap/saslauthd.tmpl index f07c985d..47957501 100644 --- a/target/features/ldap/saslauthd.tmpl +++ b/target/features/ldap/saslauthd.tmpl @@ -1,36 +1,36 @@ -# Parameter docs: https://github.com/cyrusimap/cyrus-sasl/blob/3959d45aa187d906d5fb3e8edf7e3661780967a5/saslauthd/LDAP_SASLAUTHD#L85-L242 -ldap_auth_method: ${LDAP_AUTH_METHOD} -ldap_bind_dn: ${LDAP_BIND_DN} -ldap_bind_pw: ${LDAP_BIND_PW} -ldap_default_domain: ${LDAP_DEFAULT_DOMAIN} -ldap_default_realm: ${LDAP_DEFAULT_REALM} -ldap_deref: ${LDAP_DEREF} -ldap_filter: ${LDAP_FILTER} -ldap_group_attr: ${LDAP_GROUP_ATTR} -ldap_group_dn: ${LDAP_GROUP_DN} -ldap_group_filter: ${LDAP_GROUP_FILTER} -ldap_group_match_method: ${LDAP_GROUP_MATCH_METHOD} -ldap_group_search_base: ${LDAP_GROUP_SEARCH_BASE} -ldap_group_scope: ${LDAP_GROUP_SCOPE} -ldap_password: ${LDAP_PASSWORD} -ldap_password_attr: ${LDAP_PASSWORD_ATTR} -ldap_referrals: ${LDAP_REFERRALS} -ldap_restart: ${LDAP_RESTART} -ldap_id: ${LDAP_ID} -ldap_authz_id: ${LDAP_AUTHZ_ID} -ldap_mech: ${LDAP_MECH} -ldap_realm: ${LDAP_REALM} -ldap_scope: ${LDAP_SCOPE} -ldap_search_base: ${LDAP_SEARCH_BASE} -ldap_servers: ${LDAP_SERVERS} -ldap_start_tls: ${LDAP_START_TLS} -ldap_time_limit: ${LDAP_TIME_LIMIT} -ldap_timeout: ${LDAP_TIMEOUT} -ldap_tls_check_peer: ${LDAP_TLS_CHECK_PEER} -ldap_tls_cacert_file: ${LDAP_TLS_CACERT_FILE} -ldap_tls_cacert_dir: ${LDAP_TLS_CACERT_DIR} -ldap_tls_ciphers: ${LDAP_TLS_CIPHERS} -ldap_tls_cert: ${LDAP_TLS_CERT} -ldap_tls_key: ${LDAP_TLS_KEY} -ldap_use_sasl: ${LDAP_USE_SASL} -ldap_version: ${LDAP_VERSION} +# Parameter docs: https://github.com/cyrusimap/cyrus-sasl/blob/3959d45aa187d906d5fb3e8edf7e3661780967a5/saslauthd/LDAP_SASLAUTHD#L85-L242 +ldap_auth_method: ${LDAP_AUTH_METHOD} +ldap_bind_dn: ${LDAP_BIND_DN} +ldap_bind_pw: ${LDAP_BIND_PW} +ldap_default_domain: ${LDAP_DEFAULT_DOMAIN} +ldap_default_realm: ${LDAP_DEFAULT_REALM} +ldap_deref: ${LDAP_DEREF} +ldap_filter: ${LDAP_FILTER} +ldap_group_attr: ${LDAP_GROUP_ATTR} +ldap_group_dn: ${LDAP_GROUP_DN} +ldap_group_filter: ${LDAP_GROUP_FILTER} +ldap_group_match_method: ${LDAP_GROUP_MATCH_METHOD} +ldap_group_search_base: ${LDAP_GROUP_SEARCH_BASE} +ldap_group_scope: ${LDAP_GROUP_SCOPE} +ldap_password: ${LDAP_PASSWORD} +ldap_password_attr: ${LDAP_PASSWORD_ATTR} +ldap_referrals: ${LDAP_REFERRALS} +ldap_restart: ${LDAP_RESTART} +ldap_id: ${LDAP_ID} +ldap_authz_id: ${LDAP_AUTHZ_ID} +ldap_mech: ${LDAP_MECH} +ldap_realm: ${LDAP_REALM} +ldap_scope: ${LDAP_SCOPE} +ldap_search_base: ${LDAP_SEARCH_BASE} +ldap_servers: ${LDAP_SERVERS} +ldap_start_tls: ${LDAP_START_TLS} +ldap_time_limit: ${LDAP_TIME_LIMIT} +ldap_timeout: ${LDAP_TIMEOUT} +ldap_tls_check_peer: ${LDAP_TLS_CHECK_PEER} +ldap_tls_cacert_file: ${LDAP_TLS_CACERT_FILE} +ldap_tls_cacert_dir: ${LDAP_TLS_CACERT_DIR} +ldap_tls_ciphers: ${LDAP_TLS_CIPHERS} +ldap_tls_cert: ${LDAP_TLS_CERT} +ldap_tls_key: ${LDAP_TLS_KEY} +ldap_use_sasl: ${LDAP_USE_SASL} +ldap_version: ${LDAP_VERSION} From d6ae1a8d4dd21c6283ec495641c76aa20fb0e9fb Mon Sep 17 00:00:00 2001 From: polarathene <5098581+polarathene@users.noreply.github.com> Date: Thu, 5 Oct 2023 12:31:59 +1300 Subject: [PATCH 11/15] chore: Add new file extensions to `.gitattributes` --- .gitattributes | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitattributes b/.gitattributes index 869c153e..8c981a29 100644 --- a/.gitattributes +++ b/.gitattributes @@ -69,6 +69,9 @@ target/postsrsd/** text *.local text ### Postfix *.pcre text +### Config Templates feature +*.base text +*.tmpl text ################################################# ### Tests ##################################### From 4b02933dd25ed029dc27ea8edfe73f75c75e436d Mon Sep 17 00:00:00 2001 From: polarathene <5098581+polarathene@users.noreply.github.com> Date: Mon, 4 Sep 2023 18:23:24 +1200 Subject: [PATCH 12/15] tests: Adjust KV match logic Inverse the assert to process the config file entry lookup to reduce the white-space between key and value to a consistent ` = ` which can then be compared directly to the `KEY_VALUE` input (_instead of the KV isolation dance used previously_). --- target/scripts/startup/setup.d/ldap.sh | 2 +- test/tests/serial/mail_with_ldap.bats | 17 ++++++----------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/target/scripts/startup/setup.d/ldap.sh b/target/scripts/startup/setup.d/ldap.sh index f4ffb36e..eb2fdf3b 100644 --- a/target/scripts/startup/setup.d/ldap.sh +++ b/target/scripts/startup/setup.d/ldap.sh @@ -59,6 +59,6 @@ function _create_config_postfix() { if ! grep --silent '^query_filter =' "${LDAP_CONFIG_FILE}"; then _log 'warn' "'${LDAP_CONFIG_FILE}' is missing the 'query_filter' setting - disabling" - sed -i "s/$(_escape_for_sed <<< ${LDAP_CONFIG_FILE})//" /etc/postfix/main.cf + sed -i "s/$(_escape_for_sed <<< "${LDAP_CONFIG_FILE}")//" /etc/postfix/main.cf fi } diff --git a/test/tests/serial/mail_with_ldap.bats b/test/tests/serial/mail_with_ldap.bats index 771a18ae..32cbc9ec 100644 --- a/test/tests/serial/mail_with_ldap.bats +++ b/test/tests/serial/mail_with_ldap.bats @@ -429,17 +429,12 @@ function _should_successfully_deliver_mail_to() { function _should_have_matching_setting() { local KEY_VALUE=${1} local CONFIG_FILE=${2} + local KV_DELIMITER=${3:-'='} - function __trim_whitespace() { - sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' <<< "${1}" - } - - local KEY VALUE - # Split string into key/value vars and trim white-space: - KEY=$(__trim_whitespace "${KEY_VALUE%=*}") - VALUE=$(__trim_whitespace "${KEY_VALUE#*=}") - - _run_in_container grep "${KEY}" "${CONFIG_FILE}" - assert_output --regexp "^${KEY}\s*=\s*${VALUE}$" + local KEY + KEY="${KEY_VALUE%%"${KV_DELIMITER}"*}" + # Look up the KEY portion from the target config file and use sed to reduce white-space between key and value: + _run_in_container_bash "grep '^${KEY}' '${CONFIG_FILE}' | sed 's/\s*${KV_DELIMITER}\s*/ ${KV_DELIMITER} /'" + assert_output "${LDAP_SETTING}" assert_success } From 24d3ba368c6d9c98a8fea7fb0d48c46c44e82a48 Mon Sep 17 00:00:00 2001 From: polarathene <5098581+polarathene@users.noreply.github.com> Date: Mon, 4 Sep 2023 19:39:11 +1200 Subject: [PATCH 13/15] tests: Fix LDAP test for Postfix `start_tls = no` --- test/tests/serial/mail_with_ldap.bats | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/tests/serial/mail_with_ldap.bats b/test/tests/serial/mail_with_ldap.bats index 32cbc9ec..2852712a 100644 --- a/test/tests/serial/mail_with_ldap.bats +++ b/test/tests/serial/mail_with_ldap.bats @@ -95,7 +95,6 @@ function setup_file() { # NOTE: `scripts/startup/setup.d/ldap.sh:_setup_ldap()` uses helper methods to generate / override LDAP configs (grouped by common ENV prefixes): --env LDAP_SERVER_HOST="ldap://${FQDN_LDAP}" --env LDAP_SEARCH_BASE='ou=users,dc=example,dc=test' - --env LDAP_START_TLS=no # Credentials needed for read access to LDAP_SEARCH_BASE: --env LDAP_BIND_DN='cn=admin,dc=example,dc=test' --env LDAP_BIND_PW='admin' @@ -117,6 +116,7 @@ function setup_file() { --env POSTFIX_GROUPS_QUERY_FILTER="${QUERY_GROUPS}" --env POSTFIX_SENDERS_QUERY_FILTER="${QUERY_SENDERS}" --env POSTFIX_USERS_QUERY_FILTER="${QUERY_USERS}" + --env POSTFIX_START_TLS=no ) # Extra ENV needed to support specific test-cases: From b007533380d27f52bef9ff9cb2d60fc7a805ac2d Mon Sep 17 00:00:00 2001 From: Brennan Kinney <5098581+polarathene@users.noreply.github.com> Date: Tue, 5 Sep 2023 22:44:21 +1200 Subject: [PATCH 14/15] Apply suggestions from code review Co-authored-by: Georg Lauterbach <44545919+georglauterbach@users.noreply.github.com> --- target/scripts/build/packages.sh | 4 +--- target/scripts/startup/setup.d/ldap.sh | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/target/scripts/build/packages.sh b/target/scripts/build/packages.sh index f6f9c638..2c3ab403 100644 --- a/target/scripts/build/packages.sh +++ b/target/scripts/build/packages.sh @@ -99,10 +99,8 @@ function _install_feature_config_templates() { apt-get "${QUIET}" --no-install-recommends install gettext-base # zenv: - local URL_ZENV - URL_ZENV="https://github.com/numToStr/zenv/releases/download/0.8.0/zenv-0.8.0-$(uname --machine)-unknown-linux-gnu.tar.gz" # Download from GH releases to stdout, then extract the zenv file to make available via PATH: - curl -L "${URL_ZENV}" -o - | tar --gzip --extract --directory /usr/local/bin --file - zenv + curl -L "https://github.com/numToStr/zenv/releases/download/0.8.0/zenv-0.8.0-$(uname --machine)-unknown-linux-gnu.tar.gz" -o - | tar --gzip --extract --directory /usr/local/bin --file - zenv } function _install_dovecot() { diff --git a/target/scripts/startup/setup.d/ldap.sh b/target/scripts/startup/setup.d/ldap.sh index eb2fdf3b..438bc6ac 100644 --- a/target/scripts/startup/setup.d/ldap.sh +++ b/target/scripts/startup/setup.d/ldap.sh @@ -45,7 +45,7 @@ function _create_config_dovecot() { } function _create_config_postfix() { - local QUERY_KIND=${1} +local QUERY_KIND=${1:?QUERY_KIND is required in _create_config_postfix} local LDAP_CONFIG_FILE="/etc/postfix/ldap/${QUERY_KIND}.cf" _cleanse_config '=' <(cat 2>/dev/null \ @@ -56,9 +56,9 @@ function _create_config_postfix() { ) > "${LDAP_CONFIG_FILE}" # Opt-out of generated config if `query_filter` was not configured: - if ! grep --silent '^query_filter =' "${LDAP_CONFIG_FILE}"; then + if ! grep -q '^query_filter =' "${LDAP_CONFIG_FILE}"; then _log 'warn' "'${LDAP_CONFIG_FILE}' is missing the 'query_filter' setting - disabling" - sed -i "s/$(_escape_for_sed <<< "${LDAP_CONFIG_FILE}")//" /etc/postfix/main.cf + sed -i "s/$(_escape_for_sed "${LDAP_CONFIG_FILE}")//" /etc/postfix/main.cf fi } From c811e6c8542c5abed836497b28b3311ffd07a269 Mon Sep 17 00:00:00 2001 From: Brennan Kinney <5098581+polarathene@users.noreply.github.com> Date: Wed, 6 Sep 2023 00:59:19 +1200 Subject: [PATCH 15/15] Apply suggestions from code review --- target/scripts/helpers/utils.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/target/scripts/helpers/utils.sh b/target/scripts/helpers/utils.sh index 91900fc6..4d21e2dc 100644 --- a/target/scripts/helpers/utils.sh +++ b/target/scripts/helpers/utils.sh @@ -154,9 +154,7 @@ function _env_var_expect_integer() { return 1 } -# Ensures that zenv only runs envsubst with ENV filtered from the provided prefix. -# Those ENV are loaded by zenv in the format of an `.env` file (with prefix dropped by sed) -# When an ENV is not available, envsubst will evaluate it as empty. +# Replace `${VAR}` variables of the input file with their equivalent ENV values (excluding the common prefix) # # @param ${1} = Use a prefix for a group of environment variables # @param ${2} = Filepath to ENV template @@ -169,6 +167,10 @@ function _template_with_env() { _dms_panic__invalid_value "file '${ENV_TEMPLATE}' does not exist" 'utils.sh:_use_env_template' fi + # Ensures that zenv only runs envsubst with ENV filtered from the provided prefix. + # Those ENV are loaded by zenv in the format of an `.env` file (with prefix dropped by sed) + # When an ENV is not available, envsubst will evaluate it as empty. + # # NOTE: $PATH is retained to avoid needing absolute paths for binaries. env --ignore-environment PATH="${PATH}" \ zenv --file <(env | grep "^${ENV_PREFIX}" | sed "s/^${ENV_PREFIX}//") \