1
0
mirror of https://github.com/mail-in-a-box/mailinabox.git synced 2026-03-04 15:54:48 +01:00

Issue #1340 - LDAP backend for accounts

This commit will:

1. Change the user account database from sqlite to OpenLDAP
2. Add policyd-spf to postfix for SPF validation
3. Add a test runner with some automated test suites

Notes:

User account password hashes are preserved.

There is a new Roundcube contact list called "Directory" that lists the users in LDAP (MiaB users), similar to what Google Suite does.

Users can still change their password in Roundcube.

OpenLDAP is configured with TLS, but all remote access is blocked by firewall rules. Manual changes are required to open it for remote access (eg. "ufw allow proto tcp from <HOST> to any port ldaps").

The test runner is started by executing tests/runner.sh. Be aware that it will make changes to your system, including adding new users, domains, mailboxes, start/stop services, etc. It is highly unadvised to run it on a production system!

The LDAP schema that supports mail delivery with postfix and dovecot is located in conf/postfix.schema. This file is copied verbatim from the LdapAdmin project (GPL, ldapadmin.org). Instead of including the file in git, it could be referenced by URL and downloaded by the setup script if GPL is an issue or apply for a PEN from IANA.

Mangement console and other services should not appear or behave any differently than before.
This commit is contained in:
downtownallday
2020-01-17 17:03:21 -05:00
parent a67f90593d
commit 1f0d2ddb92
41 changed files with 5509 additions and 339 deletions

1
tests/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
out

58
tests/prep_vm.sh Executable file
View File

