1
0
mirror of https://github.com/mail-in-a-box/mailinabox.git synced 2025-04-03 00:07:05 +00:00
mailinabox/management/mfa.py
downtownallday d349150dd0 Merge remote-tracking branch 'upstream/main' into merge-upstream
# Conflicts:
#	.gitignore
#	management/auth.py
#	management/daemon.py
#	management/mail_log.py
#	management/mailconfig.py
#	management/mfa.py
#	management/ssl_certificates.py
#	management/status_checks.py
#	management/utils.py
#	management/web_update.py
#	setup/mail-postfix.sh
#	setup/migrate.py
#	setup/preflight.sh
#	setup/webmail.sh
#	tests/test_mail.py
#	tools/editconf.py
2024-03-12 07:41:14 -04:00

155 lines
4.7 KiB
Python

# -*- indent-tabs-mode: t; tab-width: 4; python-indent-offset: 4; -*-
#####
##### This file is part of Mail-in-a-Box-LDAP which is released under the
##### terms of the GNU Affero General Public License as published by the
##### Free Software Foundation, either version 3 of the License, or (at
##### your option) any later version. See file LICENSE or go to
##### https://github.com/downtownallday/mailinabox-ldap for full license
##### details.
#####
from mailconfig import open_database, find_mail_user
import mfa_totp
def strip_order_prefix(rec, attributes):
'''strip the order prefix from X-ORDERED ldap values for the
list of attributes specified
`rec` is modified in-place
the server returns X-ORDERED values ordered so the values will be
sorted in the record making the prefix superfluous.
For example, the function will change:
totpSecret: {0}secret1
totpSecret: {1}secret2
to:
totpSecret: secret1
totpSecret: secret2
TODO: move to backend.py and/or integrate with LdapConnection.search()
'''
for attr in attributes:
# ignore attribute that doesn't exist
if not attr in rec: continue
# ..as well as None values and empty list
if not rec[attr]: continue
newvals = []
for val in rec[attr]:
i = val.find('}')
newvals.append(val[i+1:])
rec[attr] = newvals
def get_mfa_user(email, env, conn=None):
'''get the ldap record for the user along with all MFA-related
attributes
'''
user = find_mail_user(env, email, ['objectClass','totpSecret','totpMruToken','totpMruTokenTime','totpLabel'], conn)
if not user:
raise ValueError("User does not exist.")
strip_order_prefix(user, ['totpSecret','totpMruToken','totpMruTokenTime','totpLabel'])
return user
def get_mfa_state(email, env):
'''return details about what MFA schemes are enabled for a user
ordered by the priority that the scheme will be tried, with index
zero being the first.
'''
user = get_mfa_user(email, env)
state_list = []
state_list += mfa_totp.get_state(user)
return state_list
def get_public_mfa_state(email, env):
'''return details about what MFA schemes are enabled for a user
ordered by the priority that the scheme will be tried, with index
zero being the first. No secrets are returned by this function -
only those details that are needed by the end user to identify a
particular MFA by label and the id of each so it may be disabled.
'''
mfa_state = get_mfa_state(email, env)
return [
{ "id": s["id"], "type": s["type"], "label": s["label"] }
for s in mfa_state
]
def get_hash_mfa_state(email, env):
'''return details about what MFA schemes are enabled for a user
ordered by the priority that the scheme will be tried, with index
zero being the first. This function may return secrets. It's
intended use is for the result to be included as part of the input
to a hashing function to generate a user api key (see
auth.py:create_user_key)
'''
mfa_state = get_mfa_state(email, env)
return [
{ "id": s["id"], "type": s["type"], "secret": s["secret"] }
for s in mfa_state
]
def enable_mfa(email, type, secret, token, label, env):
'''enable MFA using the scheme specified in `type`. users may have
multiple mfa schemes enabled of the same type.
'''
user = get_mfa_user(email, env)
if type == "totp":
mfa_totp.enable(user, secret, token, label, env)
else:
msg = "Invalid MFA type."
raise ValueError(msg)
def disable_mfa(email, mfa_id, env):
'''disable a specific MFA scheme. `mfa_id` identifies the specific
entry and is available in the `id` field of an item in the list
obtained from get_mfa_state()
'''
user = get_mfa_user(email, env)
if mfa_id is None:
# Disable all MFA for a user.
return mfa_totp.disable(user, None, env)
elif mfa_id.startswith("totp:"):
# Disable a particular MFA mode for a user.
return mfa_totp.disable(user, mfa_id, env)
else:
return False
def validate_auth_mfa(email, request, env):
# Validates that a login request satisfies any MFA modes
# that have been enabled for the user's account. Returns
# a tuple (status, [hints]). status is True for a successful
# MFA login, False for a missing token. If status is False,
# hints is an array of codes that indicate what the user
# can try. Possible codes are:
# "missing-totp-token"
# "invalid-totp-token"
mfa_state = get_mfa_state(email, env)
# If no MFA modes are added, return True.
if len(mfa_state) == 0:
return (True, [])
# Try the enabled MFA modes.
hints = set()
for mfa_mode in mfa_state:
if mfa_mode["type"] == "totp":
user = get_mfa_user(email, env)
result, hint = mfa_totp.validate_auth(user, mfa_mode, request, True, env)
if not result:
hints.add(hint)
else:
return (True, [])
# On a failed login, indicate failure and any hints for what the user can do instead.
return (False, list(hints))