Add MFA list/disable to the management CLI so admins can restore access if MFA device is lost
This commit is contained in:
parent
ac9ecc3bd3
commit
545e7a52e4
6 changed files with 59 additions and 19 deletions
|
@ -1666,16 +1666,16 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
/mfa/status:
|
/mfa/status:
|
||||||
get:
|
post:
|
||||||
tags:
|
tags:
|
||||||
- MFA
|
- MFA
|
||||||
summary: Retrieve MFA status
|
summary: Retrieve MFA status for you or another user
|
||||||
description: Retrieves which type of MFA is used and configuration
|
description: Retrieves which type of MFA is used and configuration
|
||||||
operationId: mfaStatus
|
operationId: mfaStatus
|
||||||
x-codeSamples:
|
x-codeSamples:
|
||||||
- lang: curl
|
- lang: curl
|
||||||
source: |
|
source: |
|
||||||
curl -X GET "https://{host}/admin/mfa/status" \
|
curl -X POST "https://{host}/admin/mfa/status" \
|
||||||
-u "<email>:<password>"
|
-u "<email>:<password>"
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
|
@ -1733,8 +1733,8 @@ paths:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
- MFA
|
- MFA
|
||||||
summary: Disable multi-factor authentication
|
summary: Disable multi-factor authentication for you or another user
|
||||||
description: Disables multi-factor authentication for the currently logged-in admin user. Either disables all multi-factor authentication methods or the method corresponding to the optional property `mfa_id`
|
description: Disables multi-factor authentication for the currently logged-in admin user or another user if a 'user' parameter is submitted. Either disables all multi-factor authentication methods or the method corresponding to the optional property `mfa_id`.
|
||||||
operationId: mfaTotpDisable
|
operationId: mfaTotpDisable
|
||||||
requestBody:
|
requestBody:
|
||||||
required: false
|
required: false
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
# root API key. This file is readable only by root, so this
|
# root API key. This file is readable only by root, so this
|
||||||
# tool can only be used as root.
|
# tool can only be used as root.
|
||||||
|
|
||||||
import sys, getpass, urllib.request, urllib.error, json, re
|
import sys, getpass, urllib.request, urllib.error, json, re, csv
|
||||||
|
|
||||||
def mgmt(cmd, data=None, is_json=False):
|
def mgmt(cmd, data=None, is_json=False):
|
||||||
# The base URL for the management daemon. (Listens on IPv4 only.)
|
# The base URL for the management daemon. (Listens on IPv4 only.)
|
||||||
|
@ -60,14 +60,16 @@ def setup_key_auth(mgmt_uri):
|
||||||
|
|
||||||
if len(sys.argv) < 2:
|
if len(sys.argv) < 2:
|
||||||
print("""Usage:
|
print("""Usage:
|
||||||
{cli} user (lists users)
|
{cli} user (lists users)
|
||||||
{cli} user add user@domain.com [password]
|
{cli} user add user@domain.com [password]
|
||||||
{cli} user password user@domain.com [password]
|
{cli} user password user@domain.com [password]
|
||||||
{cli} user remove user@domain.com
|
{cli} user remove user@domain.com
|
||||||
{cli} user make-admin user@domain.com
|
{cli} user make-admin user@domain.com
|
||||||
{cli} user remove-admin user@domain.com
|
{cli} user remove-admin user@domain.com
|
||||||
{cli} user admins (lists admins)
|
{cli} user admins (lists admins)
|
||||||
{cli} alias (lists aliases)
|
{cli} user mfa show user@domain.com (shows MFA devices for user, if any)
|
||||||
|
{cli} user mfa disable user@domain.com [id] (disables MFA for user)
|
||||||
|
{cli} alias (lists aliases)
|
||||||
{cli} alias add incoming.name@domain.com sent.to@other.domain.com
|
{cli} alias add incoming.name@domain.com sent.to@other.domain.com
|
||||||
{cli} alias add incoming.name@domain.com 'sent.to@other.domain.com, multiple.people@other.domain.com'
|
{cli} alias add incoming.name@domain.com 'sent.to@other.domain.com, multiple.people@other.domain.com'
|
||||||
{cli} alias remove incoming.name@domain.com
|
{cli} alias remove incoming.name@domain.com
|
||||||
|
@ -121,6 +123,18 @@ elif sys.argv[1] == "user" and sys.argv[2] == "admins":
|
||||||
if "admin" in user['privileges']:
|
if "admin" in user['privileges']:
|
||||||
print(user['email'])
|
print(user['email'])
|
||||||
|
|
||||||
|
elif sys.argv[1] == "user" and len(sys.argv) == 5 and sys.argv[2:4] == ["mfa", "show"]:
|
||||||
|
# Show MFA status for a user.
|
||||||
|
status = mgmt("/mfa/status", { "user": sys.argv[4] }, is_json=True)
|
||||||
|
W = csv.writer(sys.stdout)
|
||||||
|
W.writerow(["id", "type", "label"])
|
||||||
|
for mfa in status["enabled_mfa"]:
|
||||||
|
W.writerow([mfa["id"], mfa["type"], mfa["label"]])
|
||||||
|
|
||||||
|
elif sys.argv[1] == "user" and len(sys.argv) in (5, 6) and sys.argv[2:4] == ["mfa", "disable"]:
|
||||||
|
# Disable MFA (all or a particular device) for a user.
|
||||||
|
print(mgmt("/mfa/disable", { "user": sys.argv[4], "mfa-id": sys.argv[5] if len(sys.argv) == 6 else None }))
|
||||||
|
|
||||||
elif sys.argv[1] == "alias" and len(sys.argv) == 2:
|
elif sys.argv[1] == "alias" and len(sys.argv) == 2:
|
||||||
print(mgmt("/mail/aliases"))
|
print(mgmt("/mail/aliases"))
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ from functools import wraps
|
||||||
|
|
||||||
from flask import Flask, request, render_template, abort, Response, send_from_directory, make_response
|
from flask import Flask, request, render_template, abort, Response, send_from_directory, make_response
|
||||||
|
|
||||||
import auth, utils, mfa
|
import auth, utils
|
||||||
from mailconfig import get_mail_users, get_mail_users_ex, get_admins, add_mail_user, set_mail_password, remove_mail_user
|
from mailconfig import get_mail_users, get_mail_users_ex, get_admins, add_mail_user, set_mail_password, remove_mail_user
|
||||||
from mailconfig import get_mail_user_privileges, add_remove_mail_user_privilege
|
from mailconfig import get_mail_user_privileges, add_remove_mail_user_privilege
|
||||||
from mailconfig import get_mail_aliases, get_mail_aliases_ex, get_mail_domains, add_mail_alias, remove_mail_alias
|
from mailconfig import get_mail_aliases, get_mail_aliases_ex, get_mail_domains, add_mail_alias, remove_mail_alias
|
||||||
|
@ -399,15 +399,27 @@ def ssl_provision_certs():
|
||||||
|
|
||||||
# multi-factor auth
|
# multi-factor auth
|
||||||
|
|
||||||
@app.route('/mfa/status', methods=['GET'])
|
@app.route('/mfa/status', methods=['POST'])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only
|
||||||
def mfa_get_status():
|
def mfa_get_status():
|
||||||
return json_response({
|
# Anyone accessing this route is an admin, and we permit them to
|
||||||
"enabled_mfa": get_public_mfa_state(request.user_email, env),
|
# see the MFA status for any user if they submit a 'user' form
|
||||||
"new_mfa": {
|
# field. But we don't include provisioning info since a user can
|
||||||
"totp": provision_totp(request.user_email, env)
|
# only provision for themselves.
|
||||||
|
email = request.form.get('user', request.user_email) # user field if given, otherwise the user making the request
|
||||||
|
try:
|
||||||
|
resp = {
|
||||||
|
"enabled_mfa": get_public_mfa_state(email, env)
|
||||||
}
|
}
|
||||||
})
|
if email == request.user_email:
|
||||||
|
resp.update({
|
||||||
|
"new_mfa": {
|
||||||
|
"totp": provision_totp(email, env)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
except ValueError as e:
|
||||||
|
return (str(e), 400)
|
||||||
|
return json_response(resp)
|
||||||
|
|
||||||
@app.route('/mfa/totp/enable', methods=['POST'])
|
@app.route('/mfa/totp/enable', methods=['POST'])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only
|
||||||
|
@ -427,8 +439,18 @@ def totp_post_enable():
|
||||||
@app.route('/mfa/disable', methods=['POST'])
|
@app.route('/mfa/disable', methods=['POST'])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only
|
||||||
def totp_post_disable():
|
def totp_post_disable():
|
||||||
disable_mfa(request.user_email, request.form.get('mfa-id'), env)
|
# Anyone accessing this route is an admin, and we permit them to
|
||||||
return "OK"
|
# disable the MFA status for any user if they submit a 'user' form
|
||||||
|
# field.
|
||||||
|
email = request.form.get('user', request.user_email) # user field if given, otherwise the user making the request
|
||||||
|
try:
|
||||||
|
result = disable_mfa(email, request.form.get('mfa-id') or None, env) # convert empty string to None
|
||||||
|
except ValueError as e:
|
||||||
|
return (str(e), 400)
|
||||||
|
if result: # success
|
||||||
|
return "OK"
|
||||||
|
else: # error
|
||||||
|
return ("Invalid user or MFA id.", 400)
|
||||||
|
|
||||||
# WEB
|
# WEB
|
||||||
|
|
||||||
|
|
|
@ -63,6 +63,7 @@ def disable_mfa(email, mfa_id, env):
|
||||||
# Disable a particular MFA mode for a user.
|
# Disable a particular MFA mode for a user.
|
||||||
c.execute('DELETE FROM mfa WHERE user_id=? AND id=?', (get_user_id(email, c), mfa_id))
|
c.execute('DELETE FROM mfa WHERE user_id=? AND id=?', (get_user_id(email, c), mfa_id))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
return c.rowcount > 0
|
||||||
|
|
||||||
def validate_totp_secret(secret):
|
def validate_totp_secret(secret):
|
||||||
if type(secret) != str or secret.strip() == "":
|
if type(secret) != str or secret.strip() == "":
|
||||||
|
|
|
@ -186,7 +186,7 @@ and ensure every administrator account for this control panel does the same.</st
|
||||||
|
|
||||||
api(
|
api(
|
||||||
'/mfa/status',
|
'/mfa/status',
|
||||||
'GET',
|
'POST',
|
||||||
{},
|
{},
|
||||||
function(res) {
|
function(res) {
|
||||||
el.wrapper.classList.add('loaded');
|
el.wrapper.classList.add('loaded');
|
||||||
|
|
3
tools/mail.py
Executable file
3
tools/mail.py
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# This script has moved.
|
||||||
|
management/cli.py "$@"
|
Loading…
Reference in a new issue