@@ -0,0 +1,58 @@
#!/bin/bash
# Run this on a VM to pre-install all the packages, then
# take a snapshot - it will greatly speed up subsequent
# test installs
remove_line_continuation() {
local file="$1"
awk '
BEGIN { C=0 }
C==1 && /[^\\]$/ { C=0; print $0; next }
C==1 { printf("%s",substr($0,0,length($0)-1)); next }
/\\$/ { C=1; printf("%s",substr($0,0,length($0)-1)); next }
{ print $0 }' \
"$file"
}
install_packages() {
while read line; do
pkgs=""
case "$line" in
apt_install* )
pkgs="$(cut -c12- <<<"$line")"
;;
"apt-get install"* )
pkgs="$(cut -c16- <<<"$line")"
;;
"apt install"* )
pkgs="$(cut -c12- <<<"$line")"
;;
esac
# don't install postfix - causes problems with setup scripts
pkgs="$(sed s/postfix//g <<<"$pkgs")"
if [ ! -z "$pkgs" ]; then
echo "install: $pkgs"
apt-get install $pkgs -y
fi
done
}
apt-get update -y
apt-get upgrade -y
apt-get autoremove -y
for file in $(ls setup/*.sh); do
remove_line_continuation "$file" | install_packages
done
apt-get install openssh-server -y
apt-get install emacs-nox -y
echo ""
echo ""
echo "Done. Take a snapshot...."
echo ""

81
tests/runner.sh Executable file
View File

@@ -0,0 +1,81 @@
#!/bin/bash
# -*- indent-tabs-mode: t; tab-width: 4; -*-
#
# Runner for test suites
#
# operate from the runner's directory
cd "$(dirname $0)"
# load global functions and variables
. suites/_init.sh
runner_suites=(
ldap-connection
ldap-access
mail-basic
mail-from
mail-aliases
mail-access
management-users
)
usage() {
echo ""
echo "Usage: $(basename $0) [-failfatal] [suite-name ...]"
echo "Valid suite names:"
for runner_suite in ${runner_suites[@]}; do
echo " $runner_suite"
done
echo "If no suite-name(s) given, all suites are run"
echo ""
echo "Options:"
echo " -failfatal The runner will stop if any test fails"
echo ""
echo "Output directory: $(dirname $0)/${base_outputdir}"
echo ""
exit 1
}
# process command line
while [ $# -gt 0 ]; do
case "$1" in
-failfatal )
# failure is fatal (via global option, see _init.sh)
FAILURE_IS_FATAL=yes
;;
-* )
echo "Invalid argument $1" 1>&2
usage
;;
* )
# run named suite
if array_contains "$1" ${runner_suites[@]}; then
. "suites/$1.sh"
else
echo "Unknown suite '$1'" 1>&2
usage
fi
;;
esac
shift
done
# if no suites specified on command line, run all suites
if [ $OVERALL_COUNT_SUITES -eq 0 ]; then
rm -rf "${base_outputdir}"
for runner_suite in ${runner_suites[@]}; do
. suites/$runner_suite.sh
done
fi
echo ""
echo "Done"
echo "$OVERALL_COUNT tests ($OVERALL_SUCCESSES success/$OVERALL_FAILURES failures) in $OVERALL_COUNT_SUITES test suites"
if [ $OVERALL_FAILURES -gt 0 ]; then
exit 1
else
exit 0
fi

176
tests/suites/_init.sh Normal file
View File

@@ -0,0 +1,176 @@
# -*- indent-tabs-mode: t; tab-width: 4; -*-
# load useful functions from setup
. ../setup/functions.sh || exit 1
. ../setup/functions-ldap.sh || exit 1
set +eu
# load test suite helper functions
. suites/_ldap-functions.sh || exit 1
. suites/_mail-functions.sh || exit 1
. suites/_mgmt-functions.sh || exit 1
# globals - all global variables are UPPERCASE
BASE_OUTPUTDIR="out"
PYMAIL="./test_mail.py"
declare -i OVERALL_SUCCESSES=0
declare -i OVERALL_FAILURES=0
declare -i OVERALL_COUNT=0
declare -i OVERALL_COUNT_SUITES=0
# ansi escapes for hilighting text
F_DANGER=$(echo -e "\033[31m")
F_WARN=$(echo -e "\033[93m")
F_RESET=$(echo -e "\033[39m")
# options
FAILURE_IS_FATAL=no
suite_start() {
let TEST_NUM=1
let SUITE_COUNT_SUCCESS=0
let SUITE_COUNT_FAILURE=0
let SUITE_COUNT_TOTAL=0
SUITE_NAME="$1"
OUTDIR="$BASE_OUTPUTDIR/$SUITE_NAME"
mkdir -p "$OUTDIR"
echo ""
echo "Starting suite: $SUITE_NAME"
suite_setup "$2"
}
suite_end() {
suite_cleanup "$1"
echo "Suite $SUITE_NAME finished"
let OVERALL_SUCCESSES+=$SUITE_COUNT_SUCCESS
let OVERALL_FAILURES+=$SUITE_COUNT_FAILURE
let OVERALL_COUNT+=$SUITE_COUNT_TOTAL
let OVERALL_COUNT_SUITES+=1
}
suite_setup() {
[ -z "$1" ] && return 0
TEST_OF="$OUTDIR/setup"
eval "$1"
TEST_OF=""
}
suite_cleanup() {
[ -z "$1" ] && return 0
TEST_OF="$OUTDIR/cleanup"
eval "$1"
TEST_OF=""
}
test_start() {
TEST_DESC="${1:-}"
TEST_NAME="$(printf "%03d" $TEST_NUM)"
TEST_OF="$OUTDIR/$TEST_NAME"
TEST_STATE=""
TEST_STATE_MSG=()
echo "TEST-START \"${TEST_DESC:-unnamed}\"" >$TEST_OF
echo -n " $TEST_NAME: $TEST_DESC: "
let TEST_NUM+=1
let SUITE_COUNT_TOTAL+=1
}
test_end() {
[ -z "$TEST_OF" ] && return
if [ $# -gt 0 ]; then
[ -z "$1" ] && test_success || test_failure "$1"
fi
case $TEST_STATE in
SUCCESS | "" )
record "[SUCCESS]"
echo "SUCCESS"
let SUITE_COUNT_SUCCESS+=1
;;
FAILURE )
record "[FAILURE]"
echo "${F_DANGER}FAILURE${F_RESET}:"
local idx=0
while [ $idx -lt ${#TEST_STATE_MSG[*]} ]; do
record "${TEST_STATE_MSG[$idx]}"
echo " why: ${TEST_STATE_MSG[$idx]}"
let idx+=1
done
echo " see: $(dirname $0)/$TEST_OF"
let SUITE_COUNT_FAILURE+=1
if [ "$FAILURE_IS_FATAL" == "yes" ]; then
record "FATAL: failures are fatal option enabled"
echo "FATAL: failures are fatal option enabled"
exit 1
fi
;;
* )
record "[INVALID TEST STATE '$TEST_STATE']"
echo "Invalid TEST_STATE=$TEST_STATE"
let SUITE_COUNT_FAILURE+=1
;;
esac
TEST_OF=""
}
test_success() {
[ -z "$TEST_OF" ] && return
[ -z "$TEST_STATE" ] && TEST_STATE="SUCCESS"
}
test_failure() {
local why="$1"
[ -z "$TEST_OF" ] && return
TEST_STATE="FAILURE"
TEST_STATE_MSG+=( "$why" )
}
have_test_failures() {
[ "$TEST_STATE" == "FAILURE" ] && return 0
return 1
}
record() {
if [ ! -z "$TEST_OF" ]; then
echo "$@" >>$TEST_OF
else
echo "$@"
fi
}
die() {
record "FATAL: $@"
test_failure "a fatal error occurred"
test_end
echo "FATAL: $@"
exit 1
}
array_contains() {
local searchfor="$1"
shift
local item
for item; do
[ "$item" == "$searchfor" ] && return 0
done
return 1
}
python_error() {
# finds tracebacks and outputs just the final error message of
# each
local output="$1"
awk 'BEGIN { TB=0; FOUND=0 } TB==0 && /^Traceback/ { TB=1; FOUND=1; next } TB==1 && /^[^ ]/ { print $0; TB=0 } END { if (FOUND==0) exit 1 }' <<< "$output"
[ $? -eq 1 ] && echo "$output"
}
##
## Initialize
##
mkdir -p "$BASE_OUTPUTDIR"
# load global vars
. /etc/mailinabox.conf || die "Could not load '/etc/mailinabox.conf'"
. "${STORAGE_ROOT}/ldap/miab_ldap.conf" || die "Could not load miab_ldap.conf"

View File

@@ -0,0 +1,427 @@
# -*- indent-tabs-mode: t; tab-width: 4; -*-
generate_uuid() {
local uuid
uuid=$(python3 -c "import uuid; print(uuid.uuid4())")
[ $? -ne 0 ] && die "Unable to generate a uuid"
echo "$uuid"
}
delete_user() {
local email="$1"
local domainpart="$(awk -F@ '{print $2}' <<< "$email")"
get_attribute "$LDAP_USERS_BASE" "mail=$email" "dn"
[ -z "$ATTR_DN" ] && return 0
record "[delete user $email]"
ldapdelete -H $LDAP_URL -x -D "$LDAP_ADMIN_DN" -w "$LDAP_ADMIN_PASSWORD" "$ATTR_DN" >>$TEST_OF 2>&1 || die "Unable to delete user $ATTR_DN (as admin)"
record "deleted"
# delete the domain if there are no more users in the domain
get_attribute "$LDAP_USERS_BASE" "mail=*@${domainpart}" "dn"
[ ! -z "$ATTR_DN" ] && return 0
get_attribute "$LDAP_DOMAINS_BASE" "dc=${domainpart}" "dn"
if [ ! -z "$ATTR_DN" ]; then
record "[delete domain $domainpart]"
ldapdelete -H $LDAP_URL -x -D "$LDAP_ADMIN_DN" -w "$LDAP_ADMIN_PASSWORD" "$ATTR_DN" >>$TEST_OF 2>&1 || die "Unable to delete domain $ATTR_DN (as admin)"
record "deleted"
fi
}
create_user() {
local email="$1"
local pass="${2:-$email}"
local priv="${3:-test}"
local localpart="$(awk -F@ '{print $1}' <<< "$email")"
local domainpart="$(awk -F@ '{print $2}' <<< "$email")"
local uid="$localpart"
local dn="uid=${uid},${LDAP_USERS_BASE}"
delete_user "$email"
record "[create user $email]"
delete_dn "$dn"
ldapadd -H "$LDAP_URL" -x -D "$LDAP_ADMIN_DN" -w "$LDAP_ADMIN_PASSWORD" >>$TEST_OF 2>&1 <<EOF
dn: $dn
objectClass: inetOrgPerson
objectClass: mailUser
objectClass: shadowAccount
uid: $uid
cn: $localpart
sn: $localpart
displayName: $localpart
mail: $email
maildrop: $email
mailaccess: $priv
userPassword: $(slappasswd_hash "$pass")
EOF
[ $? -ne 0 ] && die "Unable to add user $dn (as admin)"
# create domain entry, if needed
get_attribute "$LDAP_DOMAINS_BASE" "dc=${domainpart}" dn
if [ -z "$ATTR_DN" ]; then
record "[create domain entry $domainpart]"
ldapadd -H $LDAP_URL -x -D "$LDAP_ADMIN_DN" -w "$LDAP_ADMIN_PASSWORD" >>$TEST_OF 2>&1 <<EOF
dn: dc=${domainpart},$LDAP_DOMAINS_BASE
objectClass: domain
dc: ${domainpart}
businessCategory: mail
EOF
[ $? -ne 0 ] && die "Unable to add domain ${domainpart} (as admin)"
fi
ATTR_DN="$dn"
}
delete_dn() {
local dn="$1"
get_attribute "$dn" "objectClass=*" "dn" base
[ -z "$ATTR_DN" ] && return 0
record "delete dn: $dn"
ldapdelete -H $LDAP_URL -x -D "$LDAP_ADMIN_DN" -w "$LDAP_ADMIN_PASSWORD" "$dn" >>$TEST_OF 2>&1 || die "Unable to delete $dn (as admin)"
}
create_service_account() {
local cn="$1"
local pass="${2:-$cn}"
local dn="cn=${cn},${LDAP_SERVICES_BASE}"
record "[create service account $cn]"
delete_dn "$dn"
ldapadd -H "$LDAP_URL" -x -D "$LDAP_ADMIN_DN" -w "$LDAP_ADMIN_PASSWORD" >>$TEST_OF 2>&1 <<EOF
dn: $dn
objectClass: simpleSecurityObject
objectClass: organizationalRole
cn: $cn
description: TEST ${cn} service account
userPassword: $(slappasswd_hash "$pass")
EOF
[ $? -ne 0 ] && die "Unable to add service account $dn (as admin)"
ATTR_DN="$dn"
}
delete_service_account() {
local cn="$1"
local dn="cn=${cn},${LDAP_SERVICES_BASE}"
record "[delete service account $cn]"
delete_dn "$dn"
}
create_alias_group() {
local alias="$1"
shift
record "[Create new alias group $alias]"
# add alias group with dn's as members
get_attribute "$LDAP_ALIASES_BASE" "mail=$alias" "dn"
if [ ! -z "$ATTR_DN" ]; then
delete_dn "$ATTR_DN"
fi
ATTR_DN="cn=$(generate_uuid),$LDAP_ALIASES_BASE"
of="/tmp/create_alias.$$.ldif"
cat >$of 2>>$TEST_OF <<EOF
dn: $ATTR_DN
objectClass: mailGroup
mail: $alias
EOF
local member
for member; do
case $member in
*@* )
echo "rfc822MailMember: $member" >>$TEST_OF
echo "rfc822MailMember: $member" >>$of 2>>$TEST_OF
;;
* )
echo "member: $member" >>$TEST_OF
echo "member: $member" >>$of 2>>$TEST_OF
;;
esac
done
ldapadd -H "$LDAP_URL" -x -D "$LDAP_ADMIN_DN" -w "$LDAP_ADMIN_PASSWORD" -f $of >>$TEST_OF 2>&1 || die "Unable to add alias group $alias"
rm -f $of
}
delete_alias_group() {
record "[delete alias group $1]"
get_attribute "$LDAP_ALIASES_BASE" "(mail=$1)" dn
[ ! -z "$ATTR_DN" ] && delete_dn "$ATTR_DN"
}
add_alias() {
local user_dn="$1"
local alias="$2"
local type="${3:-group}"
if [ $type == user ]; then
# add alias as additional 'mail' attribute to user's dn
record "[Add alias $alias to $user_dn]"
ldapmodify -H "$LDAP_URL" -x -D "$LDAP_ADMIN_DN" -w "$LDAP_ADMIN_PASSWORD" >>$TEST_OF 2>&1 <<EOF
dn: $user_dn
add: mail
mail: $alias
EOF
local r=$?
[ $r -ne 0 ] && die "Unable to modify $user_dn"
elif [ $type == group ]; then
# add alias as additional 'member" to a mailGroup alias list
record "[Add member $user_dn to alias $alias]"
get_attribute "$LDAP_ALIASES_BASE" "mail=$alias" "dn"
if [ -z "$ATTR_DN" ]; then
# don't automatically add because it should be cleaned
# up by the caller
die "Alias grour $alias does not exist"
else
ldapmodify -H "$LDAP_URL" -x -D "$LDAP_ADMIN_DN" -w "$LDAP_ADMIN_PASSWORD" >>$TEST_OF 2>&1 <<EOF
dn: $ATTR_DN
add: member
member: $user_dn
EOF
local code=$?
if [ $code -ne 20 -a $code -ne 0 ]; then
# 20=Type or value exists
die "Unable to add user $user_dn to alias $alias"
fi
fi
else
die "Invalid type '$type' to add_alias"
fi
}
create_permitted_senders_group() {
# add a permitted senders group. specify the email address that
# the members may MAIL FROM as the first argument, followed by all
# member dns. If the group already exists, it is deleted first.
#
# on return, the global variable ATTR_DN is set to the dn of the
# created mailGroup
local mail_from="$1"
shift
record "[create permitted sender list $mail_from]"
get_attribute "$LDAP_PERMITTED_SENDERS_BASE" "(&(objectClass=mailGroup)(mail=$mail_from))" dn
if [ ! -z "$ATTR_DN" ]; then
delete_dn "$ATTR_DN"
fi
local tmp="/tmp/tests.$$.ldif"
ATTR_DN="cn=$(generate_uuid),$LDAP_PERMITTED_SENDERS_BASE"
cat >$tmp <<EOF
dn: $ATTR_DN
objectClass: mailGroup
mail: $mail_from
EOF
local member
for member; do
echo "member: $member" >>$tmp
echo "member: $member" >>$TEST_OF
done
ldapadd -H "$LDAP_URL" -x -D "$LDAP_ADMIN_DN" -w "$LDAP_ADMIN_PASSWORD" -f $tmp >>$TEST_OF 2>&1
local r=$?
rm -f $tmp
[ $r -ne 0 ] && die "Unable to add permitted senders group $mail_from"
}
delete_permitted_senders_group() {
local mail_from="$1"
record "[delete permitted sender list $mail_from]"
get_attribute "$LDAP_PERMITTED_SENDERS_BASE" "(&(objectClass=mailGroup)(mail=$mail_from))" dn
if [ ! -z "$ATTR_DN" ]; then
delete_dn "$ATTR_DN"
fi
}
test_r_access() {
# tests read or unreadable access
# sets global variable FAILURE on return
local user_dn="$1"
local login_dn="$2"
local login_pass="$3"
local access="${4:-no-read}" # should be "no-read" or "read"
shift; shift; shift; shift
if ! array_contains $access read no-read; then
die "Invalid parameter '$access' to function test_r_access"
fi
# get all attributes using login_dn's account
local attr
local search_output result=()
record "[Get attributes of $user_dn by $login_dn]"
search_output=$(ldapsearch -LLL -o ldif-wrap=no -H "$LDAP_URL" -b "$user_dn" -s base -x -D "$login_dn" -w "$login_pass" 2>>$TEST_OF)
local code=$?
# code 32: No such object (doesn't exist or login can't see it)
[ $code -ne 0 -a $code -ne 32 ] && die "Unable to find entry $user_dn by $login_dn"
while read attr; do
record "line: $attr"
attr=$(awk -F: '{print $1}' <<< "$attr")
[ "$attr" != "dn" -a "$attr" != "objectClass" ] && result+=($attr)
done <<< "$search_output"
record "check for $access access to ${@:-ALL}"
record "comparing to actual: ${result[@]}"
local failure=""
if [ $access == "no-read" -a $# -eq 0 ]; then
# check that no attributes are readable
if [ ${#result[*]} -gt 0 ]; then
failure="Attributes '${result[*]}' of $user_dn should not be readable by $login_dn"
fi
else
# check that specified attributes are/aren't readable
for attr; do
if [ $access == "no-read" ]; then
if array_contains $attr ${result[@]}; then
failure="Attribute $attr of $user_dn should not be readable by $login_dn"
break
fi
else
if ! array_contains $attr ${result[@]}; then
failure="Attribute $attr of $user_dn should be readable by $login_dn got (${result[*]})"
break
fi
fi
done
fi
FAILURE="$failure"
}
assert_r_access() {
# asserts read or unreadable access
FAILURE=""
test_r_access "$@"
[ ! -z "$FAILURE" ] && test_failure "$FAILURE"
}
test_w_access() {
# tests write or unwritable access
# sets global variable FAILURE on return
# if no attributes given, test user attributes
# uuid, cn, sn, mail, maildrop, mailaccess
local user_dn="$1"
local login_dn="$2"
local login_pass="$3"
local access="${4:-no-write}" # should be "no-write" or "write"
shift; shift; shift; shift
local moddn=""
local attrs=( $@ )
if ! array_contains $access write no-write; then
die "Invalid parameter '$access' to function test_w_access"
fi
if [ ${#attrs[*]} -eq 0 ]; then
moddn=uid
attrs=("cn=alice fiction" "sn=fiction" "mail" "maildrop" "mailaccess=admin")
fi
local failure=""
# check that select attributes are not writable
if [ ! -z "$moddn" ]; then
record "[Change attribute ${moddn}]"
delete_dn "${moddn}=some-uuid,$LDAP_USERS_BASE"
ldapmodify -H "$LDAP_URL" -x -D "$login_dn" -w "$login_pass" >>$TEST_OF 2>&1 <<EOF
dn: $user_dn
changetype: moddn
newrdn: ${moddn}=some-uuid
deleteoldrdn: 1
EOF
local r=$?
if [ $r -eq 0 ]; then
if [ "$access" == "no-write" ]; then
failure="Attribute $moddn of $user_dn should not be changeable by $login_dn"
fi
elif [ $r -eq 50 ]; then
if [ "$access" == "write" ]; then
failure="Attribute $moddn of $user_dn should be changeable by $login_dn"
fi
else
die "Error attempting moddn change of $moddn (code $?)"
fi
fi
if [ -z "$failure" ]; then
local attrvalue attr value
for attrvalue in "${attrs[@]}"; do
attr="$(awk -F= '{print $1}' <<< "$attrvalue")"
value="$(awk -F= '{print substr($0,length($1)+2)}' <<< "$attrvalue")"
[ -z "$value" ] && value="alice2@abc.com"
record "[Change attribute $attr]"
ldapmodify -H "$LDAP_URL" -x -D "$login_dn" -w "$login_pass" >>$TEST_OF 2>&1 <<EOF
dn: $user_dn
replace: $attr
$attr: $value
EOF
r=$?
if [ $r -eq 0 ]; then
if [ $access == "no-write" ]; then
failure="Attribute $attr of $user_dn should not be changeable by $login_dn"
break
fi
elif [ $r -eq 50 ]; then
if [ $access == "write" ]; then
failure="Attribute $attr of $user_dn should be changeable by $login_dn"
break
fi
else
die "Error attempting change of $attr to '$value'"
fi
done
fi
FAILURE="$failure"
}
assert_w_access() {
# asserts write or unwritable access
FAILURE=""
test_w_access "$@"
[ ! -z "$FAILURE" ] && test_failure "$FAILURE"
}
test_search() {
# test if access to search something is allowed
# sets global variable SEARCH_DN_COUNT on return
local base_dn="$1"
local login_dn="$2"
local login_pass="$3"
local scope="${4:-sub}"
local filter="$5"
let SEARCH_DN_COUNT=0
local line search_output
record "[Perform $scope search of $base_dn by $login_dn]"
search_output=$(ldapsearch -H $LDAP_URL -o ldif-wrap=no -b "$base_dn" -s "$scope" -LLL -x -D "$login_dn" -w "$login_pass" $filter 2>>$TEST_OF)
local code=$?
# code 32: No such object (doesn't exist or login can't see it)
[ $code -ne 0 -a $code -ne 32 ] && die "Unable to search $base_dn by $login_dn"
while read line; do
record "line: $line"
case $line in
dn:*)
let SEARCH_DN_COUNT+=1
;;
esac
done <<< "$search_output"
record "$SEARCH_DN_COUNT entries found"
}
record_search() {
local dn="$1"
record "[Contents of $dn]"
debug_search "$dn" >>$TEST_OF 2>&1
return 0
}

View File

@@ -0,0 +1,369 @@
# -*- indent-tabs-mode: t; tab-width: 4; -*-
clear_postfix_queue() {
record "[Clear postfix queue]"
postsuper -d ALL >>$TEST_OF 2>&1 || die "Unable to clear postfix undeliverable mail queue"
}
ensure_root_user() {
# ensure there is a local email account for root.
#
# on exit, ROOT, ROOT_MAILDROP, and ROOT_DN are set, and if no
# account exists, a new root@$(hostname) is created having a
# random password
#
if [ ! -z "$ROOT_MAILDROP" ]; then
# already have it
return
fi
ROOT="${USER}@$(hostname)"
record "[Find user $ROOT]"
get_attribute "$LDAP_USERS_BASE" "mail=$ROOT" "maildrop"
ROOT_MAILDROP="$ATTR_VALUE"
ROOT_DN="$ATTR_DN"
if [ -z "$ROOT_DN" ]; then
local pw="$(generate_password 128)"
create_user "$ROOT" "$pw"
record "new password is: $pw"
ROOT_DN="$ATTR_DN"
ROOT_MAILDROP="$ROOT"
else
record "$ROOT => $ROOT_DN ($ROOT_MAILDROP)"
fi
}
dovecot_mailbox_home() {
local email="$1"
echo -n "${STORAGE_ROOT}/mail/mailboxes/"
awk -F@ '{print $2"/"$1}' <<< "$email"
}
start_log_capture() {
SYS_LOG_LINECOUNT=$(wc -l /var/log/syslog 2>>$TEST_OF | awk '{print $1}') || die "could not access /var/log/syslog"
SLAPD_LOG_LINECOUNT=0
if [ -e /var/log/ldap/slapd.log ]; then
SLAPD_LOG_LINECOUNT=$(wc -l /var/log/ldap/slapd.log 2>>$TEST_OF | awk '{print $1}') || die "could not access /var/log/ldap/slapd.log"
fi
MAIL_ERRLOG_LINECOUNT=0
if [ -e /var/log/mail.err ]; then
MAIL_ERRLOG_LINECOUNT=$(wc -l /var/log/mail.err 2>>$TEST_OF | awk '{print $1}') || die "could not access /var/log/mail.err"
fi
MAIL_LOG_LINECOUNT=0
if [ -e /var/log/mail.log ]; then
MAIL_LOG_LINECOUNT=$(wc -l /var/log/mail.log 2>>$TEST_OF | awk '{print $1}') || die "could not access /var/log/mail.log"
fi
DOVECOT_LOG_LINECOUNT=$(doveadm log errors 2>>$TEST_OF | wc -l | awk '{print $1}') || die "could not access doveadm error logs"
}
start_mail_capture() {
local email="$1"
local newdir="$(dovecot_mailbox_home "$email")/new"
record "[Start mail capture $email]"
DOVECOT_CAPTURE_USER="$email"
DOVECOT_CAPTURE_FILECOUNT=0
if [ -e "$newdir" ]; then
DOVECOT_CAPTURE_FILECOUNT=$(ls "$newdir" 2>>$TEST_OF | wc -l)
[ $? -ne 0 ] && die "Error accessing mailbox of $email"
fi
record "mailbox: $(dirname $newdir)"
record "mailbox has $DOVECOT_CAPTURE_FILECOUNT files"
}
dump_capture_logs() {
# dump log files
record "[capture log dump]"
echo ""
echo "============= SYSLOG ================"
tail --lines=+$SYS_LOG_LINECOUNT /var/log/syslog 2>>$TEST_OF
echo ""
echo "============= SLAPD ================="
tail --lines=+$SLAPD_LOG_LINECOUNT /var/log/ldap/slapd.log 2>>$TEST_OF
echo ""
echo "============= MAIL.ERR =============="
tail --lines=+$MAIL_ERRLOG_LINECOUNT /var/log/mail.err 2>>$TEST_OF
echo ""
echo "============= MAIL.LOG =============="
tail --lines=+$MAIL_LOG_LINECOUNT /var/log/mail.log 2>>$TEST_OF
echo ""
echo "============= DOVECOT ERRORS =============="
doveadm log errors | tail --lines=+$DOVECOT_LOG_LINECOUNT 2>>$TEST_OF
}
detect_syslog_error() {
record
record "[Detect syslog errors]"
local count
let count="$SYS_LOG_LINECOUNT + 1"
tail --lines=+$count /var/log/syslog 2>>$TEST_OF | (
let ec=0 # error count
while read line; do
awk '
/status=(bounced|deferred|undeliverable)/ { exit 1 }
!/postfix\/qmgr/ && /warning:/ { exit 1 }
/(fatal|reject|error):/ { exit 1 }
/Error in / { exit 1 }
/named\[\d+\]:.* verify failed/ { exit 1 }
' \
>>$TEST_OF 2>&1 <<< "$line"
if [ $? -eq 1 ]; then
let ec+=1
record "$F_DANGER[ERROR] $line$F_RESET"
else
record "[ OK] $line"
fi
done
[ $ec -gt 0 ] && exit 0
exit 1 # no errors
)
local x=( ${PIPESTATUS[*]} )
[ ${x[0]} -ne 0 ] && die "Could not read /var/log/syslog"
return ${x[1]}
}
detect_slapd_log_error() {
record
record "[Detect slapd log errors]"
local count
let count="SLAPD_LOG_LINECOUNT + 1"
tail --lines=+$count /var/log/ldap/slapd.log 2>>$TEST_OF | (
let ec=0 # error count
let wc=0 # warning count
let ignored=0
while read line; do
# slapd error 68 = "entry already exists". Mark it as a
# warning because code often attempts to add entries
# silently ignoring the error, which is expected behavior
#
# slapd error 32 = "no such object". Mark it as a warning
# because code often attempts to resolve a dn (eg member)
# that is orphaned, so no entry exists. Code may or may
# not care about this.
#
# slapd error 4 - "size limit exceeded". Mark it as a warning
# because code often attempts to find just 1 entry so sets
# the limit to 1 purposefully.
#
# slapd error 20 - "attribute or value exists". Mark it as a
# warning becuase code often attempts to add a new value
# to an existing attribute and doesn't care if the new
# value fails to add because it already exists.
#
awk '
/SEARCH RESULT.*err=(32|4) / { exit 2}
/RESULT.*err=(68|20) / { exit 2 }
/ not indexed/ { exit 2 }
/RESULT.*err=[^0]/ { exit 1 }
/(get|test)_filter/ { exit 3 }
/mdb_(filter|list)_candidates/ { exit 3 }
/:( | #011| )(AND|OR|EQUALITY)/ { exit 3 }
' \
>>$TEST_OF 2>&1 <<< "$line"
r=$?
if [ $r -eq 1 ]; then
let ec+=1
record "$F_DANGER[ERROR] $line$F_RESET"
elif [ $r -eq 2 ]; then
let wc+=1
record "$F_WARN[WARN ] $line$F_RESET"
elif [ $r -eq 3 ]; then
let ignored+=1
else
record "[OK ] $line"
fi
done
record "$ignored unreported/ignored log lines"
[ $ec -gt 0 ] && exit 0
exit 1 # no errors
)
local x=( ${PIPESTATUS[*]} )
[ ${x[0]} -ne 0 ] && die "Could not read /var/log/ldap/slapd.log"
return ${x[1]}
}
detect_dovecot_log_error() {
record
record "[Detect dovecot log errors]"
local count
let count="$MAIL_LOG_LINECOUNT + 1"
if [ ! -e /var/log/mail.log ]; then
return 0
fi
# prefer mail.log over `dovadm log errors` because the latter does
# not have as much output - it's helpful to have success logs when
# diagnosing logs...
cat /var/log/mail.log 2>>$TEST_OF | tail --lines=+$count | (
let ec=0 # error count
let ignored=0
while read line; do
awk '
/LDAP server, reconnecting/ { exit 2 }
/postfix/ { exit 2 }
/auth failed/ { exit 1 }
/ Error: / { exit 1 }
' \
>>$TEST_OF 2>&1 <<< "$line"
r=$?
if [ $r -eq 1 ]; then
let ec+=1
record "$F_DANGER[ERROR] $line$F_RESET"
elif [ $r -eq 2 ]; then
let ignored+=1
else
record "[ OK] $line"
fi
done
record "$ignored unreported/ignored log lines"
[ $ec -gt 0 ] && exit 0
exit 1 # no errors
)
local x=( ${PIPESTATUS[*]} )
[ ${x[0]} -ne 0 -o ${x[1]} -ne 0 ] && die "Could not read mail log"
return ${x[2]}
}
check_logs() {
local assert="${1:-false}"
[ "$1" == "true" -o "$1" == "false" ] && shift
local types=($@)
[ ${#types[@]} -eq 0 ] && types=(syslog slapd mail)
# flush records
kill -HUP $(cat /var/run/rsyslogd.pid)
sleep 2
if array_contains syslog ${types[@]}; then
detect_syslog_error && $assert &&
test_failure "detected errors in syslog"
fi
if array_contains slapd ${types[@]}; then
detect_slapd_log_error && $assert &&
test_failure "detected errors in slapd log"
fi
if array_contains mail ${types[@]}; then
detect_dovecot_log_error && $assert &&
test_failure "detected errors in dovecot log"
fi
}
assert_check_logs() {
check_logs true $@
}
grep_postfix_log() {
local msg="$1"
local count
let count="$SYS_LOG_LINECOUNT + 1"
tail --lines=+$count /var/log/syslog 2>>$TEST_OF | grep -iF "$msg" >/dev/null 2>>$TEST_OF
return $?
}
wait_mail() {
local x mail_files elapsed max_s="${1:-60}"
let elapsed=0
record "[Waiting for mail to $DOVECOT_CAPTURE_USER]"
while [ $elapsed -lt $max_s ]; do
mail_files=( $(get_captured_mail_files) )
[ ${#mail_files[*]} -gt 0 ] && break
sleep 1
let elapsed+=1
let x="$elapsed % 10"
[ $x -eq 0 ] && record "...~${elapsed} seconds has passed"
done
if [ $elapsed -ge $max_s ]; then
record "Timeout waiting for mail"
return 1
fi
record "new mail files:"
for x in ${mail_files[@]}; do
record "$x"
done
}
get_captured_mail_files() {
local newdir="$(dovecot_mailbox_home "$DOVECOT_CAPTURE_USER")/new"
local count
let count="$DOVECOT_CAPTURE_FILECOUNT + 1"
[ ! -e "$newdir" ] && return 0
# output absolute path names
local file
for file in $(ls "$newdir" 2>>$TEST_OF | tail --lines=+${count}); do
echo "$newdir/$file"
done
}
record_captured_mail() {
local files=( $(get_captured_mail_files) )
local file
for file in ${files[@]}; do
record
record "[Captured mail file: $file]"
cat "$file" >>$TEST_OF 2>&1
done
}
sendmail_bv_send() {
# test sending mail, but don't actually send it...
local recpt="$1"
local timeout="$2"
local bvfrom from="$3"
# delivery status is emailed back to us, or 'from' if supplied
clear_postfix_queue
if [ -z "$from" ]; then
ensure_root_user
start_mail_capture "$ROOT"
else
bvfrom="-f $from"
start_mail_capture "$from"
fi
record "[Running sendmail -bv $bvfrom]"
sendmail $bvfrom -bv "$recpt" >>$TEST_OF 2>&1
if [ $? -ne 0 ]; then
test_failure "Error executing sendmail"
else
wait_mail $timeout || test_failure "Timeout waiting for delivery report"
fi
}
assert_python_success() {
local code="$1"
local output="$2"
record "$output"
record
record "python exit code: $code"
if [ $code -ne 0 ]; then
test_failure "unable to process mail: $(python_error "$output")"
return 1
fi
return 0
}
assert_python_failure() {
local code="$1"
local output="$2"
shift; shift
record "$output"
record
record "python exit code: $code"
if [ $code -eq 0 ]; then
test_failure "python succeeded but expected failure"
return 1
fi
local look_for
for look_for; do
if [ ! -z "$look_for" ] && ! grep "$look_for" <<< "$output" 1>/dev/null
then
test_failure "unexpected python failure: $(python_error "$output")"
return 1
fi
done
return 0
}

View File

@@ -0,0 +1,175 @@
# -*- indent-tabs-mode: t; tab-width: 4; -*-
# Available REST calls:
#
# general curl format:
# curl -X <b>VERB</b> [-d "<b>parameters</b>"] --user {email}:{password} https://{{hostname}}/admin/mail/users[<b>action</b>]
# ALIASES:
# curl -X GET https://{{hostname}}/admin/mail/aliases?format=json
# curl -X POST -d "address=new_alias@mydomail.com" -d "forwards_to=my_email@mydomain.com" https://{{hostname}}/admin/mail/aliases/add
# curl -X POST -d "address=new_alias@mydomail.com" https://{{hostname}}/admin/mail/aliases/remove
# USERS:
# curl -X GET https://{{hostname}}/admin/mail/users?format=json
# curl -X POST -d "email=new_user@mydomail.com" -d "password=s3curE_pa5Sw0rD" https://{{hostname}}/admin/mail/users/add
# curl -X POST -d "email=new_user@mydomail.com" https://{{hostname}}/admin/mail/users/remove
# curl -X POST -d "email=new_user@mydomail.com" -d "privilege=admin" https://{{hostname}}/admin/mail/users/privileges/add
# curl -X POST -d "email=new_user@mydomail.com" https://{{hostname}}/admin/mail/users/privileges/remove
mgmt_start() {
# Must be called before performing any REST calls
local domain="${1:-somedomain.com}"
MGMT_ADMIN_EMAIL="test_admin@$domain"
MGMT_ADMIN_PW="$(generate_password)"
delete_user "$MGMT_ADMIN_EMAIL"
record "[Creating a new account with admin rights for management tests]"
create_user "$MGMT_ADMIN_EMAIL" "$MGMT_ADMIN_PW" "admin"
MGMT_ADMIN_DN="$ATTR_DN"
record "Created: $MGMT_ADMIN_EMAIL at $MGMT_ADMIN_DN"
}
mgmt_end() {
# Clean up after mgmt_start
delete_user "$MGMT_ADMIN_EMAIL"
}
mgmt_rest() {
# Issue a REST call to the management subsystem
local verb="$1" # eg "POST"
local uri="$2" # eg "/mail/users/add"
shift; shift; # remaining arguments are data
local auth_user="${MGMT_ADMIN_EMAIL}"
local auth_pass="${MGMT_ADMIN_PW}"
local url="https://$PRIMARY_HOSTNAME${uri}"
local data=()
local item output
for item; do data+=("--data-urlencode" "$item"); done
record "spawn: curl -w \"%{http_code}\" -X $verb --user \"${auth_user}:xxx\" ${data[@]} $url"
output=$(curl -s -S -w "%{http_code}" -X $verb --user "${auth_user}:${auth_pass}" "${data[@]}" $url 2>>$TEST_OF)
local code=$?
# http status is last 3 characters of output, extract it
REST_HTTP_CODE=$(awk '{S=substr($0,length($0)-2)} END {print S}' <<<"$output")
REST_OUTPUT=$(awk 'BEGIN{L=""}{ if(L!="") print L; L=$0 } END { print substr(L,1,length(L)-3) }' <<<"$output")
REST_ERROR=""
[ -z "$REST_HTTP_CODE" ] && REST_HTTP_CODE="000"
if [ $code -ne 0 ]; then
if [ $code -ne 16 -o $REST_HTTP_CODE -ne 200 ]; then
REST_ERROR="CURL failed with code $code"
record "${F_DANGER}$REST_ERROR${F_RESET}"
record "$output"
return 1
fi
fi
if [ $REST_HTTP_CODE -lt 200 -o $REST_HTTP_CODE -ge 300 ]; then
REST_ERROR="REST status $REST_HTTP_CODE: $REST_OUTPUT"
record "${F_DANGER}$REST_ERROR${F_RESET}"
return 2
fi
record "CURL succeded, HTTP status $REST_HTTP_CODE"
record "$output"
return 0
}
mgmt_create_user() {
local email="$1"
local pass="${2:-$email}"
local delete_first="${3:-yes}"
# ensure the user is deleted (clean test run)
if [ "$delete_first" == "yes" ]; then
delete_user "$email"
fi
record "[create user $email]"
mgmt_rest POST /admin/mail/users/add "email=$email" "password=$pass"
return $?
}
mgmt_assert_create_user() {
local email="$1"
local pass="$2"
local delete_first="${3}"
if ! mgmt_create_user "$email" "$pass" "$delete_first"; then
test_failure "Unable to create user $email"
test_failure "${REST_ERROR}"
return 1
fi
return 0
}
mgmt_delete_user() {
local email="$1"
record "[delete user $email]"
mgmt_rest POST /admin/mail/users/remove "email=$email"
return $?
}
mgmt_assert_delete_user() {
local email="$1"
if ! mgmt_delete_user "$email"; then
test_failure "Unable to cleanup/delete user $email"
test_failure "$REST_ERROR"
return 1
fi
return 0
}
mgmt_create_alias_group() {
local alias="$1"
shift
record "[Create new alias group $alias]"
record "members: $@"
# ensure the group is deleted (clean test run)
record "Try deleting any existing entry"
if ! mgmt_rest POST /admin/mail/aliases/remove "address=$alias"; then
get_attribute "$LDAP_ALIASES_BASE" "mail=$alias" "dn"
if [ ! -z "$ATTR_DN" ]; then
delete_dn "$ATTR_DN"
fi
fi
record "Create the alias group"
local members="$1" member
shift
for member; do members="${members},${member}"; done
mgmt_rest POST /admin/mail/aliases/add "address=$alias" "forwards_to=$members"
return $?
}
mgmt_assert_create_alias_group() {
local alias="$1"
shift
if ! mgmt_create_alias_group "$alias" "$@"; then
test_failure "Unable to create alias group $alias"
test_failure "${REST_ERROR}"
return 1
fi
return 0
}
mgmt_delete_alias_group() {
local alias="$1"
record "[Delete alias group $alias]"
mgmt_rest POST /admin/mail/aliases/remove "address=$alias"
return $?
}
mgmt_assert_delete_alias_group() {
local alias="$1"
if ! mgmt_delete_alias_group "$alias"; then
test_failure "Unable to cleanup/delete alias group $alias"
test_failure "$REST_ERROR"
return 1
fi
return 0
}

233
tests/suites/ldap-access.sh Normal file
View File

@@ -0,0 +1,233 @@
# -*- indent-tabs-mode: t; tab-width: 4; -*-
#
# Access assertions:
# service accounts, except management:
# can bind but not change passwords, including their own
# can read all attributes of all users but not userPassword
# can not write any user attributes, include shadowLastChange
# can read config subtree (permitted-senders, domains)
# no access to services subtree, except their own dn
# users:
# can bind and change their own password
# can read and change their own shadowLastChange
# can read attributess of all users except mailaccess
# no access to config subtree
# no access to services subtree
# other:
# no anonymous binds to root DSE
# no anonymous binds to database
#
test_user_change_password() {
# users should be able to change their own passwords
test_start "user-change-password"
# create regular user with password "alice"
local alice="alice@somedomain.com"
create_user "$alice" "alice"
local alice_dn="$ATTR_DN"
# bind as alice and update userPassword
assert_w_access "$alice_dn" "$alice_dn" "alice" write "userPassword=$(slappasswd_hash "alice-new")"
delete_user "$alice"
test_end
}
test_user_access() {
# 1. can read attributess of all users except mailaccess
# 2. can read and change their own shadowLastChange
# 3. no access to config subtree
# 4. no access to services subtree
test_start "user-access"
# create regular user's alice and bob
local alice="alice@somedomain.com"
create_user "alice@somedomain.com" "alice"
local alice_dn="$ATTR_DN"
local bob="bob@somedomain.com"
create_user "bob@somedomain.com" "bob"
local bob_dn="$ATTR_DN"
# alice should be able to set her own shadowLastChange
assert_w_access "$alice_dn" "$alice_dn" "alice" write "shadowLastChange=0"
# test that alice can read her own attributes
assert_r_access "$alice_dn" "$alice_dn" "alice" read mail maildrop cn sn shadowLastChange
# alice should not have access to her own mailaccess, though
assert_r_access "$alice_dn" "$alice_dn" "alice" no-read mailaccess
# test that alice cannot change her own select attributes
assert_w_access "$alice_dn" "$alice_dn" "alice"
# test that alice can read bob's attributes
assert_r_access "$bob_dn" "$alice_dn" "alice" read mail maildrop cn sn
# alice does not have access to bob's mailaccess though
assert_r_access "$bob_dn" "$alice_dn" "alice" no-read mailaccess
# test that alice cannot change bob's attributes
assert_w_access "$bob_dn" "$alice_dn" "alice"
# test that alice cannot read a service account's attributes
assert_r_access "$LDAP_POSTFIX_DN" "$alice_dn" "alice"
# test that alice cannot read config entries
assert_r_access "dc=somedomain.com,$LDAP_DOMAINS_BASE" "$alice_dn" "alice"
assert_r_access "$LDAP_PERMITTED_SENDERS_BASE" "$alice_dn" "alice"
# test that alice cannot find anything searching config
test_search "$LDAP_CONFIG_BASE" "$alice_dn" "alice"
[ $SEARCH_DN_COUNT -gt 0 ] && test_failure "Users should not be able to search config"
# test that alice cannot find anything searching config domains
test_search "$LDAP_DOMAINS_BASE" "$alice_dn" "alice"
[ $SEARCH_DN_COUNT -gt 0 ] && test_failure "Users should not be able to search config domains"
# test that alice cannot find anything searching services
test_search "$LDAP_SERVICES_BASE" "$alice_dn" "alice"
[ $SEARCH_DN_COUNT -gt 0 ] && test_failure "Users should not be able to search services"
delete_user "$alice"
delete_user "$bob"
test_end
}
test_service_change_password() {
# service accounts should not be able to change other user's
# passwords
# service accounts should not be able to change their own password
test_start "service-change-password"
# create regular user with password "alice"
local alice="alice@somedomain.com"
create_user "alice@somedomain.com" "alice"
local alice_dn="$ATTR_DN"
# create a test service account
create_service_account "test" "test"
local service_dn="$ATTR_DN"
# update userPassword of user using service account
assert_w_access "$alice_dn" "$LDAP_POSTFIX_DN" "$LDAP_POSTFIX_PASSWORD" no-write "userPassword=$(slappasswd_hash "alice-new")"
# update userPassword of service account using service account
assert_w_access "$service_dn" "$service_dn" "test" no-write "userPassword=$(slappasswd_hash "test-new")"
delete_user "$alice"
delete_service_account "test"
test_end
}
test_service_access() {
# service accounts should have read-only access to all attributes
# of all users except userPassword
# can not write any user attributes, include shadowLastChange
# can read config subtree (permitted-senders, domains)
# no access to services subtree, except their own dn
test_start "service-access"
# create regular user with password "alice"
local alice="alice@somedomain.com"
create_user "alice@somedomain.com" "alice"
# create a test service account
create_service_account "test" "test"
local service_dn="$ATTR_DN"
# Use service account to find alice
record "[Use service account to find alice]"
get_attribute "$LDAP_USERS_BASE" "mail=${alice}" dn sub "$LDAP_POSTFIX_DN" "$LDAP_POSTFIX_PASSWORD"
if [ -z "$ATTR_DN" ]; then
test_failure "Unable to search for user account using service account"
else
local alice_dn="$ATTR_DN"
# set shadowLastChange on alice's entry (to test reading it back)
assert_w_access "$alice_dn" "$alice_dn" "alice" write "shadowLastChange=0"
# check that service account can read user attributes
assert_r_access "$alice_dn" "$LDAP_POSTFIX_DN" "$LDAP_POSTFIX_PASSWORD" read mail maildrop uid cn sn shadowLastChange
# service account should not be able to read user's userPassword
assert_r_access "$alice_dn" "$LDAP_POSTFIX_DN" "$LDAP_POSTFIX_PASSWORD" no-read userPassword
# service accounts cannot change user attributes
assert_w_access "$alice_dn" "$LDAP_POSTFIX_DN" "$LDAP_POSTFIX_PASSWORD"
assert_w_access "$alice_dn" "$LDAP_POSTFIX_DN" "$LDAP_POSTFIX_PASSWORD" no-write "shadowLastChange=1"
fi
# service accounts can read config subtree (permitted-senders, domains)
assert_r_access "dc=somedomain.com,$LDAP_DOMAINS_BASE" "$LDAP_POSTFIX_DN" "$LDAP_POSTFIX_PASSWORD" read dc
# service accounts can search and find things in the config subtree
test_search "$LDAP_CONFIG_BASE" "$LDAP_POSTFIX_DN" "$LDAP_POSTFIX_PASSWORD" sub
[ $SEARCH_DN_COUNT -lt 4 ] && test_failure "Service accounts should be able to search config"
# service accounts can read attributes in their own dn
assert_r_access "$LDAP_POSTFIX_DN" "$LDAP_POSTFIX_DN" "$LDAP_POSTFIX_PASSWORD" read cn description
# ... but not userPassword
assert_r_access "$LDAP_POSTFIX_DN" "$LDAP_POSTFIX_DN" "$LDAP_POSTFIX_PASSWORD" no-read userPassword
# services cannot read other service's attributes
assert_r_access "$service_dn" "$LDAP_POSTFIX_DN" "$LDAP_POSTFIX_PASSWORD" no-read cn description userPassword
delete_user "$alice"
delete_service_account "test"
test_end
}
test_root_dse() {
# no anonymous binds to root dse
test_start "root-dse"
record "[bind anonymously to root dse]"
ldapsearch -H $LDAP_URL -x -b "" -s base >>$TEST_OF 2>&1
local r=$?
if [ $r -eq 0 ]; then
test_failure "Anonymous access to root dse should not be permitted"
elif [ $r -eq 48 ]; then
# 48=inappropriate authentication (anon binds not allowed)
test_success
else
die "Error accessing root dse"
fi
test_end
}
test_anon_bind() {
test_start "anon-bind"
record "[bind anonymously to $LDAP_BASE]"
ldapsearch -H $LDAP_URL -x -b "$LDAP_BASE" -s base >>$TEST_OF 2>&1
local r=$?
if [ $r -eq 0 ]; then
test_failure "Anonymous access should not be permitted"
elif [ $r -eq 48 ]; then
# 48=inappropriate authentication (anon binds not allowed)
test_success
else
die "Error accessing $LDAP_BASE"
fi
test_end
}
suite_start "ldap-access"
test_user_change_password
test_user_access
test_service_change_password
test_service_access
test_root_dse
test_anon_bind
suite_end

View File

@@ -0,0 +1,151 @@
# -*- indent-tabs-mode: t; tab-width: 4; -*-
exe_test() {
# run an executable and assert success or failure
# argument 1 must be:
# "ZERO_RC" to assert the return code was 0
# "NONZERO_RC" to assert the return code was not 0
# argument 2 is a description of the test for logging
# argument 3 and higher are the executable and arguments
local result_type=$1
shift
local desc="$1"
shift
test_start "$desc"
record "[CMD: $@]"
"$@" >>"$TEST_OF" 2>&1
local code=$?
case $result_type in
ZERO_RC)
if [ $code -ne 0 ]; then
test_failure "expected zero return code, got $code"
else
test_success
fi
;;
NONZERO_RC)
if [ $code -eq 0 ]; then
test_failure "expected non-zero return code"
else
test_success
fi
;;
*)
test_failure "unknown TEST type '$result_type'"
;;
esac
test_end
}
tests() {
# TLS: auth search to (local)host - expect success
exe_test ZERO_RC "TLS-auth-host" \
ldapsearch -d 1 -b "dc=mailinabox" -H ldaps://$PRIMARY_HOSTNAME/ -x -D "$LDAP_ADMIN_DN" -w "$LDAP_ADMIN_PASSWORD"
# TLS: auth search to localhost - expect failure ("hostname does not match CN in peer certificate")
exe_test NONZERO_RC "TLS-auth-local" \
ldapsearch -d 1 -b "dc=mailinabox" -H ldaps://127.0.0.1/ -x -D "$LDAP_ADMIN_DN" -w "$LDAP_ADMIN_PASSWORD"
# TLS: anon search - expect failure (anon bind disallowed)
exe_test NONZERO_RC "TLS-anon-host" \
ldapsearch -d 1 -b "dc=mailinabox" -H ldaps://$PRIMARY_HOSTNAME/ -x
# CLEAR: auth search to host - expected failure (not listening there)
exe_test NONZERO_RC "CLEAR-auth-host" \
ldapsearch -d 1 -b "dc=mailinabox" -H ldap://$PRIVATE_IP/ -x -D "$LDAP_ADMIN_DN" -w "$LDAP_ADMIN_PASSWORD"
# CLEAR: auth search to localhost - expect success
exe_test ZERO_RC "CLEAR-auth-local" \
ldapsearch -d 1 -b "dc=mailinabox" -H ldap://127.0.0.1/ -x -D "$LDAP_ADMIN_DN" -w "$LDAP_ADMIN_PASSWORD"
# CLEAR: anon search - expect failure (anon bind disallowed)
exe_test NONZERO_RC "CLEAR-anon-local" \
ldapsearch -d 1 -b "dc=mailinabox" -H ldap://127.0.0.1/ -x
# STARTTLS: auth search to localhost - expected failure ("hostname does not match CN in peer certificate")
exe_test NONZERO_RC "STARTTLS-auth-local" \
ldapsearch -d 1 -b "dc=mailinabox" -H ldap://127.0.0.1/ -x -D "$LDAP_ADMIN_DN" -w "$LDAP_ADMIN_PASSWORD" -ZZ
# STARTTLS: auth search to host - expected failure (not listening there)
exe_test NONZERO_RC "STARTTLS-auth-host" \
ldapsearch -d 1 -b "dc=mailinabox" -H ldap://$PRIVATE_IP/ -x -D "$LDAP_ADMIN_DN" -w "$LDAP_ADMIN_PASSWORD" -ZZ
}
test_fail2ban() {
test_start "fail2ban"
# reset fail2ban
record "[reset fail2ban]"
fail2ban-client unban --all >>$TEST_OF 2>&1 ||
test_failure "Unable to execute unban --all"
# create regular user with password "alice"
local alice="alice@somedomain.com"
create_user "$alice" "alice"
local alice_dn="$ATTR_DN"
# log in a bunch of times with wrong password
local n=0
local total=25
local banned=no
record '[log in 25 times with wrong password]'
while ! have_test_failures && [ $n -lt $total ]; do
ldapsearch -H $LDAP_URL -D "$alice_dn" -w "bad-alice" -b "$LDAP_USERS_BASE" -s base "(objectClass=*)" 1>>$TEST_OF 2>&1
local code=$?
record "TRY $n: result code $code"
if [ $code -eq 255 -a $n -gt 5 ]; then
# banned - could not connect
banned=yes
break
elif [ $code -ne 49 ]; then
test_failure "Expected error code 49 (invalidCredentials), but got $code"
continue
fi
let n+=1
if [ $n -lt $total ]; then
record "sleep 1"
sleep 1
fi
done
if ! have_test_failures && [ "$banned" == "no" ]; then
# wait for fail2ban to ban
record "[waiting for fail2ban]"
record "sleep 5"
sleep 5
ldapsearch -H ldap://$PRIVATE_IP -D "$alice_dn" -w "bad-alice" -b "$LDAP_USERS_BASE" -s base "(objectClass=*)" 1>>$TEST_OF 2>&1
local code=$?
record "$n result: $code"
if [ $code -ne 255 ]; then
test_failure "Expected to be banned after repeated login failures, but wasn't"
fi
fi
# delete alice
delete_user "$alice"
# reset fail2ban
record "[reset fail2ban]"
fail2ban-client unban --all >>$TEST_OF 2>&1 ||
test_failure "Unable to execute unban --all"
# done
test_end
}
suite_start "ldap-connection"
tests
test_fail2ban
suite_end

199
tests/suites/mail-access.sh Normal file
View File

@@ -0,0 +1,199 @@
# -*- indent-tabs-mode: t; tab-width: 4; -*-
#
_test_greylisting_x() {
# helper function sends mail and checks that it was greylisted
local email_to="$1"
local email_from="$2"
start_log_capture
start_mail_capture "$email_to"
record "[Send mail anonymously TO $email_to FROM $email_from]"
local output
output="$($PYMAIL -no-delete -f $email_from -to $email_to '' $PRIVATE_IP '' '' 2>&1)"
local code=$?
if [ $code -eq 0 ]; then
wait_mail
local file=( $(get_captured_mail_files) )
record "[Check captured mail for X-Greylist header]"
if ! grep "X-Greylist: delayed" <"$file" >/dev/null; then
record "not found"
test_failure "message not greylisted - X-Greylist header missing"
record_captured_mail
else
record "found"
fi
else
assert_python_failure $code "$output" "SMTPRecipientsRefused" "Greylisted"
fi
check_logs
}
postgrey_reset() {
# when postgrey receives a message for processing that is suspect,
# it will:
# 1. initally reject it
# 2. after a delay, permit delivery (end entity must resend),
# but with a X-Greyist header
# 3. subsequent deliveries will succeed with no header
# modifications
#
# because of #3, reset postgrey to establish a "clean" greylisting
# testing scenario
#
record "[Reset postgrey]"
if [ ! -d "/var/lib/postgrey" ]; then
die "Postgrey database directory /var/lib/postgrey does not exist!"
fi
systemctl stop postgrey >>$TEST_OF 2>&1 || die "unble to stop postgrey"
if ! rm -f /var/lib/postgrey/* >>$TEST_OF 2>&1; then
systemctl start postgrey >>$TEST_OF 2>&1
die "unable to remove the postgrey database files"
fi
systemctl start postgrey >>$TEST_OF 2>&1 || die "unble to start postgrey"
}
test_greylisting() {
# test that mail is delayed by greylisting
test_start "greylisting"
# reset postgrey's database to start the cycle over
postgrey_reset
# create standard user alice
local alice="alice@somedomain.com"
create_user "$alice" "alice"
# IMPORTANT: bob's domain must be from one that has no SPF record
# in DNS. At the time of creation of this script, yahoo.com did
# not...
local bob="bob@yahoo.com"
# send to alice anonymously from bob
_test_greylisting_x "$alice" "$bob"
delete_user "$alice"
test_end
}
test_relay_prohibited() {
# test that the server does not relay
test_start "relay-prohibited"
start_log_capture
record "[Attempt relaying mail anonymously]"
local output
output="$($PYMAIL -no-delete -f joe@badguy.com -to naive@gmail.com '' $PRIVATE_IP '' '' 2>&1)"
assert_python_failure $? "$output" "SMTPRecipientsRefused" "Relay access denied"
check_logs
test_end
}
test_spf() {
# test mail rejection due to SPF policy of FROM address
test_start "spf"
# create standard user alice
local alice="alice@somedomain.com"
create_user "$alice" "alice"
# who we will impersonate
local from="test@google.com"
local domain=$(awk -F@ '{print $2}' <<<"$from")
# send to alice anonymously from imposter
start_log_capture
start_mail_capture "$alice"
record "[Test SPF for $domain FROM $from TO $alice]"
local output
output="$($PYMAIL -no-delete -f $from -to $alice '' $PRIVATE_IP '' '' 2>&1)"
local code=$?
if ! assert_python_failure $code "$output" "SMTPRecipientsRefused" "SPF" && [ $code -eq 0 ]
then
wait_mail
record_captured_mail
fi
check_logs
delete_user "$alice"
test_end
}
test_mailbox_pipe() {
# postfix allows piped commands in aliases for local processing,
# which is a serious security issue. test that pipes are not
# permitted or don't work
test_start "mailbox-pipe"
# create standard user alice
local alice="alice@somedomain.com"
create_user "$alice" "alice"
local alice_dn="$ATTR_DN"
# create the program to handle piped mail
local cmd="/tmp/pipedrop.$$.sh"
local outfile="/tmp/pipedrop.$$.out"
cat 2>>$TEST_OF >$cmd <<EOF
#!/bin/bash
cat > $outfile
EOF
chmod 755 $cmd
rm -f $outfile
# add a piped maildrop
record "[Add pipe command as alice's maildrop]"
ldapmodify -H $LDAP_URL -x -D "$LDAP_ADMIN_DN" -w "$LDAP_ADMIN_PASSWORD" >>$TEST_OF 2>&1 <<EOF
dn: $alice_dn
replace: maildrop
maildrop: |$cmd
EOF
[ $? -ne 0 ] && die "Could not modify ${alice}'s maildrop"
# send an email message to alice
start_log_capture
record "[Send an email to $alice - test pipe]"
local output
output="$($PYMAIL -no-delete $PRIVATE_IP $alice alice 2>&1)"
local code=$?
if [ $code -ne 0 ]; then
assert_python_failure $code "$output" SMTPAuthenticationError
check_logs
else
sleep 5
if grep_postfix_log "User doesn't exist: |$cmd@"; then
# ok
check_logs
else
assert_check_logs
fi
if [ -e $outfile ]; then
test_failure "a maildrop containing a pipe was executed by postfix"
fi
fi
delete_user "$alice"
rm -f $cmd
rm -f $outfile
test_end
}
suite_start "mail-access"
test_greylisting
test_relay_prohibited
test_spf
test_mailbox_pipe
suite_end

View File

@@ -0,0 +1,329 @@
# -*- indent-tabs-mode: t; tab-width: 4; -*-
# mail alias tests
#
test_shared_user_alias_login() {
# a login attempt should fail when using 'mail' aliases that map
# to two or more users
test_start "shared-user-alias-login"
# create standard users alice and bob
local alice="alice@somedomain.com"
local bob="bob@anotherdomain.com"
create_user "$alice" "alice"
local alice_dn="$ATTR_DN"
create_user "$bob" "bob"
local bob_dn="$ATTR_DN"
# add common alias to alice and bob
local alias="us@somedomain.com"
add_alias $alice_dn $alias user
add_alias $bob_dn $alias user
start_log_capture
record "[Log in as alias to postfix]"
local output
local subject="Mail-In-A-Box test $(generate_uuid)"
# login as the alias to postfix - should fail
output="$($PYMAIL -subj "$subject" -no-delete $PRIVATE_IP $alias alice 2>&1)"
assert_python_failure $? "$output" "SMTPAuthenticationError"
# login as the alias to dovecot - should fail
record "[Log in as alias to dovecot]"
local timeout=""
if have_test_failures; then
timeout="-timeout 0"
fi
output="$($PYMAIL -subj "$subject" $timeout -no-send $PRIVATE_IP $alias alice 2>&1)"
assert_python_failure $? "$output" "authentication failure"
check_logs
delete_user "$alice"
delete_user "$bob"
test_end
}
test_alias_group_member_login() {
# a login attempt should fail when using an alias defined in a
# mailGroup type alias
test_start "alias-group-member-login"
# create standard user alice
local alice="alice@somedomain.com"
create_user "$alice" "alice"
local alice_dn="$ATTR_DN"
# create alias group with alice in it
local alias="us@somedomain.com"
create_alias_group "$alias" "$alice_dn"
start_log_capture
record "[Log in as alias to postfix]"
local output
local subject="Mail-In-A-Box test $(generate_uuid)"
# login as the alias to postfix - should fail
output="$($PYMAIL -subj "$subject" -no-delete $PRIVATE_IP $alias alice 2>&1)"
assert_python_failure $? "$output" "SMTPAuthenticationError"
# login as the alias to dovecot - should fail
record "[Log in as alias to dovecot]"
local timeout=""
if have_test_failures; then
timeout="-timeout 0"
fi
output="$($PYMAIL -subj "$subject" $timeout -no-send $PRIVATE_IP $alias alice 2>&1)"
assert_python_failure $? "$output" "AUTHENTICATIONFAILED"
check_logs
delete_user "$alice"
delete_alias_group "$alias"
test_end
}
test_shared_alias_delivery() {
# mail sent to the shared alias of two users (eg. postmaster),
# should be sent to both users
test_start "shared-alias-delivery"
# create standard users alice, bob, and mary
local alice="alice@somedomain.com"
local bob="bob@anotherdomain.com"
local mary="mary@anotherdomain.com"
create_user "$alice" "alice"
local alice_dn="$ATTR_DN"
create_user "$bob" "bob"
local bob_dn="$ATTR_DN"
create_user "$mary" "mary"
# add common alias to alice and bob
local alias="us@somedomain.com"
create_alias_group $alias $alice_dn $bob_dn
# login as mary and send to alias
start_log_capture
record "[Sending mail to alias]"
local output
local subject="Mail-In-A-Box test $(generate_uuid)"
output="$($PYMAIL -subj "$subject" -no-delete -to $alias na $PRIVATE_IP $mary mary 2>&1)"
if assert_python_success $? "$output"; then
# check that alice and bob received it by deleting the mail in
# both mailboxes
record "[Delete mail alice's mailbox]"
output="$($PYMAIL -subj "$subject" -no-send $PRIVATE_IP $alice alice 2>&1)"
assert_python_success $? "$output"
record "[Delete mail bob's mailbox]"
output="$($PYMAIL -subj "$subject" -no-send $PRIVATE_IP $bob bob 2>&1)"
assert_python_success $? "$output"
fi
assert_check_logs
delete_user "$alice"
delete_user "$bob"
delete_user "$mary"
delete_alias_group $alias
test_end
}
test_trial_nonlocal_alias_delivery() {
# verify that mail sent to an alias with a non-local address
# (rfc822MailMember) can be delivered
test_start "trial-nonlocal-alias-delivery"
# add alias
local alias="external@somedomain.com"
create_alias_group $alias "test@google.com"
# trail send...doesn't actually get delivered
start_log_capture
sendmail_bv_send "$alias" 120
assert_check_logs
have_test_failures && record_captured_mail
delete_alias_group $alias
test_end
}
test_catch_all() {
# 1. ensure users in the catch-all alias receive messages to
# invalid users for handled domains
#
# 2. ensure sending mail to valid user does not go to catch-all
#
test_start "catch-all"
# create standard users alice, bob, and mary
local alice="alice@somedomain.com"
local bob="bob@anotherdomain.com"
local mary="mary@anotherdomain.com"
create_user "$alice" "alice"
local alice_dn="$ATTR_DN"
create_user "$bob" "bob"
local bob_dn="$ATTR_DN"
create_user "$mary" "mary"
# add catch-all alias to alice and bob
local alias="@somedomain.com"
create_alias_group $alias $alice_dn $bob_dn
# login as mary, then send to an invalid address. alice and bob
# should receive that mail because they're aliases to the
# catch-all for the domain
record "[Sending mail to invalid user at catch-all domain]"
start_log_capture
local output
local subject="Mail-In-A-Box test $(generate_uuid)"
output="$($PYMAIL -subj "$subject" -no-delete -to INVALID${alias} na $PRIVATE_IP $mary mary 2>&1)"
if assert_python_success $? "$output"; then
# check that alice and bob received it by deleting the mail in
# both mailboxes
record "[Delete mail in alice's and bob's mailboxes]"
output="$($PYMAIL -subj "$subject" -no-send $PRIVATE_IP $alice alice 2>&1)"
assert_python_success $? "$output"
output="$($PYMAIL -subj "$subject" -no-send $PRIVATE_IP $bob bob 2>&1)"
assert_python_success $? "$output"
fi
assert_check_logs
# login as mary and send to a valid address at the catch-all
# domain. that user should receive it and the catch-all should not
record "[Sending mail to valid user at catch-all domain]"
start_log_capture
subject="Mail-In-A-Box test $(generate_uuid)"
output="$($PYMAIL -subj "$subject" -to $alice alice $PRIVATE_IP $mary mary 2>&1)"
if assert_python_success $? "$output"; then
# alice got the mail and it was deleted
# make sure bob didn't also receive the message
record "[Delete mail in bob's mailbox]"
output="$($PYMAIL -timeout 10 -subj "$subject" -no-send $PRIVATE_IP $bob bob 2>&1)"
assert_python_failure $? "$output" "TimeoutError"
fi
assert_check_logs
delete_user "$alice"
delete_user "$bob"
delete_user "$mary"
delete_alias_group $alias
test_end
}
test_nested_alias_groups() {
# sending to an alias with embedded aliases should reach final
# recipients
test_start "nested-alias-groups"
# create standard users alice and bob
local alice="alice@zdomain.z"
create_user "$alice" "alice"
local alice_dn="$ATTR_DN"
local bob="bob@zdomain.z"
create_user "$bob" "bob"
local bob_dn="$ATTR_DN"
# add nested alias groups [ alias1 -> alias2 -> alice ]
local alias1="z1@xyzdomain.z"
local alias2="z2@xyzdomain.z"
create_alias_group $alias2 $alice_dn
create_alias_group $alias1 $ATTR_DN
# send to alias1 from bob, then ensure alice received it
record "[Sending mail to alias $alias1]"
start_log_capture
local output
local subject="Mail-In-A-Box test $(generate_uuid)"
output="$($PYMAIL -subj "$subject" -no-delete -to $alias1 na $PRIVATE_IP $bob bob 2>&1)"
if assert_python_success $? "$output"; then
record "[Test delivery - delete mail in alice's mailbox]"
output="$($PYMAIL -subj "$subject" -no-send $PRIVATE_IP $alice alice 2>&1)"
assert_python_success $? "$output"
fi
assert_check_logs
delete_user "$alice"
delete_user "$bob"
delete_alias_group "$alias1"
delete_alias_group "$alias2"
test_end
}
test_user_rename() {
# test the use case where someone's name changed
# in this test we rename the user's 'mail' address, but
# leave maildrop as-is
test_start "user-rename"
# create standard user alice
local alice1="alice.smith@somedomain.com"
local alice2="alice.jones@somedomain.com"
create_user "$alice1" "alice"
local alice_dn="$ATTR_DN"
local output
# send email to alice with subject1
record "[Testing mail to alice1]"
local subject1="Mail-In-A-Box test $(generate_uuid)"
local success1=false
start_mail_capture "$alice1"
record "[Sending mail to $alice1]"
output="$($PYMAIL -subj "$subject1" -no-delete $PRIVATE_IP $alice1 alice 2>&1)"
assert_python_success $? "$output" && success1=true
# alice1 got married, add a new mail address alice2
wait_mail # rename too soon, and the first message is bounced
record "[Changing alice's mail address]"
ldapmodify -H $LDAP_URL -x -D "$LDAP_ADMIN_DN" -w "$LDAP_ADMIN_PASSWORD" >>$TEST_OF 2>&1 <<EOF
dn: $alice_dn
replace: mail
mail: $alice2
EOF
[ $? -ne 0 ] && die "Unable to modify ${alice1}'s mail address!"
# send email to alice with subject2
start_log_capture
local subject2="Mail-In-A-Box test $(generate_uuid)"
local success2=false
record "[Sending mail to $alice2]"
output="$($PYMAIL -subj "$subject2" -no-delete $PRIVATE_IP $alice2 alice 2>&1)"
assert_python_success $? "$output" && success2=true
assert_check_logs
# delete both messages
if $success1; then
record "[Deleting mail 1]"
output="$($PYMAIL -subj "$subject1" -no-send $PRIVATE_IP $alice2 alice 2>&1)"
assert_python_success $? "$output"
fi
if $success2; then
record "[Deleting mail 2]"
output="$($PYMAIL -subj "$subject2" -no-send $PRIVATE_IP $alice2 alice 2>&1)"
assert_python_success $? "$output"
fi
delete_user "$alice2"
test_end
}
suite_start "mail-aliases"
test_shared_user_alias_login
test_alias_group_member_login
test_shared_alias_delivery # local alias delivery
test_trial_nonlocal_alias_delivery
test_catch_all
test_nested_alias_groups
test_user_rename
suite_end

View File

@@ -0,0 +1,73 @@
# -*- indent-tabs-mode: t; tab-width: 4; -*-
#
# Test basic mail functionality
test_trial_send_local() {
# use sendmail -bv to test mail delivery without actually mailing
# anything
test_start "trial_send_local"
# create a standard users alice and bobo
local alice="alice@somedomain.com" bob="bob@somedomain.com"
create_user "$alice" "alice"
create_user "$bob" "bob"
# test delivery, but don't actually mail it
start_log_capture
sendmail_bv_send "$alice" 30 "$bob"
assert_check_logs
have_test_failures && record_captured_mail
# clean up / end
delete_user "$alice"
delete_user "$bob"
test_end
}
test_trial_send_remote() {
# use sendmail -bv to test mail delivery without actually mailing
# anything
test_start "trial_send_remote"
start_log_capture
sendmail_bv_send "test@google.com" 120
assert_check_logs
have_test_failures && record_captured_mail
test_end
}
test_self_send_receive() {
# test sending mail to yourself
test_start "self-send-receive"
# create standard user alice
local alice="alice@somedomain.com"
create_user "$alice" "alice"
# test actual delivery
start_log_capture
record "[Sending mail to alice as alice]"
local output
output="$($PYMAIL $PRIVATE_IP $alice alice 2>&1)"
local code=$?
record "$output"
if [ $code -ne 0 ]; then
test_failure "$PYMAIL exit code $code: $output"
fi
assert_check_logs
delete_user "$alice"
test_end
}
suite_start "mail-basic"
test_trial_send_local
test_trial_send_remote
test_self_send_receive
suite_end

141
tests/suites/mail-from.sh Normal file
View File

@@ -0,0 +1,141 @@
# -*- indent-tabs-mode: t; tab-width: 4; -*-
test_permitted_sender_fail() {
# a user may not send MAIL FROM someone else, when not permitted
test_start "permitted-sender-fail"
# create standard users alice, bob, and mary
local alice="alice@somedomain.com"
local bob="bob@anotherdomain.com"
local mary="mary@anotherdomain.com"
create_user "$alice" "alice"
create_user "$bob" "bob"
create_user "$mary" "mary"
# login as mary, send from bob, to alice
start_log_capture
record "[Mailing to alice from bob as mary]"
local output
output="$($PYMAIL -f $bob -to $alice alice $PRIVATE_IP $mary mary 2>&1)"
if ! assert_python_failure $? "$output" SMTPRecipientsRefused
then
# additional "color"
test_failure "user should not be permitted to send as another user"
fi
# expect errors, so don't assert
check_logs
delete_user "$alice"
delete_user "$bob"
delete_user "$mary"
test_end
}
test_permitted_sender_alias() {
# a user may send MAIL FROM one of their own aliases
test_start "permitted-sender-alias"
# create standard users alice and bob
local alice="alice@somedomain.com"
local bob="bob@anotherdomain.com"
local mary="mary@anotherdomain.com"
local jane="jane@google.com"
create_user "$alice" "alice"
create_user "$bob" "bob"
local bob_dn="$ATTR_DN"
# add mary as one of bob's aliases - to bob's 'mail' attribute
add_alias $bob_dn $mary user
# add jane as one of bob's aliases - to jane's alias group
create_alias_group $jane $bob_dn
# login as bob, send from mary, to alice
start_log_capture
record "[Mailing to alice from mary as bob]"
local output
output="$($PYMAIL -f $mary -to $alice alice $PRIVATE_IP $bob bob 2>&1)"
if ! assert_python_success $? "$output"; then
# additional "color"
test_failure "bob should be permitted to MAIL FROM $mary, his own alias: $(python_error "$output")"
fi
assert_check_logs
# login as bob, send from jane, to alice
start_log_capture
record "[Mailing to alice from jane as bob]"
local output
output="$($PYMAIL -f $jane -to $alice alice $PRIVATE_IP $bob bob 2>&1)"
if ! assert_python_success $? "$output"; then
# additional "color"
test_failure "bob should be permitted to MAIL FROM $jane, his own alias: $(python_error "$output")"
fi
assert_check_logs
delete_user "$alice"
delete_user "$bob"
delete_alias_group "$jane"
test_end
}
test_permitted_sender_explicit() {
# a user may send MAIL FROM an address that is explicitly allowed
# by a permitted-senders group
# a user may not send MAIL FROM an address that has a permitted
# senders list which they are not a member, even if they are an
# alias group member
test_start "permitted-sender-explicit"
# create standard users alice and bob
local alice="alice@somedomain.com"
local bob="bob@anotherdomain.com"
create_user "$alice" "alice"
local alice_dn="$ATTR_DN"
create_user "$bob" "bob"
local bob_dn="$ATTR_DN"
# create an alias that forwards to bob and alice
local alias="mary@anotherdomain.com"
create_alias_group $alias $bob_dn $alice_dn
# create a permitted-senders group with only alice in it
create_permitted_senders_group $alias $alice_dn
# login as alice, send from alias to bob
start_log_capture
record "[Mailing to bob from alice as alias/mary]"
local output
output="$($PYMAIL -f $alias -to $bob bob $PRIVATE_IP $alice alice 2>&1)"
if ! assert_python_success $? "$output"; then
test_failure "user should be allowed to MAIL FROM a user for which they are a permitted sender: $(python_error "$output")"
fi
assert_check_logs
# login as bob, send from alias to alice
# expect failure because bob is not a permitted-sender
start_log_capture
record "[Mailing to alice from bob as alias/mary]"
output="$($PYMAIL -f $alias -to $alice alice $PRIVATE_IP $bob bob 2>&1)"
assert_python_failure $? "$output" "SMTPRecipientsRefused" "not owned by user"
check_logs
delete_user $alice
delete_user $bob
delete_permitted_senders_group $alias
create_alias_group $alias
test_end
}
suite_start "mail-from"
test_permitted_sender_fail
test_permitted_sender_alias
test_permitted_sender_explicit
suite_end

View File

@@ -0,0 +1,210 @@
# -*- indent-tabs-mode: t; tab-width: 4; -*-
#
# User management tests
_test_mixed_case() {
# helper function sends multiple email messages to test mixed case
# input scenarios
local alices=($1) # list of mixed-case email addresses for alice
local bobs=($2) # list of mixed-case email addresses for bob
local aliases=($3) # list of mixed-case email addresses for an alias
start_log_capture
local alice_pw="$(generate_password 16)"
local bob_pw="$(generate_password 16)"
# create local user alice and alias group
if mgmt_assert_create_user "${alices[0]}" "$alice_pw"; then
# test that alice cannot also exist at the same time
if mgmt_create_user "${alices[1]}" "$alice_pw" no; then
test_failure "Creation of a user with the same email address, but different case, succeeded."
test_failure "${REST_ERROR}"
fi
# create an alias group with alice in it
mgmt_assert_create_alias_group "${aliases[0]}" "${alices[1]}"
fi
# create local user bob
mgmt_assert_create_user "${bobs[0]}" "$bob_pw"
assert_check_logs
# send mail from bob to alice
#
if ! have_test_failures; then
record "[Mailing to alice from bob]"
start_log_capture
local output
output="$($PYMAIL -to ${alices[2]} "$alice_pw" $PRIVATE_IP ${bobs[1]} "$bob_pw" 2>&1)"
assert_python_success $? "$output"
assert_check_logs
# send mail from bob to the alias, ensure alice got it
#
record "[Mailing to alias from bob]"
start_log_capture
local subject="Mail-In-A-Box test $(generate_uuid)"
output="$($PYMAIL -subj "$subject" -no-delete -to ${aliases[1]} na $PRIVATE_IP ${bobs[2]} "$bob_pw" 2>&1)"
assert_python_success $? "$output"
output="$($PYMAIL -subj "$subject" -no-send $PRIVATE_IP ${alices[3]} "$alice_pw" 2>&1)"
assert_python_success $? "$output"
assert_check_logs
# send mail from alice as the alias to bob, ensure bob got it
#
record "[Mailing to bob as alias from alice]"
start_log_capture
local subject="Mail-In-A-Box test $(generate_uuid)"
output="$($PYMAIL -subj "$subject" -no-delete -f ${aliases[2]} -to ${bobs[2]} "$bob_pw" $PRIVATE_IP ${alices[4]} "$alice_pw" 2>&1)"
assert_python_success $? "$output"
output="$($PYMAIL -subj "$subject" -no-send $PRIVATE_IP ${bobs[3]} "$bob_pw" 2>&1)"
assert_python_success $? "$output"
assert_check_logs
fi
mgmt_assert_delete_user "${alices[1]}"
mgmt_assert_delete_user "${bobs[1]}"
mgmt_assert_delete_alias_group "${aliases[1]}"
}
test_mixed_case_users() {
# create mixed-case user name
# add user to alias using different cases
# send mail from another user to that user - validates smtp, imap, delivery
# send mail from another user to the alias
# send mail from that user as the alias to the other user
test_start "mixed-case-users"
local alices=(alice@mgmt.somedomain.com
aLICE@mgmt.somedomain.com
aLiCe@mgmt.somedomain.com
ALICE@mgmt.somedomain.com
alIce@mgmt.somedomain.com)
local bobs=(bob@mgmt.somedomain.com
Bob@mgmt.somedomain.com
boB@mgmt.somedomain.com
BOB@mgmt.somedomain.com)
local aliases=(aLICE@mgmt.anotherdomain.com
aLiCe@mgmt.anotherdomain.com
ALICE@mgmt.anotherdomain.com)
_test_mixed_case "${alices[*]}" "${bobs[*]}" "${aliases[*]}"
test_end
}
test_mixed_case_domains() {
# create mixed-case domain names
# add user to alias using different cases
# send mail from another user to that user - validates smtp, imap, delivery
# send mail from another user to the alias
# send mail from that user as the alias to the other user
test_start "mixed-case-domains"
local alices=(alice@mgmt.somedomain.com
alice@MGMT.somedomain.com
alice@mgmt.SOMEDOMAIN.com
alice@mgmt.somedomain.COM
alice@Mgmt.SomeDomain.Com)
local bobs=(bob@mgmt.somedomain.com
bob@MGMT.somedomain.com
bob@mgmt.SOMEDOMAIN.com
bob@Mgmt.SomeDomain.com)
local aliases=(alice@MGMT.anotherdomain.com
alice@mgmt.ANOTHERDOMAIN.com
alice@Mgmt.AnotherDomain.Com)
_test_mixed_case "${alices[*]}" "${bobs[*]}" "${aliases[*]}"
test_end
}
test_intl_domains() {
test_start "intl-domains"
# local intl alias
local alias="alice@bücher.example"
local alias_idna="alice@xn--bcher-kva.example"
# remote intl user / forward-to
local intl_person="hans@bücher.example"
local intl_person_idna="hans@xn--bcher-kva.example"
# local users
local bob="bob@somedomain.com"
local bob_pw="$(generate_password 16)"
local mary="mary@somedomain.com"
local mary_pw="$(generate_password 16)"
start_log_capture
# international domains are not permitted for user accounts
if mgmt_create_user "$intl_person" "$bob_pw"; then
test_failure "A user account is not permitted to have an international domain"
# ensure user is removed as is expected by the remaining tests
mgmt_delele_user "$intl_person"
delete_user "$intl_person"
delete_user "$intl_person_idna"
fi
# create local users bob and mary
mgmt_assert_create_user "$bob" "$bob_pw"
mgmt_assert_create_user "$mary" "$mary_pw"
# create intl alias with local user bob and intl_person in it
if mgmt_assert_create_alias_group "$alias" "$bob" "$intl_person"; then
# examine LDAP server to verify IDNA-encodings
get_attribute "$LDAP_ALIASES_BASE" "(mail=$alias_idna)" "rfc822MailMember"
if [ -z "$ATTR_DN" ]; then
test_failure "IDNA-encoded alias group not found! created as:$alias expected:$alias_idna"
elif [ "$ATTR_VALUE" != "$intl_person_idna" ]; then
test_failure "Alias group with user having an international domain was not ecoded properly. added as:$intl_person expected:$intl_person_idna"
fi
fi
# re-create intl alias with local user bob only
mgmt_assert_create_alias_group "$alias" "$bob"
assert_check_logs
if ! have_test_failures; then
# send mail to alias from mary, ensure bob got it
record "[Sending to intl alias from mary]"
# note PYMAIL does not do idna conversion - it'll throw
# "UnicodeEncodeError: 'ascii' codec can't encode character
# '\xfc' in position 38".
#
# we'll have to send to the idna address directly
start_log_capture
local subject="Mail-In-A-Box test $(generate_uuid)"
local output
output="$($PYMAIL -subj "$subject" -no-delete -to "$alias_idna" na $PRIVATE_IP $mary "$mary_pw" 2>&1)"
assert_python_success $? "$output"
output="$($PYMAIL -subj "$subject" -no-send $PRIVATE_IP $bob "$bob_pw" 2>&1)"
assert_python_success $? "$output"
assert_check_logs
fi
mgmt_assert_delete_alias_group "$alias"
mgmt_assert_delete_user "$bob"
mgmt_assert_delete_user "$mary"
test_end
}
suite_start "management-users" mgmt_start
test_mixed_case_users
test_mixed_case_domains
test_intl_domains
suite_end mgmt_end

View File

@@ -1,109 +1,204 @@
#!/usr/bin/env python3
# -*- indent-tabs-mode: t; tab-width: 4; -*-
#
# Tests sending and receiving mail by sending a test message to yourself.
import sys, imaplib, smtplib, uuid, time
import socket, dns.reversename, dns.resolver
if len(sys.argv) < 3:
print("Usage: tests/mail.py hostname emailaddress password")
def usage():
print("Usage: test_mail.py [options] hostname login password")
print("Send, then delete message")
print(" options")
print(" -f <email>: use <email> as the MAIL FROM address")
print(" -to <email> <pass>: recipient of email and password")
print(" -subj <subject>: subject of the message (required with --no-send)")
print(" -no-send: don't send, just delete")
print(" -no-delete: don't delete, just send")
print(" -timeout <seconds>: how long to wait for message")
print("");
sys.exit(1)
host, emailaddress, pw = sys.argv[1:4]
def if_unset(a,b):
return b if a is None else a
# Attempt to login with IMAP. Our setup uses email addresses
# as IMAP/SMTP usernames.
try:
M = imaplib.IMAP4_SSL(host)
M.login(emailaddress, pw)
except OSError as e:
print("Connection error:", e)
sys.exit(1)
except imaplib.IMAP4.error as e:
# any sort of login error
e = ", ".join(a.decode("utf8") for a in e.args)
print("IMAP error:", e)
sys.exit(1)
# option defaults
host=None # smtp server address
login=None # smtp server login
pw=None # smtp server password
emailfrom=None # MAIL FROM address
emailto=None # RCPT TO address
emailto_pw=None # recipient password for imap login
send_msg=True # deliver message
delete_msg=True # login to imap and delete message
wait_timeout=30 # abandon timeout wiating for message delivery
wait_cycle_sleep=5 # delay between delivery checks
subject="Mail-in-a-Box Automated Test Message " + uuid.uuid4().hex # message subject
M.select()
print("IMAP login is OK.")
# process command line
argi=1
while argi<len(sys.argv):
arg=sys.argv[argi]
arg_remaining = len(sys.argv) - argi - 1
if not arg.startswith('-'):
break
if (arg=="-f" or arg=="-from") and arg_remaining>0:
emailfrom=sys.argv[argi+1]
argi+=2
elif arg=="-to" and arg_remaining>1:
emailto=sys.argv[argi+1]
emailto_pw=sys.argv[argi+2]
argi+=3
elif arg=="-subj" and arg_remaining>1:
subject=sys.argv[argi+1]
argi+=2
elif arg=="-no-send":
send_msg=False
argi+=1
elif arg=="-no-delete":
delete_msg=False
argi+=1
elif arg=="-timeout" and arg_remaining>1:
wait_timeout=int(sys.argv[argi+1])
argi+=2
else:
usage()
if len(sys.argv) - argi != 3: usage()
host, login, pw = sys.argv[argi:argi+3]
argi+=3
# Attempt to send a mail to ourself.
mailsubject = "Mail-in-a-Box Automated Test Message " + uuid.uuid4().hex
emailto = emailaddress
msg = """From: {emailaddress}
emailfrom = if_unset(emailfrom, login)
emailto = if_unset(emailto, login)
emailto_pw = if_unset(emailto_pw, pw)
msg = """From: {emailfrom}
To: {emailto}
Subject: {subject}
This is a test message. It should be automatically deleted by the test script.""".format(
emailaddress=emailaddress,
emailfrom=emailfrom,
emailto=emailto,
subject=mailsubject,
subject=subject,
)
# Connect to the server on the SMTP submission TLS port.
server = smtplib.SMTP(host, 587)
#server.set_debuglevel(1)
server.starttls()
# Verify that the EHLO name matches the server's reverse DNS.
ipaddr = socket.gethostbyname(host) # IPv4 only!
reverse_ip = dns.reversename.from_address(ipaddr) # e.g. "1.0.0.127.in-addr.arpa."
try:
reverse_dns = dns.resolver.query(reverse_ip, 'PTR')[0].target.to_text(omit_final_dot=True) # => hostname
except dns.resolver.NXDOMAIN:
print("Reverse DNS lookup failed for %s. SMTP EHLO name check skipped." % ipaddr)
reverse_dns = None
if reverse_dns is not None:
server.ehlo_or_helo_if_needed() # must send EHLO before getting the server's EHLO name
helo_name = server.ehlo_resp.decode("utf8").split("\n")[0] # first line is the EHLO name
if helo_name != reverse_dns:
print("The server's EHLO name does not match its reverse hostname. Check DNS settings.")
else:
print("SMTP EHLO name (%s) is OK." % helo_name)
# Login and send a test email.
server.login(emailaddress, pw)
server.sendmail(emailaddress, [emailto], msg)
server.quit()
print("SMTP submission is OK.")
def imap_login(host, login, pw):
# Attempt to login with IMAP. Our setup uses email addresses
# as IMAP/SMTP usernames.
try:
M = imaplib.IMAP4_SSL(host)
M.login(login, pw)
except OSError as e:
print("Connection error:", e)
sys.exit(1)
except imaplib.IMAP4.error as e:
# any sort of login error
e = ", ".join(a.decode("utf8") for a in e.args)
print("IMAP error:", e)
sys.exit(1)
while True:
# Wait so the message can propagate to the inbox.
time.sleep(10)
M.select()
print("IMAP login is OK.")
return M
def imap_search_for(M, subject):
# Read the subject lines of all of the emails in the inbox
# to find our test message, and then delete it.
found = False
# to find our test message, then return the number
typ, data = M.search(None, 'ALL')
for num in data[0].split():
typ, data = M.fetch(num, '(BODY[HEADER.FIELDS (SUBJECT)])')
imapsubjectline = data[0][1].strip().decode("utf8")
if imapsubjectline == "Subject: " + mailsubject:
# We found our test message.
found = True
if imapsubjectline == "Subject: " + subject:
return num
return None
# To test DKIM, download the whole mssage body. Unfortunately,
# pydkim doesn't actually work.
# You must 'sudo apt-get install python3-dkim python3-dnspython' first.
#typ, msgdata = M.fetch(num, '(RFC822)')
#msg = msgdata[0][1]
#if dkim.verify(msg):
# print("DKIM signature on the test message is OK (verified).")
#else:
# print("DKIM signature on the test message failed verification.")
def imap_test_dkim(M, num):
# To test DKIM, download the whole mssage body. Unfortunately,
# pydkim doesn't actually work.
# You must 'sudo apt-get install python3-dkim python3-dnspython' first.
#typ, msgdata = M.fetch(num, '(RFC822)')
#msg = msgdata[0][1]
#if dkim.verify(msg):
# print("DKIM signature on the test message is OK (verified).")
#else:
# print("DKIM signature on the test message failed verification.")
pass
def smtp_login(host, login, pw):
# Connect to the server on the SMTP submission TLS port.
server = smtplib.SMTP(host, 587)
#server.set_debuglevel(1)
server.starttls()
# Verify that the EHLO name matches the server's reverse DNS.
ipaddr = socket.gethostbyname(host) # IPv4 only!
reverse_ip = dns.reversename.from_address(ipaddr) # e.g. "1.0.0.127.in-addr.arpa."
try:
reverse_dns = dns.resolver.query(reverse_ip, 'PTR')[0].target.to_text(omit_final_dot=True) # => hostname
except dns.resolver.NXDOMAIN:
print("Reverse DNS lookup failed for %s. SMTP EHLO name check skipped." % ipaddr)
reverse_dns = None
if reverse_dns is not None:
server.ehlo_or_helo_if_needed() # must send EHLO before getting the server's EHLO name
helo_name = server.ehlo_resp.decode("utf8").split("\n")[0] # first line is the EHLO name
if helo_name != reverse_dns:
print("The server's EHLO name does not match its reverse hostname. Check DNS settings.")
else:
print("SMTP EHLO name (%s) is OK." % helo_name)
# Login and send a test email.
if login is not None and login != "":
server.login(login, pw)
return server
if send_msg:
# Attempt to send a mail.
server = smtp_login(host, login, pw)
server.sendmail(emailfrom, [emailto], msg)
server.quit()
print("SMTP submission is OK.")
if delete_msg:
# Wait for mail and delete it.
M = imap_login(host, emailto, emailto_pw)
start_time = time.time()
found = False
if send_msg:
# Wait so the message can propagate to the inbox.
time.sleep(wait_cycle_sleep / 2)
while time.time() - start_time < wait_timeout:
num = imap_search_for(M, subject)
if num is not None:
# Delete the test message.
found = True
imap_test_dkim(M, num)
M.store(num, '+FLAGS', '\\Deleted')
M.expunge()
print("Message %s deleted successfully." % num)
break
print("Test message not present in the inbox yet...")
time.sleep(wait_cycle_sleep)
M.close()
M.logout()
if not found:
raise TimeoutError("Timeout waiting for message")
if found:
break
if send_msg and delete_msg:
print("Test message sent & received successfully.")
print("Test message not present in the inbox yet...")
M.close()
M.logout()
print("Test message sent & received successfully.")