From 5852a7aabb761b8e78a0d20f9ed48681cf6e6358 Mon Sep 17 00:00:00 2001 From: downtownallday Date: Thu, 10 Sep 2020 15:24:47 -0400 Subject: [PATCH] Add QA tests for TOTP --- tests/lib/rest.sh | 10 +- tests/suites/_ldap-functions.sh | 21 ++- tests/suites/_mgmt-functions.sh | 206 ++++++++++++++++++++++++++++++ tests/suites/ldap-access.sh | 50 +++++--- tests/suites/management-users.sh | 80 ++++++++++++ tests/system-setup/setup-funcs.sh | 3 +- 6 files changed, 350 insertions(+), 20 deletions(-) diff --git a/tests/lib/rest.sh b/tests/lib/rest.sh index 2c8fd39d..1cf6d22e 100644 --- a/tests/lib/rest.sh +++ b/tests/lib/rest.sh @@ -59,7 +59,15 @@ rest_urlencoded() { if $onlydata; then data+=("--data-urlencode" "$item"); else - data+=("$item") + # if argument is like "--header=", then change to + # "--header " because curl wants the latter + local arg="$(awk -F= '{print $1}' <<<"$item")" + local val="$(awk -F= '{print substr($0,length($1)+2)}' <<<"$item")" + if [ -z "$val" ]; then + data+=("$item") + else + data+=("$arg" "$val") + fi fi ;; * ) diff --git a/tests/suites/_ldap-functions.sh b/tests/suites/_ldap-functions.sh index 6c69200d..bb19a6f9 100644 --- a/tests/suites/_ldap-functions.sh +++ b/tests/suites/_ldap-functions.sh @@ -29,6 +29,7 @@ create_user() { local email="$1" local pass="${2:-$email}" local priv="${3:-test}" + local totpVal="${4:-}" # "secret,token" local localpart="$(awk -F@ '{print $1}' <<< "$email")" local domainpart="$(awk -F@ '{print $2}' <<< "$email")" #local uid="$localpart" @@ -39,19 +40,33 @@ create_user() { record "[create user $email ($dn)]" delete_dn "$dn" - + + # totpSecret: base-32 digits (see RFC 4648), qty 32 + # totpMruToken: base-10 digits, qty 6 + # note: comma is not a base32 symbol + local totpObjectClass="" + local totpSecret="$(awk -F, '{print $1}' <<< "$totpVal")" + local totpMruToken="$(awk -F, '{print $2}' <<< "$totpVal")" + if [ ! -z "$totpVal" ]; then + local nl=$'\n' + totpObjectClass="${nl}objectClass: totpUser" + totpSecret="${nl}totpSecret: ${totpSecret}" + [ ! -z "$totpMruToken" ] && \ + totpMruToken="${nl}totpMruToken: ${totpMruToken}" + fi + ldapadd -H "$LDAP_URL" -x -D "$LDAP_ADMIN_DN" -w "$LDAP_ADMIN_PASSWORD" >>$TEST_OF 2>&1 <>$TEST_OF 2>&1 + return $? +} + + mgmt_create_user() { local email="$1" @@ -145,3 +159,195 @@ mgmt_assert_delete_alias_group() { fi return 0 } + + +mgmt_privileges_add() { + local user="$1" + local priv="$2" # only one privilege allowed + record "[add privilege '$priv' to $user]" + mgmt_rest POST "/admin/mail/users/privileges/add" "email=$user" "privilege=$priv" + rc=$? + return $rc +} + +mgmt_assert_privileges_add() { + if ! mgmt_privileges_add "$@"; then + test_failure "Unable to add privilege '$2' to $1" + test_failure "${REST_ERROR}" + return 1 + fi + return 0 +} + +mgmt_get_totp_token() { + local secret="$1" + local mru_token="$2" + + TOTP_TOKEN="" # this is set to the acquired token on success + + # the user would normally give the secret to an authenticator app + # and get a token -- we'll do that out-of-band. we have to run + # the admin's python because setup does not do a 'pip install + # pyotp', so the system python3 probably won't have it + + record "[Get the current token for the secret '$secret']" + + local count=0 + + while [ -z "$TOTP_TOKEN" -a $count -lt 10 ]; do + TOTP_TOKEN="$(/usr/local/lib/mailinabox/env/bin/python -c "import pyotp; totp=pyotp.TOTP(r'$secret'); print(totp.now());" 2>>"$TEST_OF")" + if [ $? -ne 0 ]; then + record "Failed: Could not generate a TOTP token !" + return 1 + fi + + if [ "$TOTP_TOKEN" == "$mru_token" ]; then + TOTP_TOKEN="" + record "Waiting for unique token!" + sleep 5 + else + record "Success: token is '$TOTP_TOKEN'" + return 0 + fi + + let count+=1 + done + + record "Failed: timeout !" + TOTP_TOKEN="" + return 1 +} + + +mgmt_totp_enable() { + # enable TOTP for user specified + # returns 0 if successful and TOTP_SECRET will contain the secret and TOTP_TOKEN will contain the token used + # returns 1 if a REST error occured. $REST_ERROR has the message + # returns 2 if some other error occured + # + + local user="$1" + local pw="$2" + TOTP_SECRET="" + + record "[Enable TOTP for $user]" + + # 1. get a totp secret + if ! mgmt_rest_as_user "GET" "/admin/mfa/status" "$user" "$pw"; then + REST_ERROR="Failed: GET/admin/mfa/status: $REST_ERROR" + return 1 + fi + + TOTP_SECRET="$(/usr/bin/jq -r ".totp_secret" <<<"$REST_OUTPUT")" + if [ $? -ne 0 ]; then + record "Unable to obtain setup totp secret - is 'jq' installed?" + return 2 + fi + + if [ "$TOTP_SECRET" == "null" ]; then + record "No 'totp_secret' in the returned json !" + return 2 + else + record "Found TOTP secret '$TOTP_SECRET'" + fi + + if ! mgmt_get_totp_token "$TOTP_SECRET"; then + return 2 + fi + + # 3. enable TOTP + record "Enabling TOTP using the secret and token" + if ! mgmt_rest_as_user "POST" "/admin/mfa/totp/enable" "$user" "$pw" "secret=$TOTP_SECRET" "token=$TOTP_TOKEN"; then + REST_ERROR="Failed: POST /admin/mfa/totp/enable: ${REST_ERROR}" + return 1 + else + record "Success: POST /mfa/totp/enable: '$REST_OUTPUT'" + fi + + return 0 +} + + +mgmt_assert_totp_enable() { + local user="$1" + mgmt_totp_enable "$@" + local code=$? + if [ $code -ne 0 ]; then + test_failure "Unable to enable TOTP for $user" + if [ $code -eq 1 ]; then + test_failure "${REST_ERROR}" + fi + return 1 + fi + get_attribute "$LDAP_USERS_BASE" "(&(mail=$user)(objectClass=totpUser))" "dn" + if [ -z "$ATTR_DN" ]; then + test_failure "totpUser objectClass not present on $user" + fi + record_search "(mail=$user)" + return 0 +} + + +mgmt_totp_disable() { + local user="$1" + local pw="$2" + record "[Disable TOTP for $user]" + if ! mgmt_rest_as_user "POST" "/admin/mfa/totp/disable" "$user" "$pw" + then + REST_ERROR="Failed: POST /admin/mfa/totp/disable: $REST_ERROR" + return 1 + else + record "Success" + return 0 + fi +} + +mgmt_assert_totp_disable() { + local user="$1" + mgmt_totp_disable "$@" + local code=$? + if [ $code -ne 0 ]; then + test_failure "Unable to disable TOTP for $user: $REST_ERROR" + return 1 + fi + get_attribute "$LDAP_USERS_BASE" "(&(mail=$user)(objectClass=totpUser))" "dn" + if [ ! -z "$ATTR_DN" ]; then + test_failure "totpUser objectClass still present on $user" + fi + record_search "(mail=$user)" + return 0 +} + +mgmt_assert_admin_me() { + local user="$1" + local pw="$2" + local expected_status="${3:-ok}" + shift; shift; shift; # remaining arguments are data + + # note: GET /admin/me always returns http status 200, but errors are in + # the json payload + record "[Get /admin/me as $user]" + if ! mgmt_rest_as_user "GET" "/admin/me" "$user" "$pw" "$@"; then + test_failure "GET /admin/me as $user failed: $REST_ERROR" + return 1 + + else + local status code + status="$(/usr/bin/jq -r '.status' <<<"$REST_OUTPUT")" + code=$? + if [ $code -ne 0 ]; then + test_failure "Unable to run jq ($code) on /admin/me json" + return 1 + + elif [ "$status" == "null" ]; then + test_failure "No 'status' in /admin/me json" + return 1 + + elif [ "$status" != "$expected_status" ]; then + test_failure "Expected a login status of '$expected_status', but got '$status'" + return 1 + + fi + fi + return 0 +} diff --git a/tests/suites/ldap-access.sh b/tests/suites/ldap-access.sh index d35d2834..6f0be291 100644 --- a/tests/suites/ldap-access.sh +++ b/tests/suites/ldap-access.sh @@ -3,14 +3,16 @@ # 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 all attributes of all users but not userPassword, totpSecret, totpMruToken +# can not write any user attributes, including 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 read or write access to user's own totpSecret or totpMruToken +# can read attributess of all users except: +# mailaccess, totpSecret, totpMruToken # no access to config subtree # no access to services subtree # other: @@ -36,19 +38,24 @@ test_user_change_password() { test_user_access() { - # 1. can read attributess of all users except mailaccess + # 1. can read attributess of all users except mailaccess, totpSecret, totpMruToken # 2. can read and change their own shadowLastChange # 3. no access to config subtree # 4. no access to services subtree + # 5. no read or write access to own totpSecret or totpMruToken + test_start "user-access" + local totpSecret="12345678901234567890" + local totpMruToken="94287082" + # create regular user's alice and bob local alice="alice@somedomain.com" - create_user "alice@somedomain.com" "alice" + create_user "alice@somedomain.com" "alice" "" "$totpSecret,$totpMruToken" local alice_dn="$ATTR_DN" local bob="bob@somedomain.com" - create_user "bob@somedomain.com" "bob" + create_user "bob@somedomain.com" "bob" "" "$totpSecret,$totpMruToken" local bob_dn="$ATTR_DN" # alice should be able to set her own shadowLastChange @@ -56,19 +63,29 @@ test_user_access() { # 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 + + # alice should not have access to her own mailaccess, totpSecret or totpMruToken, though + assert_r_access "$alice_dn" "$alice_dn" "alice" no-read mailaccess totpSecret totpMruToken + # test that alice cannot change her own select attributes assert_w_access "$alice_dn" "$alice_dn" "alice" + # test that alice cannot change her own totpSecret or totpMruToken + assert_w_access "$alice_dn" "$alice_dn" "alice" no-write "totpSecret=ABC" "totpMruToken=123456" + # 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 + + # alice should not have access to bob's mailaccess, totpSecret, or totpMruToken + assert_r_access "$bob_dn" "$alice_dn" "alice" no-read mailaccess totpSecret totpMruToken + + # test that alice cannot change bob's select attributes assert_w_access "$bob_dn" "$alice_dn" "alice" + # test that alice cannot change bob's attributes + assert_w_access "$bob_dn" "$alice_dn" "alice" no-write "totpSecret=ABC" "totpMruToken=123456" + # test that alice cannot read a service account's attributes assert_r_access "$LDAP_POSTFIX_DN" "$alice_dn" "alice" @@ -132,9 +149,12 @@ test_service_access() { test_start "service-access" + local totpSecret="12345678901234567890" + local totpMruToken="94287082" + # create regular user with password "alice" local alice="alice@somedomain.com" - create_user "alice@somedomain.com" "alice" + create_user "alice@somedomain.com" "alice" "" "$totpSecret,$totpMruToken" # create a test service account create_service_account "test" "test" @@ -154,12 +174,12 @@ test_service_access() { # 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 account should not be able to read user's userPassword, totpSecret or totpMruToken + assert_r_access "$alice_dn" "$LDAP_POSTFIX_DN" "$LDAP_POSTFIX_PASSWORD" no-read userPassword totpSecret totpMruToken # 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" + assert_w_access "$alice_dn" "$LDAP_POSTFIX_DN" "$LDAP_POSTFIX_PASSWORD" no-write "shadowLastChange=1" "totpSecret=ABC" "totpMruToken=333333" fi # service accounts can read config subtree (permitted-senders, domains) diff --git a/tests/suites/management-users.sh b/tests/suites/management-users.sh index 1cc0f0fc..32b33aad 100644 --- a/tests/suites/management-users.sh +++ b/tests/suites/management-users.sh @@ -200,8 +200,88 @@ test_intl_domains() { } + +test_totp() { + test_start "totp" + + # local intl alias + local alice="alice@somedomain.com" + local alice_pw="$(generate_password 16)" + + start_log_capture + + # create alice + mgmt_assert_create_user "$alice" "$alice_pw" + + # alice must be admin to use TOTP + if ! have_test_failures; then + mgmt_assert_privileges_add "$alice" "admin" + fi + + # add totp to alice's account (if successful, secret is in TOTP_SECRET) + if ! have_test_failures; then + mgmt_assert_totp_enable "$alice" "$alice_pw" + # TOTP_SECRET and TOTP_TOKEN are now set... + fi + + # logging in with just the password should now fail + if ! have_test_failures; then + record "Expect a login failure..." + mgmt_assert_admin_me "$alice" "$alice_pw" "missing_token" + fi + + + # logging into /admin/me with a password and a token should + # succeed, and an api_key generated + local api_key + if ! have_test_failures; then + record "Try using a password and a token to get the user api key, we may have to wait 30 seconds to get a new token..." + + local old_totp_token="$TOTP_TOKEN" + if ! mgmt_get_totp_token "$TOTP_SECRET" "$TOTP_TOKEN"; then + test_failure "Could not obtain a new TOTP token" + + else + # we have a new token, try logging in ... + # the token must be placed in the header "x-auth-token" + if mgmt_assert_admin_me "$alice" "$alice_pw" "ok" "--header=x-auth-token: $TOTP_TOKEN" + then + api_key="$(/usr/bin/jq -r '.api_key' <<<"$REST_OUTPUT")" + record "Success: login with TOTP token successful. api_key=$api_key" + + # ensure the totpMruToken was changed in LDAP + get_attribute "$LDAP_USERS_BASE" "(mail=$alice)" "totpMruToken" + if [ "$ATTR_VALUE" != "$TOTP_TOKEN" ]; then + record_search "(mail=$alice)" + test_failure "totpMruToken wasn't updated in LDAP" + fi + fi + fi + fi + + # we should be able to login using the user's api key + if ! have_test_failures; then + record "Login using the user's api key" + mgmt_assert_admin_me "$alice" "$api_key" "ok" + fi + + # disable totp on the account - login should work with just the password + # and the ldap entry should not have the 'totpUser' objectClass + if ! have_test_failures; then + if mgmt_assert_totp_disable "$alice" "$api_key"; then + mgmt_assert_admin_me "$alice" "$alice_pw" "ok" + fi + fi + + mgmt_assert_delete_user "$alice" + test_end +} + + + suite_start "management-users" mgmt_start +test_totp test_mixed_case_domains test_mixed_case_users test_intl_domains diff --git a/tests/system-setup/setup-funcs.sh b/tests/system-setup/setup-funcs.sh index ca13abfb..565dc52c 100755 --- a/tests/system-setup/setup-funcs.sh +++ b/tests/system-setup/setup-funcs.sh @@ -118,8 +118,9 @@ init_miab_testing() { # python3-dnspython: is used by the python scripts in 'tests' and is # not installed by setup + # also install 'jq' for json processing wait_for_apt - apt-get install -y -qq python3-dnspython + apt-get install -y -qq python3-dnspython jq # copy in pre-built MiaB-LDAP ssl files # 1. avoid the lengthy generation of DH params