1
0
mirror of https://github.com/mail-in-a-box/mailinabox.git synced 2025-04-29 04:17:07 +00:00

refactor management daemon authentication a) to separate authorization and b) to use 'doveadm pw' rather than 'doveadm auth test' so that it is decoupled from dovecot's login mechanism

This commit is contained in:
Joshua Tauberer 2014-11-30 10:43:07 -05:00
parent 31d6128a2b
commit c2ec6e66b8
4 changed files with 89 additions and 29 deletions

View File

@ -3,7 +3,7 @@ import base64, os, os.path
from flask import make_response
import utils
from mailconfig import get_mail_user_privileges
from mailconfig import get_mail_password, get_mail_user_privileges
DEFAULT_KEY_PATH = '/var/lib/mailinabox/api.key'
DEFAULT_AUTH_REALM = 'Mail-in-a-Box Management Server'
@ -40,10 +40,25 @@ class KeyAuthService:
with create_file_with_mode(self.key_path, 0o640) as key_file:
key_file.write(self.key + '\n')
def is_authenticated(self, request, env):
def authorize_admin(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.
Returns 'OK' if the key is good or the user is an administrator, otherwise an error message."""
Returns OK on success or another string with an error message."""
try:
privs = self.authenticate(request, env)
except ValueError as e:
# Don't reveal whether the email address was valid on failure.
return "Invalid email address or password."
if "admin" not in privs:
return "You are not an administrator for this system."
else:
return "OK"
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.
Returns a list of user privileges (e.g. [] or ['admin']) raise a ValueError on
login failure."""
def decode(s):
return base64.b64decode(s.encode('ascii')).decode('ascii')
@ -63,46 +78,56 @@ class KeyAuthService:
header = request.headers.get('Authorization')
if not header:
return "No authorization header provided."
raise ValueError("No authorization header provided.")
username, password = parse_basic_auth(header)
if username in (None, ""):
return "Authorization header invalid."
raise ValueError("Authorization header invalid.")
elif username == self.key:
return "OK"
# The user passed the API key which grants administrative privs.
return ["admin"]
else:
return self.check_imap_login( username, password, env)
# The user is trying to log in with a username and password.
# Raises or returns privs.
return self.get_user_credentials(username, password, env)
def check_imap_login(self, email, pw, env):
# Validate a user's credentials.
def get_user_credentials(self, email, pw, env):
# Validate a user's credentials. On success returns a list of
# privileges (e.g. [] or ['admin']). On failure raises a ValueError
# with a login error message.
# Sanity check.
if email == "" or pw == "":
return "Enter an email address and password."
raise ValueError("Enter an email address and password.")
# Get the hashed password of the user. Raise a ValueError if the
# email address does not correspond to a user.
pw_hash = get_mail_password(email, env)
# Authenticate.
try:
# Use doveadm to check credentials. doveadm will return
# Use 'doveadm pw' to check credentials. doveadm will return
# a non-zero exit status if the credentials are no good,
# and check_call will raise an exception in that case.
utils.shell('check_call', [
"/usr/bin/doveadm",
"auth", "test",
email, pw
"/usr/bin/doveadm", "pw",
"-p", pw,
"-t", pw_hash,
])
except:
# Login failed.
return "Invalid email address or password."
raise ValueError("Invalid password.")
# Authorize.
# (This call should never fail on a valid user.)
# Get privileges for authorization.
# (This call should never fail on a valid user. But if it did fail, it would
# return a tuple of an error message and an HTTP status code.)
privs = get_mail_user_privileges(email, env)
if isinstance(privs, tuple): raise Exception("Error getting privileges.")
if "admin" not in privs:
return "You are not an administrator for this system."
return "OK"
# Return a list of privileges.
return privs
def _generate_key(self):
raw_key = os.urandom(32)

View File

@ -29,7 +29,7 @@ def authorized_personnel_only(viewfunc):
@wraps(viewfunc)
def newview(*args, **kwargs):
# Check if the user is authorized.
authorized_status = auth_service.is_authenticated(request, env)
authorized_status = auth_service.authorize_admin(request, env)
if authorized_status == "OK":
# Authorized. Call view func.
return viewfunc(*args, **kwargs)
@ -81,16 +81,26 @@ def index():
@app.route('/me')
def me():
# Is the caller authorized?
authorized_status = auth_service.is_authenticated(request, env)
if authorized_status != "OK":
try:
privs = auth_service.authenticate(request, env)
except ValueError as e:
# Don't reveal whether the email address was valid on failure.
return json_response({
"status": "not-authorized",
"reason": authorized_status,
"reason": "Invalid email address or password.",
})
return json_response({
resp = {
"status": "authorized",
"api_key": auth_service.key,
})
"privileges": privs,
}
# Is authorized as admin?
if "admin" in privs:
resp["api_key"] = auth_service.key
# Return.
return json_response(resp)
# MAIL

View File

@ -251,7 +251,7 @@ def add_mail_user(email, pw, privs, env):
conn, c = open_database(env, with_connection=True)
# hash the password
pw = utils.shell('check_output', ["/usr/bin/doveadm", "pw", "-s", "SHA512-CRYPT", "-p", pw]).strip()
pw = hash_password(pw)
# add the user to the database
try:
@ -287,7 +287,7 @@ def set_mail_password(email, pw, env):
validate_password(pw)
# hash the password
pw = utils.shell('check_output', ["/usr/bin/doveadm", "pw", "-s", "SHA512-CRYPT", "-p", pw]).strip()
pw = hash_password(pw)
# update the database
conn, c = open_database(env, with_connection=True)
@ -297,6 +297,24 @@ def set_mail_password(email, pw, env):
conn.commit()
return "OK"
def hash_password(pw):
# Turn the plain password into a Dovecot-format hashed password, meaning
# something like "{SCHEME}hashedpassworddata".
# http://wiki2.dovecot.org/Authentication/PasswordSchemes
return utils.shell('check_output', ["/usr/bin/doveadm", "pw", "-s", "SHA512-CRYPT", "-p", pw]).strip()
def get_mail_password(email, env):
# Gets the hashed password for a user. Passwords are stored in Dovecot's
# password format, with a prefixed scheme.
# http://wiki2.dovecot.org/Authentication/PasswordSchemes
# update the database
c = open_database(env)
c.execute('SELECT password FROM users WHERE email=?', (email,))
rows = c.fetchall()
if len(rows) != 1:
raise ValueError("That's not a user (%s)." % email)
return rows[0][0]
def remove_mail_user(email, env):
conn, c = open_database(env, with_connection=True)
c.execute("DELETE FROM users WHERE email=?", (email,))

View File

@ -74,6 +74,13 @@ function do_login() {
// Reset any saved credentials.
do_logout();
} else if (!("api_key" in response)) {
// Login succeeded but user might not be authorized!
show_modal_error("Login Failed", "You are not an administrator on this system.")
// Reset any saved credentials.
do_logout();
} else {
// Login succeeded.