diff --git a/.gitattributes b/.gitattributes index da5dadda..682b2245 100644 --- a/.gitattributes +++ b/.gitattributes @@ -68,6 +68,9 @@ target/postsrsd/** text *.local text ### Postfix *.pcre text +### Config Templates feature +*.base text +*.tmpl text ################################################# ### Tests ##################################### diff --git a/Dockerfile b/Dockerfile index e822632a..93c8783e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 <>"${TMP_VHOST}" } diff --git a/target/scripts/helpers/utils.sh b/target/scripts/helpers/utils.sh index f7095bf3..4d21e2dc 100644 --- a/target/scripts/helpers/utils.sh +++ b/target/scripts/helpers/utils.sh @@ -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 +} diff --git a/target/scripts/startup/setup.d/ldap.sh b/target/scripts/startup/setup.d/ldap.sh index 1451ec32..438bc6ac 100644 --- a/target/scripts/startup/setup.d/ldap.sh +++ b/target/scripts/startup/setup.d/ldap.sh @@ -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 +} diff --git a/target/scripts/startup/setup.d/saslauthd.sh b/target/scripts/startup/setup.d/saslauthd.sh index eb33a243..c1c23e4c 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,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 +} 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 f3ee1cc1..f12b58e4 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,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 +}