This commit is contained in:
Brennan Kinney 2024-01-25 00:47:02 +13:00 committed by GitHub
commit 75326f8c3b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 274 additions and 182 deletions

3
.gitattributes vendored
View File

@ -68,6 +68,9 @@ target/postsrsd/** text
*.local text
### Postfix
*.pcre text
### Config Templates feature
*.base text
*.tmpl text
#################################################
### Tests #####################################

View File

@ -119,14 +119,8 @@ COPY target/dovecot/dovecot-oauth2.conf.ext /etc/dovecot
# --- 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 template support:
COPY --link target/features/ldap/ /etc/dms/ldap/
# hadolint ignore=SC2016
RUN <<EOF

View File

@ -1,12 +1,9 @@
base = ou=people,dc=example,dc=com
dn = ${BIND_DN}
dnpass = ${BIND_PW}
uris = ${SERVER_HOST}
base = ${SEARCH_BASE}
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

View File

@ -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}

View File

@ -0,0 +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

View File

@ -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}

View File

@ -0,0 +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

View File

@ -0,0 +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}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -92,6 +92,17 @@ function _install_packages() {
"${DEBUG_PACKAGES[@]}"
}
function _install_feature_config_templates() {
_log 'debug' 'Installing support for feature - Config Templates'
# envsubst:
apt-get "${QUIET}" --no-install-recommends install gettext-base
# zenv:
# Download from GH releases to stdout, then extract the zenv file to make available via PATH:
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() {
declare -a DOVECOT_PACKAGES
@ -227,5 +238,6 @@ _install_rspamd
_install_fail2ban
_install_getmail
_install_utils
_install_feature_config_templates
_remove_data_after_package_installations
_post_installation_steps

View File

@ -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}"
}

View File

@ -153,3 +153,41 @@ function _env_var_expect_integer() {
_log 'warn' "The value of '${ENV_VAR_NAME}' is not an integer ('${!ENV_VAR_NAME}'), but was expected to be"
return 1
}
# 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
# @output = Template file content populated with available ENV
function _template_with_env() {
local ENV_PREFIX=${1:?ENV prefix is required}
local ENV_TEMPLATE=${2:?ENV template filepath is required}
if [[ ! -f ${ENV_TEMPLATE} ]]; then
_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}//") \
envsubst < "${ENV_TEMPLATE}"
}
# Utility to cleanup a config file that may have unset or duplicate keys.
# - sed => 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
}

View File

@ -2,80 +2,63 @@
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
_log 'trace' "Configuring Postfix for 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
_create_config_postfix "${QUERY_KIND}"
done
_log 'trace' 'Starting to override configs'
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
)
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}"
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'
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
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
}
function _create_config_postfix() {
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 \
<(_template_with_env 'LDAP_' /etc/dms/ldap/postfix.base) \
"/tmp/docker-mailserver/ldap-${QUERY_KIND}.cf" \
<(_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:
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
fi
}

View File

@ -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,14 @@ EOF
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() {
_cleanse_config ':' <(cat 2>/dev/null \
<(_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
}

View File

@ -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.

View File

@ -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,10 +92,9 @@ 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
# Credentials needed for read access to LDAP_SEARCH_BASE:
--env LDAP_BIND_DN='cn=admin,dc=example,dc=test'
--env LDAP_BIND_PW='admin'
@ -113,11 +111,12 @@ 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}"
--env POSTFIX_START_TLS=no
)
# Extra ENV needed to support specific test-cases:
@ -199,7 +198,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 +209,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
@ -230,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
}
@ -270,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
}
@ -402,20 +389,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}"
}
@ -439,3 +426,16 @@ 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}
local KV_DELIMITER=${3:-'='}
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
}