1
0
mirror of https://github.com/mail-in-a-box/mailinabox.git synced 2025-04-04 00:17:06 +00:00
# Conflicts:
#	management/auth.py
#	management/daemon.py
#	management/templates/index.html
#	setup/management.sh
This commit is contained in:
downtownallday 2021-09-14 08:16:08 -04:00
commit 402207714b
12 changed files with 247 additions and 162 deletions

View File

@ -54,24 +54,24 @@ tags:
System operations, which include system status checks, new version checks
and reboot status.
paths:
/me:
get:
/login:
post:
tags:
- User
summary: Get user information
summary: Exchange a username and password for a session API key.
description: |
Returns user information. Used for user authentication.
Returns user information and a session API key.
Authenticate a user by supplying the auth token as a base64 encoded string in
format `email:password` using basic authentication headers.
If successful, a long-lived `api_key` is returned which can be used for subsequent
requests to the API.
operationId: getMe
requests to the API in place of the password.
operationId: login
x-codeSamples:
- lang: curl
source: |
curl -X GET "https://{host}/admin/me" \
curl -X GET "https://{host}/admin/login" \
-u "<email>:<password>"
responses:
200:
@ -92,6 +92,24 @@ paths:
privileges:
- admin
status: ok
/logout:
post:
tags:
- User
summary: Invalidates a session API key.
description: |
Invalidates a session API key so that it cannot be used after this API call.
operationId: logout
x-codeSamples:
- lang: curl
source: |
curl -X GET "https://{host}/admin/logout" \
-u "<email>:<session_key>"
responses:
200:
description: Successful operation
content:
application/json:
/system/status:
post:
tags:
@ -1803,7 +1821,7 @@ components:
The `access-token` is comprised of the Base64 encoding of `username:password`.
The `username` is the mail user's email address, and `password` can either be the mail user's
password, or the `api_key` returned from the `getMe` operation.
password, or the `api_key` returned from the `login` operation.
When using `curl`, you can supply user credentials using the `-u` or `--user` parameter.
requestBodies:

View File

@ -1,6 +1,8 @@
import base64, os, os.path, hmac, json
# -*- indent-tabs-mode: t; tab-width: 4; python-indent-offset: 4; -*-
import base64, os, os.path, hmac, json, secrets
from datetime import timedelta
from flask import make_response
from expiringdict import ExpiringDict
import utils
from mailconfig import validate_login, get_mail_password, get_mail_user_privileges
@ -9,25 +11,18 @@ from mfa import get_hash_mfa_state, validate_auth_mfa
DEFAULT_KEY_PATH = '/var/lib/mailinabox/api.key'
DEFAULT_AUTH_REALM = 'Mail-in-a-Box Management Server'
class KeyAuthService:
"""Generate an API key for authenticating clients
Clients must read the key from the key file and send the key with all HTTP
requests. The key is passed as the username field in the standard HTTP
Basic Auth header.
"""
class AuthService:
def __init__(self):
self.auth_realm = DEFAULT_AUTH_REALM
self.key = self._generate_key()
self.key_path = DEFAULT_KEY_PATH
self.max_session_duration = timedelta(days=2)
def write_key(self):
"""Write key to file so authorized clients can get the key
self.init_system_api_key()
self.sessions = ExpiringDict(max_len=64, max_age_seconds=self.max_session_duration.total_seconds())
def init_system_api_key(self):
"""Write an API key to a local file so local processes can use the API"""
The key file is created with mode 0640 so that additional users can be
authorized to access the API by granting group/ACL read permissions on
the key file.
"""
def create_file_with_mode(path, mode):
# Based on answer by A-B-B: http://stackoverflow.com/a/15015748
old_umask = os.umask(0)
@ -36,111 +31,118 @@ class KeyAuthService:
finally:
os.umask(old_umask)
self.key = secrets.token_hex(32)
os.makedirs(os.path.dirname(self.key_path), exist_ok=True)
with create_file_with_mode(self.key_path, 0o640) as key_file:
key_file.write(self.key + '\n')
def authenticate(self, request, env):
"""Test if the client key passed in HTTP Authorization header matches the service key
or if the or username/password passed in the header matches an administrator user.
def authenticate(self, request, env, login_only=False, logout=False):
"""Test if the HTTP Authorization header's username matches the system key, a session key,
or if the username/password passed in the header matches a local user.
Returns a tuple of the user's email address and list of user privileges (e.g.
('my@email', []) or ('my@email', ['admin']); raises a ValueError on login failure.
If the user used an API key, the user's email is returned as None."""
If the user used the system API key, the user's email is returned as None since
this key is not associated with a user."""
def decode(s):
return base64.b64decode(s.encode('ascii')).decode('ascii')
def parse_basic_auth(header):
def parse_http_authorization_basic(header):
def decode(s):
return base64.b64decode(s.encode('ascii')).decode('ascii')
if " " not in header:
return None, None
scheme, credentials = header.split(maxsplit=1)
if scheme != 'Basic':
return None, None
credentials = decode(credentials)
if ":" not in credentials:
return None, None
username, password = credentials.split(':', maxsplit=1)
return username, password
header = request.headers.get('Authorization')
if not header:
raise ValueError("No authorization header provided.")
username, password = parse_basic_auth(header)
username, password = parse_http_authorization_basic(request.headers.get('Authorization', ''))
if username in (None, ""):
raise ValueError("Authorization header invalid.")
elif username == self.key:
# The user passed the master API key which grants administrative privs.
if username.strip() == "" and password.strip() == "":
raise ValueError("No email address, password, session key, or API key provided.")
# If user passed the system API key, grant administrative privs. This key
# is not associated with a user.
if username == self.key and not login_only:
return (None, ["admin"])
# If the password corresponds with a session token for the user, grant access for that user.
if password in self.sessions and self.sessions[password]["email"] == username and not login_only:
sessionid = password
session = self.sessions[sessionid]
if session["password_token"] != self.create_user_password_state_token(username, env):
# This session is invalid because the user's password/MFA state changed
# after the session was created.
del self.sessions[sessionid]
raise ValueError("Session expired.")
if logout:
# Clear the session.
del self.sessions[sessionid]
else:
# Re-up the session so that it does not expire.
self.sessions[sessionid] = session
# If no password was given, but a username was given, we're missing some information.
elif password.strip() == "":
raise ValueError("Enter a password.")
else:
# The user is trying to log in with a username and either a password
# (and possibly a MFA token) or a user-specific API key.
return (username, self.check_user_auth(username, password, request, env))
# The user is trying to log in with a username and a password
# (and possibly a MFA token). On failure, an exception is raised.
self.check_user_auth(username, password, request, env)
# Get privileges for authorization. This call should never fail because by this
# point we know the email address is a valid user --- unless the user has been
# deleted after the session was granted. On error the call will return a tuple
# of an error message and an HTTP status code.
privs = get_mail_user_privileges(username, env)
if isinstance(privs, tuple): raise ValueError(privs[0])
# Return the authorization information.
return (username, privs)
def check_user_auth(self, email, pw, request, env):
# Validate a user's login email address and password. If MFA is enabled,
# check the MFA token in the X-Auth-Token header.
#
# On success returns a list of privileges (e.g. [] or ['admin']). On login
# failure, raises a ValueError with a login error message.
# On login failure, raises a ValueError with a login error message. On
# success, nothing is returned.
# Sanity check.
if email == "" or pw == "":
raise ValueError("Enter an email address and password.")
# Authenticate.
if not validate_login(email, pw, env):
# Login failed.
raise ValueError("Incorrect email address or password.")
# The password might be a user-specific API key. create_user_key raises
# a ValueError if the user does not exist.
if hmac.compare_digest(self.create_user_key(email, env), pw):
# OK.
pass
else:
# Get the hashed password of the user. Raise a ValueError if the
# email address does not correspond to a user.
if not validate_login(email, pw, env):
# Login failed.
raise ValueError("Invalid password.")
# If MFA is enabled, check that MFA passes.
status, hints = validate_auth_mfa(email, request, env)
if not status:
# Login valid. Hints may have more info.
raise ValueError(",".join(hints))
# If MFA is enabled, check that MFA passes.
status, hints = validate_auth_mfa(email, request, env)
if not status:
# Login valid. Hints may have more info.
raise ValueError(",".join(hints))
# Get privileges for authorization. This call should never fail because by this
# point we know the email address is a valid user. But on error the call will
# return a tuple of an error message and an HTTP status code.
privs = get_mail_user_privileges(email, env)
if isinstance(privs, tuple): raise ValueError(privs[0])
# Return a list of privileges.
return privs
def create_user_key(self, email, env):
# Create a user API key, which is a shared secret that we can re-generate from
# static information in our database. The shared secret contains the user's
# email address, current hashed password, and current MFA state, so that the
# key becomes invalid if any of that information changes.
#
# Use an HMAC to generate the API key using our master API key as a key,
# which also means that the API key becomes invalid when our master API key
# changes --- i.e. when this process is restarted.
#
# Raises ValueError via get_mail_password if the user doesn't exist.
# Construct the HMAC message from the user's email address and current password.
msg = b"AUTH:" + email.encode("utf8") + b" " + ";".join(get_mail_password(email, env)).encode("utf8")
def create_user_password_state_token(self, email, env):
# Create a token that changes if the user's password or MFA options change
# so that sessions become invalid if any of that information changes.
msg = ';'.join(get_mail_password(email, env)).encode("utf8")
# Add to the message the current MFA state, which is a list of MFA information.
# Turn it into a string stably.
msg += b" " + json.dumps(get_hash_mfa_state(email, env), sort_keys=True).encode("utf8")
# Make the HMAC.
# Make a HMAC using the system API key as a hash key.
hash_key = self.key.encode('ascii')
return hmac.new(hash_key, msg, digestmod="sha256").hexdigest()
def _generate_key(self):
raw_key = os.urandom(32)
return base64.b64encode(raw_key).decode('ascii')
def create_session_key(self, username, env, type=None):
# Create a new session.
token = secrets.token_hex(32)
self.sessions[token] = {
"email": username,
"password_token": self.create_user_password_state_token(username, env),
}
return token

View File

@ -1,5 +1,8 @@
#!/usr/local/lib/mailinabox/env/bin/python3
#
# The API can be accessed on the command line, e.g. use `curl` like so:
# curl --user $(</var/lib/mailinabox/api.key): http://localhost:10222/mail/users
#
# During development, you can start the Mail-in-a-Box control panel
# by running this script, e.g.:
#
@ -23,7 +26,7 @@ import mfa_totp
env = utils.load_environment()
auth_service = auth.KeyAuthService()
auth_service = auth.AuthService()
# We may deploy via a symbolic link, which confuses flask's template finding.
me = __file__
@ -54,8 +57,10 @@ def authorized_personnel_only(viewfunc):
try:
email, privs = auth_service.authenticate(request, env)
except ValueError as e:
# Write a line in the log recording the failed login
log_failed_login(request)
# Write a line in the log recording the failed login, unless no authorization header
# was given which can happen on an initial request before a 403 response.
if "Authorization" in request.headers:
log_failed_login(request)
# Authentication failed.
error = str(e)
@ -132,11 +137,12 @@ def index():
csr_country_codes=csr_country_codes,
)
@app.route('/me')
def me():
# Create a session key by checking the username/password in the Authorization header.
@app.route('/login', methods=["POST"])
def login():
# Is the caller authorized?
try:
email, privs = auth_service.authenticate(request, env)
email, privs = auth_service.authenticate(request, env, login_only=True)
except ValueError as e:
if "missing-totp-token" in str(e):
return json_response({
@ -151,19 +157,29 @@ def me():
"reason": str(e),
})
# Return a new session for the user.
resp = {
"status": "ok",
"email": email,
"privileges": privs,
"api_key": auth_service.create_session_key(email, env, type='login'),
}
# Is authorized as admin? Return an API key for future use.
if "admin" in privs:
resp["api_key"] = auth_service.create_user_key(email, env)
app.logger.info("New login session created for {}".format(email))
# Return.
return json_response(resp)
@app.route('/logout', methods=["POST"])
def logout():
try:
email, _ = auth_service.authenticate(request, env, logout=True)
app.logger.info("{} logged out".format(email))
except ValueError as e:
pass
finally:
return json_response({ "status": "ok" })
# MAIL
@app.route('/mail/users')
@ -771,27 +787,9 @@ if __name__ == '__main__':
# Turn on Flask debugging.
app.debug = True
# Use a stable-ish master API key so that login sessions don't restart on each run.
# Use /etc/machine-id to seed the key with a stable secret, but add something
# and hash it to prevent possibly exposing the machine id, using the time so that
# the key is not valid indefinitely.
import hashlib
with open("/etc/machine-id") as f:
api_key = f.read()
api_key += "|" + str(int(time.time() / (60*60*2)))
hasher = hashlib.sha1()
hasher.update(api_key.encode("ascii"))
auth_service.key = hasher.hexdigest()
if "APIKEY" in os.environ: auth_service.key = os.environ["APIKEY"]
if not app.debug:
app.logger.addHandler(utils.create_syslog_handler())
# For testing on the command line, you can use `curl` like so:
# curl --user $(</var/lib/mailinabox/api.key): http://localhost:10222/mail/users
auth_service.write_key()
# For testing in the browser, you can copy the API key that's output to the
# debug console and enter that as the username
if app.debug:

View File

@ -62,6 +62,9 @@
ol li {
margin-bottom: 1em;
}
.if-logged-in { display: none; }
.if-logged-in-admin { display: none; }
</style>
<link rel="stylesheet" href="/admin/assets/bootstrap/css/bootstrap-theme.min.css">
</head>
@ -83,7 +86,7 @@
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li class="dropdown">
<li class="dropdown if-logged-in-admin">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">System <b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="#system_status" onclick="return show_panel(this);">Status Checks</a></li>
@ -97,7 +100,8 @@
<li><a href="#postgrey_whitelist" onclick="return show_panel(this);">Postgrey Whitelist</a></li>
</ul>
</li>
<li class="dropdown">
<li><a href="#mail-guide" onclick="return show_panel(this);" class="if-logged-in-not-admin">Mail</a></li>
<li class="dropdown if-logged-in-admin">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Mail &amp; Users <b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="#mail-guide" onclick="return show_panel(this);">Instructions</a></li>
@ -108,18 +112,22 @@
<li><a href="#mfa" onclick="return show_panel(this);">Two-Factor Authentication</a></li>
</ul>
</li>
<li><a href="#sync_guide" onclick="return show_panel(this);">Contacts/Calendar</a></li>
<li><a href="#web" onclick="return show_panel(this);">Web</a></li>
<li><a href="/admin/reports/" onclick="return api_credentials[0]!=''">Activity</a></li>
<li><a href="#sync_guide" onclick="return show_panel(this);" class="if-logged-in">Contacts/Calendar</a></li>
<li><a href="#web" onclick="return show_panel(this);" class="if-logged-in-admin">Web</a></li>
<li><a href="/admin/reports/" onclick="return api_credentials[0]!=''" class="if-logged-in-admin">Activity</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li><a href="#" onclick="do_logout(); return false;" style="color: white">Log out</a></li>
<li class="if-logged-in"><a href="#" onclick="do_logout(); return false;" style="color: white">Log out</a></li>
</ul>
</div><!--/.navbar-collapse -->
</div>
</div>
<div class="container">
<div id="panel_welcome" class="admin_panel">
{% include "welcome.html" %}
</div>
<div id="panel_system_status" class="admin_panel">
{% include "system-status.html" %}
</div>
@ -304,7 +312,7 @@ function ajax_with_indicator(options) {
return false; // handy when called from onclick
}
var api_credentials = ["", ""];
var api_credentials = null;
function api(url, method, data, callback, callback_error, headers) {
// from http://www.webtoolkit.info/javascript-base64.html
function base64encode(input) {
@ -352,9 +360,10 @@ function api(url, method, data, callback, callback_error, headers) {
// We don't store user credentials in a cookie to avoid the hassle of CSRF
// attacks. The Authorization header only gets set in our AJAX calls triggered
// by user actions.
xhr.setRequestHeader(
'Authorization',
'Basic ' + base64encode(api_credentials[0] + ':' + api_credentials[1]));
if (api_credentials)
xhr.setRequestHeader(
'Authorization',
'Basic ' + base64encode(api_credentials.username + ':' + api_credentials.session_key));
},
success: callback,
error: callback_error || default_error,
@ -373,12 +382,21 @@ var current_panel = null;
var switch_back_to_panel = null;
function do_logout() {
api_credentials = ["", ""];
// Clear the session from the backend.
api("/logout", "POST");
// Forget the token.
api_credentials = null;
if (typeof localStorage != 'undefined')
localStorage.removeItem("miab-cp-credentials");
if (typeof sessionStorage != 'undefined')
sessionStorage.removeItem("miab-cp-credentials");
// Return to the start.
show_panel('login');
// Reset menus.
show_hide_menus();
}
function show_panel(panelid) {
@ -401,14 +419,22 @@ function show_panel(panelid) {
$(function() {
// Recall saved user credentials.
if (typeof sessionStorage != 'undefined' && sessionStorage.getItem("miab-cp-credentials"))
api_credentials = sessionStorage.getItem("miab-cp-credentials").split(":");
else if (typeof localStorage != 'undefined' && localStorage.getItem("miab-cp-credentials"))
api_credentials = localStorage.getItem("miab-cp-credentials").split(":");
try {
if (typeof sessionStorage != 'undefined' && sessionStorage.getItem("miab-cp-credentials"))
api_credentials = JSON.parse(sessionStorage.getItem("miab-cp-credentials"));
else if (typeof localStorage != 'undefined' && localStorage.getItem("miab-cp-credentials"))
api_credentials = JSON.parse(localStorage.getItem("miab-cp-credentials"));
} catch (_) {
}
// Toggle menu state.
show_hide_menus();
// Recall what the user was last looking at.
if (typeof localStorage != 'undefined' && localStorage.getItem("miab-cp-lastpanel")) {
if (api_credentials != null && typeof localStorage != 'undefined' && localStorage.getItem("miab-cp-lastpanel")) {
show_panel(localStorage.getItem("miab-cp-lastpanel"));
} else if (api_credentials != null) {
show_panel('welcome');
} else {
show_panel('login');
}

View File

@ -102,11 +102,11 @@ function do_login() {
}
// Exchange the email address & password for an API key.
api_credentials = [$('#loginEmail').val(), $('#loginPassword').val()]
api_credentials = { username: $('#loginEmail').val(), session_key: $('#loginPassword').val() }
api(
"/me",
"GET",
"/login",
"POST",
{},
function(response) {
// This API call always succeeds. It returns a JSON object indicating
@ -141,7 +141,9 @@ function do_login() {
// Login succeeded.
// Save the new credentials.
api_credentials = [response.email, response.api_key];
api_credentials = { username: response.email,
session_key: response.api_key,
privileges: response.privileges };
// Try to wipe the username/password information.
$('#loginEmail').val('');
@ -152,18 +154,21 @@ function do_login() {
// Remember the credentials.
if (typeof localStorage != 'undefined' && typeof sessionStorage != 'undefined') {
if ($('#loginRemember').val()) {
localStorage.setItem("miab-cp-credentials", api_credentials.join(":"));
localStorage.setItem("miab-cp-credentials", JSON.stringify(api_credentials));
sessionStorage.removeItem("miab-cp-credentials");
} else {
localStorage.removeItem("miab-cp-credentials");
sessionStorage.setItem("miab-cp-credentials", api_credentials.join(":"));
sessionStorage.setItem("miab-cp-credentials", JSON.stringify(api_credentials));
}
}
// Toggle menus.
show_hide_menus();
// Open the next panel the user wants to go to. Do this after the XHR response
// is over so that we don't start a new XHR request while this one is finishing,
// which confuses the loading indicator.
setTimeout(function() { show_panel(!switch_back_to_panel || switch_back_to_panel == "login" ? 'system_status' : switch_back_to_panel) }, 300);
setTimeout(function() { show_panel(!switch_back_to_panel || switch_back_to_panel == "login" ? 'welcome' : switch_back_to_panel) }, 300);
}
},
undefined,
@ -183,4 +188,19 @@ function show_login() {
}
});
}
function show_hide_menus() {
var is_logged_in = (api_credentials != null);
var privs = api_credentials ? api_credentials.privileges : [];
$('.if-logged-in').toggle(is_logged_in);
$('.if-logged-in-admin, .if-logged-in-not-admin').toggle(false);
if (is_logged_in) {
$('.if-logged-in-not-admin').toggle(true);
privs.forEach(function(priv) {
$('.if-logged-in-' + priv).toggle(true);
$('.if-logged-in-not-' + priv).toggle(false);
});
}
$('.if-not-logged-in').toggle(!is_logged_in);
}
</script>

View File

@ -222,7 +222,7 @@ function users_set_password(elem) {
var email = $(elem).parents('tr').attr('data-email');
var yourpw = "";
if (api_credentials != null && email == api_credentials[0])
if (api_credentials != null && email == api_credentials.username)
yourpw = "<p class='text-danger'>If you change your own password, you will be logged out of this control panel and will need to log in again.</p>";
show_modal_confirm(
@ -276,7 +276,7 @@ function users_remove(elem) {
var email = $(elem).parents('tr').attr('data-email');
// can't remove yourself
if (api_credentials != null && email == api_credentials[0]) {
if (api_credentials != null && email == api_credentials.username) {
show_modal_error("Archive User", "You cannot archive your own account.");
return;
}
@ -308,7 +308,7 @@ function mod_priv(elem, add_remove) {
var priv = $(elem).parents('td').find('.name').text();
// can't remove your own admin access
if (priv == "admin" && add_remove == "remove" && api_credentials != null && email == api_credentials[0]) {
if (priv == "admin" && add_remove == "remove" && api_credentials != null && email == api_credentials.username) {
show_modal_error("Modify Privileges", "You cannot remove the admin privilege from yourself.");
return;
}

View File

@ -0,0 +1,16 @@
<style>
.title {
margin: 1em;
text-align: center;
}
.subtitle {
margin: 2em;
text-align: center;
}
</style>
<h1 class="title">{{hostname}}</h1>
<p class="subtitle">Welcome to your Mail-in-a-Box control panel.</p>

View File

@ -49,7 +49,7 @@ hide_output $venv/bin/pip install --upgrade pip
# NOTE: email_validator is repeated in setup/questions.sh, so please keep the versions synced.
hide_output $venv/bin/pip install --upgrade \
rtyaml "email_validator>=1.0.0" "exclusiveprocess" \
flask dnspython python-dateutil \
flask dnspython python-dateutil expiringdict \
qrcode[pil] pyotp \
"idna>=2.0.0" "cryptography==2.2.2" boto psutil postfix-mta-sts-resolver b2sdk ldap3

View File

@ -30,8 +30,8 @@ apt_install \
# Combine the Roundcube version number with the commit hash of plugins to track
# whether we have the latest version of everything.
VERSION=1.4.11
HASH=3877f0e70f29e7d0612155632e48c3db1e626be3
VERSION=1.5-rc
HASH=a7cb2a39702536d769c7ff93f716e27f0b93f9d9
PERSISTENT_LOGIN_VERSION=6b3fc450cae23ccb2f393d0ef67aa319e877e435 # version 5.2.0
HTML5_NOTIFIER_VERSION=68d9ca194212e15b3c7225eb6085dbcf02fd13d7 # version 0.6.4+
CARDDAV_VERSION=3.0.3
@ -133,6 +133,7 @@ cat > $RCM_CONFIG <<EOF;
\$config['plugins'] = array('html5_notifier', 'archive', 'zipdownload', 'password', 'managesieve', 'jqueryui', 'persistent_login', 'carddav');
\$config['skin'] = 'elastic';
\$config['login_autocomplete'] = 2;
\$config['login_username_filter'] = 'email';
\$config['password_charset'] = 'UTF-8';
\$config['junk_mbox'] = 'Spam';
\$config['ldap_public']['public'] = array(

View File

@ -232,7 +232,7 @@ if __name__ == "__main__":
run_test(managesieve_test, [], 20, 30, 4)
# Mail-in-a-Box control panel
run_test(http_test, ["/admin/me", 200], 20, 30, 1)
run_test(http_test, ["/admin/login", 200], 20, 30, 1)
# Munin via the Mail-in-a-Box control panel
run_test(http_test, ["/admin/munin/", 401], 20, 30, 1)

View File

@ -358,17 +358,17 @@ mgmt_assert_mfa_disable() {
return 0
}
mgmt_assert_admin_me() {
mgmt_assert_admin_login() {
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
# note: POST /admin/login 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"
record "[POST /admin/login as $user]"
if ! mgmt_rest_as_user "POST" "/admin/login" "$user" "$pw" "$@"; then
test_failure "POST /admin/login as $user failed: $REST_ERROR"
return 1
else
@ -376,11 +376,11 @@ mgmt_assert_admin_me() {
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"
test_failure "Unable to run jq ($code) on /admin/login json"
return 1
elif [ "$status" == "null" ]; then
test_failure "No 'status' in /admin/me json"
test_failure "No 'status' in /admin/login json"
return 1
elif [ "$status" != "$expected_status" ]; then

View File

@ -231,7 +231,7 @@ test_totp() {
# 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-totp-token"
mgmt_assert_admin_login "$alice" "$alice_pw" "missing-totp-token"
fi
@ -248,7 +248,7 @@ test_totp() {
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"
if mgmt_assert_admin_login "$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"
@ -265,15 +265,19 @@ test_totp() {
# 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"
record "[Use the session key to enum users]"
if ! mgmt_rest_as_user "GET" "/admin/mail/users?format=json" "$alice" "$api_key"; then
test_failure "Unable to use the session key to issue a rest call: $REST_ERROR"
else
record "Success: $REST_OUTPUT"
fi
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_mfa_disable "$alice" "$api_key"; then
mgmt_assert_admin_me "$alice" "$alice_pw" "ok"
mgmt_assert_admin_login "$alice" "$alice_pw" "ok"
fi
fi