Roundcube: Use Mail-in-a-Box admin API to drive password changes (#92)
* Use Mail-in-a-Box driver We're using the user's own credentials to authenticate themselves. There are some issues if we release as-is: * Only usable if the user in question is an admin * Cannot be used if the user has 2FA enabled * daemon: Add selective gatekeeper * Allows us to give access to features for logged in, non-admin users * Allow non-admins to change their own password * Begin password management self service, frontend * Allow all users to enable 2FA * Password change front-end form * Self password change front-end functionality * Force logout after successful password change * Clear fields after successful password change, also fix error modal
This commit is contained in:
parent
b961a2b74a
commit
3451dadde5
4 changed files with 192 additions and 115 deletions
|
@ -56,71 +56,70 @@ app = Flask(__name__,
|
||||||
|
|
||||||
# Decorator to protect views that require a user with 'admin' privileges.
|
# Decorator to protect views that require a user with 'admin' privileges.
|
||||||
|
|
||||||
|
def authorized_personnel_only(admin = True):
|
||||||
|
def gatekeeper(viewfunc):
|
||||||
|
|
||||||
def authorized_personnel_only(viewfunc):
|
@wraps(viewfunc)
|
||||||
|
def newview(*args, **kwargs):
|
||||||
|
# Authenticate the passed credentials, which is either the API key or a username:password pair
|
||||||
|
# and an optional X-Auth-Token token.
|
||||||
|
error = None
|
||||||
|
privs = []
|
||||||
|
|
||||||
@wraps(viewfunc)
|
try:
|
||||||
def newview(*args, **kwargs):
|
email, privs = auth_service.authenticate(request, env)
|
||||||
# Authenticate the passed credentials, which is either the API key or a username:password pair
|
|
||||||
# and an optional X-Auth-Token token.
|
|
||||||
error = None
|
|
||||||
privs = []
|
|
||||||
|
|
||||||
try:
|
# Store the email address of the logged in user so it can be accessed
|
||||||
email, privs = auth_service.authenticate(request, env)
|
# from the API methods that affect the calling user.
|
||||||
except ValueError as e:
|
request.user_email = email
|
||||||
# Write a line in the log recording the failed login, unless no authorization header
|
request.user_privs = privs
|
||||||
# was given which can happen on an initial request before a 403 response.
|
|
||||||
if "Authorization" in request.headers:
|
|
||||||
log_failed_login(request)
|
|
||||||
|
|
||||||
# Authentication failed.
|
if not admin or "admin" in privs:
|
||||||
error = str(e)
|
return viewfunc(*args, **kwargs)
|
||||||
|
else:
|
||||||
|
error = "You are not an administrator."
|
||||||
|
except ValueError as e:
|
||||||
|
# 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)
|
||||||
|
|
||||||
# Authorized to access an API view?
|
# Authentication failed.
|
||||||
if "admin" in privs:
|
error = str(e)
|
||||||
# Store the email address of the logged in user so it can be accessed
|
|
||||||
# from the API methods that affect the calling user.
|
|
||||||
request.user_email = email
|
|
||||||
request.user_privs = privs
|
|
||||||
|
|
||||||
# Call view func.
|
# Not authorized. Return a 401 (send auth) and a prompt to authorize by default.
|
||||||
return viewfunc(*args, **kwargs)
|
status = 401
|
||||||
|
headers = {
|
||||||
|
'WWW-Authenticate':
|
||||||
|
'Basic realm="{0}"'.format(auth_service.auth_realm),
|
||||||
|
'X-Reason': error,
|
||||||
|
}
|
||||||
|
|
||||||
if not error:
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||||
error = "You are not an administrator."
|
# Don't issue a 401 to an AJAX request because the user will
|
||||||
|
# be prompted for credentials, which is not helpful.
|
||||||
|
status = 403
|
||||||
|
headers = None
|
||||||
|
|
||||||
# Not authorized. Return a 401 (send auth) and a prompt to authorize by default.
|
if request.headers.get('Accept') in (None, "", "*/*"):
|
||||||
status = 401
|
# Return plain text output.
|
||||||
headers = {
|
return Response(error + "\n",
|
||||||
'WWW-Authenticate':
|
status=status,
|
||||||
'Basic realm="{0}"'.format(auth_service.auth_realm),
|
mimetype='text/plain',
|
||||||
'X-Reason': error,
|
headers=headers)
|
||||||
}
|
else:
|
||||||
|
# Return JSON output.
|
||||||
|
return Response(json.dumps({
|
||||||
|
"status": "error",
|
||||||
|
"reason": error,
|
||||||
|
}) + "\n",
|
||||||
|
status=status,
|
||||||
|
mimetype='application/json',
|
||||||
|
headers=headers)
|
||||||
|
|
||||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
return newview
|
||||||
# Don't issue a 401 to an AJAX request because the user will
|
|
||||||
# be prompted for credentials, which is not helpful.
|
|
||||||
status = 403
|
|
||||||
headers = None
|
|
||||||
|
|
||||||
if request.headers.get('Accept') in (None, "", "*/*"):
|
return gatekeeper
|
||||||
# Return plain text output.
|
|
||||||
return Response(error + "\n",
|
|
||||||
status=status,
|
|
||||||
mimetype='text/plain',
|
|
||||||
headers=headers)
|
|
||||||
else:
|
|
||||||
# Return JSON output.
|
|
||||||
return Response(json.dumps({
|
|
||||||
"status": "error",
|
|
||||||
"reason": error,
|
|
||||||
}) + "\n",
|
|
||||||
status=status,
|
|
||||||
mimetype='application/json',
|
|
||||||
headers=headers)
|
|
||||||
|
|
||||||
return newview
|
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(401)
|
@app.errorhandler(401)
|
||||||
|
@ -213,7 +212,7 @@ def logout():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/mail/users')
|
@app.route('/mail/users')
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def mail_users():
|
def mail_users():
|
||||||
if request.args.get("format", "") == "json":
|
if request.args.get("format", "") == "json":
|
||||||
return json_response(get_mail_users_ex(env, with_archived=True))
|
return json_response(get_mail_users_ex(env, with_archived=True))
|
||||||
|
@ -222,7 +221,7 @@ def mail_users():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/mail/users/add', methods=['POST'])
|
@app.route('/mail/users/add', methods=['POST'])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def mail_users_add():
|
def mail_users_add():
|
||||||
quota = request.form.get('quota', get_default_quota(env))
|
quota = request.form.get('quota', get_default_quota(env))
|
||||||
try:
|
try:
|
||||||
|
@ -234,7 +233,7 @@ def mail_users_add():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/mail/users/quota', methods=['GET'])
|
@app.route('/mail/users/quota', methods=['GET'])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def get_mail_users_quota():
|
def get_mail_users_quota():
|
||||||
email = request.values.get('email', '')
|
email = request.values.get('email', '')
|
||||||
quota = get_mail_quota(email, env)
|
quota = get_mail_quota(email, env)
|
||||||
|
@ -246,7 +245,7 @@ def get_mail_users_quota():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/mail/users/quota', methods=['POST'])
|
@app.route('/mail/users/quota', methods=['POST'])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def mail_users_quota():
|
def mail_users_quota():
|
||||||
try:
|
try:
|
||||||
return set_mail_quota(request.form.get('email', ''),
|
return set_mail_quota(request.form.get('email', ''),
|
||||||
|
@ -256,8 +255,13 @@ def mail_users_quota():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/mail/users/password', methods=['POST'])
|
@app.route('/mail/users/password', methods=['POST'])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only(admin = False)
|
||||||
def mail_users_password():
|
def mail_users_password():
|
||||||
|
if "admin" not in request.user_privs:
|
||||||
|
# Non-admins can only change their own password.
|
||||||
|
if request.form.get('email', '') != request.user_email:
|
||||||
|
return ("You are not an administrator; you can only change your own password!", 403)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return set_mail_password(request.form.get('email', ''),
|
return set_mail_password(request.form.get('email', ''),
|
||||||
request.form.get('password', ''), env)
|
request.form.get('password', ''), env)
|
||||||
|
@ -266,13 +270,13 @@ def mail_users_password():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/mail/users/remove', methods=['POST'])
|
@app.route('/mail/users/remove', methods=['POST'])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def mail_users_remove():
|
def mail_users_remove():
|
||||||
return remove_mail_user(request.form.get('email', ''), env)
|
return remove_mail_user(request.form.get('email', ''), env)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/mail/users/privileges')
|
@app.route('/mail/users/privileges')
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def mail_user_privs():
|
def mail_user_privs():
|
||||||
privs = get_mail_user_privileges(request.args.get('email', ''), env)
|
privs = get_mail_user_privileges(request.args.get('email', ''), env)
|
||||||
if isinstance(privs, tuple):
|
if isinstance(privs, tuple):
|
||||||
|
@ -281,7 +285,7 @@ def mail_user_privs():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/mail/users/privileges/add', methods=['POST'])
|
@app.route('/mail/users/privileges/add', methods=['POST'])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def mail_user_privs_add():
|
def mail_user_privs_add():
|
||||||
return add_remove_mail_user_privilege(request.form.get('email', ''),
|
return add_remove_mail_user_privilege(request.form.get('email', ''),
|
||||||
request.form.get('privilege', ''),
|
request.form.get('privilege', ''),
|
||||||
|
@ -289,7 +293,7 @@ def mail_user_privs_add():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/mail/users/privileges/remove', methods=['POST'])
|
@app.route('/mail/users/privileges/remove', methods=['POST'])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def mail_user_privs_remove():
|
def mail_user_privs_remove():
|
||||||
return add_remove_mail_user_privilege(request.form.get('email', ''),
|
return add_remove_mail_user_privilege(request.form.get('email', ''),
|
||||||
request.form.get('privilege', ''),
|
request.form.get('privilege', ''),
|
||||||
|
@ -297,7 +301,7 @@ def mail_user_privs_remove():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/mail/aliases')
|
@app.route('/mail/aliases')
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def mail_aliases():
|
def mail_aliases():
|
||||||
if request.args.get("format", "") == "json":
|
if request.args.get("format", "") == "json":
|
||||||
return json_response(get_mail_aliases_ex(env))
|
return json_response(get_mail_aliases_ex(env))
|
||||||
|
@ -308,7 +312,7 @@ def mail_aliases():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/mail/aliases/add', methods=['POST'])
|
@app.route('/mail/aliases/add', methods=['POST'])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def mail_aliases_add():
|
def mail_aliases_add():
|
||||||
return add_mail_alias(request.form.get('address', ''),
|
return add_mail_alias(request.form.get('address', ''),
|
||||||
request.form.get('forwards_to', ''),
|
request.form.get('forwards_to', ''),
|
||||||
|
@ -319,13 +323,13 @@ def mail_aliases_add():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/mail/aliases/remove', methods=['POST'])
|
@app.route('/mail/aliases/remove', methods=['POST'])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def mail_aliases_remove():
|
def mail_aliases_remove():
|
||||||
return remove_mail_alias(request.form.get('address', ''), env)
|
return remove_mail_alias(request.form.get('address', ''), env)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/mail/domains')
|
@app.route('/mail/domains')
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def mail_domains():
|
def mail_domains():
|
||||||
return "".join(x + "\n" for x in get_mail_domains(env))
|
return "".join(x + "\n" for x in get_mail_domains(env))
|
||||||
|
|
||||||
|
@ -334,14 +338,14 @@ def mail_domains():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/dns/zones')
|
@app.route('/dns/zones')
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def dns_zones():
|
def dns_zones():
|
||||||
from dns_update import get_dns_zones
|
from dns_update import get_dns_zones
|
||||||
return json_response([z[0] for z in get_dns_zones(env)])
|
return json_response([z[0] for z in get_dns_zones(env)])
|
||||||
|
|
||||||
|
|
||||||
@app.route('/dns/update', methods=['POST'])
|
@app.route('/dns/update', methods=['POST'])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def dns_update():
|
def dns_update():
|
||||||
from dns_update import do_dns_update
|
from dns_update import do_dns_update
|
||||||
try:
|
try:
|
||||||
|
@ -351,7 +355,7 @@ def dns_update():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/dns/secondary-nameserver')
|
@app.route('/dns/secondary-nameserver')
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def dns_get_secondary_nameserver():
|
def dns_get_secondary_nameserver():
|
||||||
from dns_update import get_custom_dns_config, get_secondary_dns
|
from dns_update import get_custom_dns_config, get_secondary_dns
|
||||||
return json_response({
|
return json_response({
|
||||||
|
@ -361,7 +365,7 @@ def dns_get_secondary_nameserver():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/dns/secondary-nameserver', methods=['POST'])
|
@app.route('/dns/secondary-nameserver', methods=['POST'])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def dns_set_secondary_nameserver():
|
def dns_set_secondary_nameserver():
|
||||||
from dns_update import set_secondary_dns
|
from dns_update import set_secondary_dns
|
||||||
try:
|
try:
|
||||||
|
@ -375,7 +379,7 @@ def dns_set_secondary_nameserver():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/dns/custom')
|
@app.route('/dns/custom')
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def dns_get_records(qname=None, rtype=None):
|
def dns_get_records(qname=None, rtype=None):
|
||||||
# Get the current set of custom DNS records.
|
# Get the current set of custom DNS records.
|
||||||
from dns_update import get_custom_dns_config, get_dns_zones
|
from dns_update import get_custom_dns_config, get_dns_zones
|
||||||
|
@ -431,7 +435,7 @@ def dns_get_records(qname=None, rtype=None):
|
||||||
@app.route('/dns/custom/<qname>', methods=['GET', 'POST', 'PUT', 'DELETE'])
|
@app.route('/dns/custom/<qname>', methods=['GET', 'POST', 'PUT', 'DELETE'])
|
||||||
@app.route('/dns/custom/<qname>/<rtype>',
|
@app.route('/dns/custom/<qname>/<rtype>',
|
||||||
methods=['GET', 'POST', 'PUT', 'DELETE'])
|
methods=['GET', 'POST', 'PUT', 'DELETE'])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def dns_set_record(qname, rtype="A"):
|
def dns_set_record(qname, rtype="A"):
|
||||||
from dns_update import do_dns_update, set_custom_dns_record
|
from dns_update import do_dns_update, set_custom_dns_record
|
||||||
try:
|
try:
|
||||||
|
@ -498,14 +502,14 @@ def dns_set_record(qname, rtype="A"):
|
||||||
|
|
||||||
|
|
||||||
@app.route('/dns/dump')
|
@app.route('/dns/dump')
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def dns_get_dump():
|
def dns_get_dump():
|
||||||
from dns_update import build_recommended_dns
|
from dns_update import build_recommended_dns
|
||||||
return json_response(build_recommended_dns(env))
|
return json_response(build_recommended_dns(env))
|
||||||
|
|
||||||
|
|
||||||
@app.route('/dns/zonefile/<zone>')
|
@app.route('/dns/zonefile/<zone>')
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def dns_get_zonefile(zone):
|
def dns_get_zonefile(zone):
|
||||||
from dns_update import get_dns_zonefile
|
from dns_update import get_dns_zonefile
|
||||||
return Response(get_dns_zonefile(zone, env),
|
return Response(get_dns_zonefile(zone, env),
|
||||||
|
@ -517,7 +521,7 @@ def dns_get_zonefile(zone):
|
||||||
|
|
||||||
|
|
||||||
@app.route('/ssl/status')
|
@app.route('/ssl/status')
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def ssl_get_status():
|
def ssl_get_status():
|
||||||
from ssl_certificates import get_certificates_to_provision
|
from ssl_certificates import get_certificates_to_provision
|
||||||
from web_update import get_web_domains_info, get_web_domains
|
from web_update import get_web_domains_info, get_web_domains
|
||||||
|
@ -557,7 +561,7 @@ def ssl_get_status():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/ssl/csr/<domain>', methods=['POST'])
|
@app.route('/ssl/csr/<domain>', methods=['POST'])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def ssl_get_csr(domain):
|
def ssl_get_csr(domain):
|
||||||
from ssl_certificates import create_csr
|
from ssl_certificates import create_csr
|
||||||
ssl_private_key = os.path.join(
|
ssl_private_key = os.path.join(
|
||||||
|
@ -567,7 +571,7 @@ def ssl_get_csr(domain):
|
||||||
|
|
||||||
|
|
||||||
@app.route('/ssl/install', methods=['POST'])
|
@app.route('/ssl/install', methods=['POST'])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def ssl_install_cert():
|
def ssl_install_cert():
|
||||||
from web_update import get_web_domains
|
from web_update import get_web_domains
|
||||||
from ssl_certificates import install_cert
|
from ssl_certificates import install_cert
|
||||||
|
@ -580,7 +584,7 @@ def ssl_install_cert():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/ssl/provision', methods=['POST'])
|
@app.route('/ssl/provision', methods=['POST'])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def ssl_provision_certs():
|
def ssl_provision_certs():
|
||||||
from ssl_certificates import provision_certificates
|
from ssl_certificates import provision_certificates
|
||||||
requests = provision_certificates(env, limit_domains=None)
|
requests = provision_certificates(env, limit_domains=None)
|
||||||
|
@ -591,7 +595,7 @@ def ssl_provision_certs():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/mfa/status', methods=['POST'])
|
@app.route('/mfa/status', methods=['POST'])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only(admin = False)
|
||||||
def mfa_get_status():
|
def mfa_get_status():
|
||||||
# Anyone accessing this route is an admin, and we permit them to
|
# Anyone accessing this route is an admin, and we permit them to
|
||||||
# see the MFA status for any user if they submit a 'user' form
|
# see the MFA status for any user if they submit a 'user' form
|
||||||
|
@ -599,6 +603,9 @@ def mfa_get_status():
|
||||||
# only provision for themselves.
|
# only provision for themselves.
|
||||||
# user field if given, otherwise the user making the request
|
# user field if given, otherwise the user making the request
|
||||||
email = request.form.get('user', request.user_email)
|
email = request.form.get('user', request.user_email)
|
||||||
|
if "admin" not in request.user_privs and email != request.user_email:
|
||||||
|
return ("You are not an administrator; you can only view your own MFA status!", 403)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
resp = {"enabled_mfa": get_public_mfa_state(email, env)}
|
resp = {"enabled_mfa": get_public_mfa_state(email, env)}
|
||||||
if email == request.user_email:
|
if email == request.user_email:
|
||||||
|
@ -609,7 +616,7 @@ def mfa_get_status():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/mfa/totp/enable', methods=['POST'])
|
@app.route('/mfa/totp/enable', methods=['POST'])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only(admin = False)
|
||||||
def totp_post_enable():
|
def totp_post_enable():
|
||||||
secret = request.form.get('secret')
|
secret = request.form.get('secret')
|
||||||
token = request.form.get('token')
|
token = request.form.get('token')
|
||||||
|
@ -625,13 +632,16 @@ def totp_post_enable():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/mfa/disable', methods=['POST'])
|
@app.route('/mfa/disable', methods=['POST'])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only(admin = False)
|
||||||
def totp_post_disable():
|
def totp_post_disable():
|
||||||
# Anyone accessing this route is an admin, and we permit them to
|
# Anyone accessing this route is an admin, and we permit them to
|
||||||
# disable the MFA status for any user if they submit a 'user' form
|
# disable the MFA status for any user if they submit a 'user' form
|
||||||
# field.
|
# field.
|
||||||
# user field if given, otherwise the user making the request
|
# user field if given, otherwise the user making the request
|
||||||
email = request.form.get('user', request.user_email)
|
email = request.form.get('user', request.user_email)
|
||||||
|
if "admin" not in request.user_privs and email != request.user_email:
|
||||||
|
return ("You are not an administrator; you can only view your own MFA status!", 403)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = disable_mfa(email,
|
result = disable_mfa(email,
|
||||||
request.form.get('mfa-id') or None,
|
request.form.get('mfa-id') or None,
|
||||||
|
@ -648,14 +658,14 @@ def totp_post_disable():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/web/domains')
|
@app.route('/web/domains')
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def web_get_domains():
|
def web_get_domains():
|
||||||
from web_update import get_web_domains_info
|
from web_update import get_web_domains_info
|
||||||
return json_response(get_web_domains_info(env))
|
return json_response(get_web_domains_info(env))
|
||||||
|
|
||||||
|
|
||||||
@app.route('/web/update', methods=['POST'])
|
@app.route('/web/update', methods=['POST'])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def web_update():
|
def web_update():
|
||||||
from web_update import do_web_update
|
from web_update import do_web_update
|
||||||
try:
|
try:
|
||||||
|
@ -668,7 +678,7 @@ def web_update():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/system/version', methods=["GET"])
|
@app.route('/system/version', methods=["GET"])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def system_version():
|
def system_version():
|
||||||
from status_checks import what_version_is_this
|
from status_checks import what_version_is_this
|
||||||
try:
|
try:
|
||||||
|
@ -678,7 +688,7 @@ def system_version():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/system/latest-upstream-version', methods=["POST"])
|
@app.route('/system/latest-upstream-version', methods=["POST"])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def system_latest_upstream_version():
|
def system_latest_upstream_version():
|
||||||
from status_checks import get_latest_miab_version
|
from status_checks import get_latest_miab_version
|
||||||
try:
|
try:
|
||||||
|
@ -688,7 +698,7 @@ def system_latest_upstream_version():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/system/status', methods=["POST"])
|
@app.route('/system/status', methods=["POST"])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def system_status():
|
def system_status():
|
||||||
from status_checks import run_checks
|
from status_checks import run_checks
|
||||||
|
|
||||||
|
@ -736,7 +746,7 @@ def system_status():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/system/updates')
|
@app.route('/system/updates')
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def show_updates():
|
def show_updates():
|
||||||
from status_checks import list_apt_updates
|
from status_checks import list_apt_updates
|
||||||
return "".join("%s (%s)\n" % (p["package"], p["version"])
|
return "".join("%s (%s)\n" % (p["package"], p["version"])
|
||||||
|
@ -744,7 +754,7 @@ def show_updates():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/system/update-packages', methods=["POST"])
|
@app.route('/system/update-packages', methods=["POST"])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def do_updates():
|
def do_updates():
|
||||||
utils.shell("check_call", ["/usr/bin/apt-get", "-qq", "update"])
|
utils.shell("check_call", ["/usr/bin/apt-get", "-qq", "update"])
|
||||||
return utils.shell("check_output", ["/usr/bin/apt-get", "-y", "upgrade"],
|
return utils.shell("check_output", ["/usr/bin/apt-get", "-y", "upgrade"],
|
||||||
|
@ -752,7 +762,7 @@ def do_updates():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/system/reboot', methods=["GET"])
|
@app.route('/system/reboot', methods=["GET"])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def needs_reboot():
|
def needs_reboot():
|
||||||
from status_checks import is_reboot_needed_due_to_package_installation
|
from status_checks import is_reboot_needed_due_to_package_installation
|
||||||
if is_reboot_needed_due_to_package_installation():
|
if is_reboot_needed_due_to_package_installation():
|
||||||
|
@ -762,7 +772,7 @@ def needs_reboot():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/system/reboot', methods=["POST"])
|
@app.route('/system/reboot', methods=["POST"])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def do_reboot():
|
def do_reboot():
|
||||||
# To keep the attack surface low, we don't allow a remote reboot if one isn't necessary.
|
# To keep the attack surface low, we don't allow a remote reboot if one isn't necessary.
|
||||||
from status_checks import is_reboot_needed_due_to_package_installation
|
from status_checks import is_reboot_needed_due_to_package_installation
|
||||||
|
@ -774,7 +784,7 @@ def do_reboot():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/system/backup/status')
|
@app.route('/system/backup/status')
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def backup_status():
|
def backup_status():
|
||||||
from backup import backup_status
|
from backup import backup_status
|
||||||
try:
|
try:
|
||||||
|
@ -784,14 +794,14 @@ def backup_status():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/system/backup/config', methods=["GET"])
|
@app.route('/system/backup/config', methods=["GET"])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def backup_get_custom():
|
def backup_get_custom():
|
||||||
from backup import get_backup_config
|
from backup import get_backup_config
|
||||||
return json_response(get_backup_config(env, for_ui=True))
|
return json_response(get_backup_config(env, for_ui=True))
|
||||||
|
|
||||||
|
|
||||||
@app.route('/system/backup/config', methods=["POST"])
|
@app.route('/system/backup/config', methods=["POST"])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def backup_set_custom():
|
def backup_set_custom():
|
||||||
from backup import backup_set_custom
|
from backup import backup_set_custom
|
||||||
return json_response(
|
return json_response(
|
||||||
|
@ -803,7 +813,7 @@ def backup_set_custom():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/system/backup/new', methods=["POST"])
|
@app.route('/system/backup/new', methods=["POST"])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def backup_new():
|
def backup_new():
|
||||||
from backup import perform_backup, get_backup_config
|
from backup import perform_backup, get_backup_config
|
||||||
|
|
||||||
|
@ -817,14 +827,14 @@ def backup_new():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/system/privacy', methods=["GET"])
|
@app.route('/system/privacy', methods=["GET"])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def privacy_status_get():
|
def privacy_status_get():
|
||||||
config = utils.load_settings(env)
|
config = utils.load_settings(env)
|
||||||
return json_response(config.get("privacy", True))
|
return json_response(config.get("privacy", True))
|
||||||
|
|
||||||
|
|
||||||
@app.route('/system/privacy', methods=["POST"])
|
@app.route('/system/privacy', methods=["POST"])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def privacy_status_set():
|
def privacy_status_set():
|
||||||
config = utils.load_settings(env)
|
config = utils.load_settings(env)
|
||||||
config["privacy"] = (request.form.get('value') == "private")
|
config["privacy"] = (request.form.get('value') == "private")
|
||||||
|
@ -833,7 +843,7 @@ def privacy_status_set():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/system/smtp/relay', methods=["GET"])
|
@app.route('/system/smtp/relay', methods=["GET"])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def smtp_relay_get():
|
def smtp_relay_get():
|
||||||
config = utils.load_settings(env)
|
config = utils.load_settings(env)
|
||||||
|
|
||||||
|
@ -864,7 +874,7 @@ def smtp_relay_get():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/system/smtp/relay', methods=["POST"])
|
@app.route('/system/smtp/relay', methods=["POST"])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def smtp_relay_set():
|
def smtp_relay_set():
|
||||||
from editconf import edit_conf
|
from editconf import edit_conf
|
||||||
from os import chmod
|
from os import chmod
|
||||||
|
@ -995,7 +1005,7 @@ def smtp_relay_set():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/system/pgp/', methods=["GET"])
|
@app.route('/system/pgp/', methods=["GET"])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def get_keys():
|
def get_keys():
|
||||||
from pgp import get_daemon_key, get_imported_keys, key_representation
|
from pgp import get_daemon_key, get_imported_keys, key_representation
|
||||||
return {
|
return {
|
||||||
|
@ -1005,7 +1015,7 @@ def get_keys():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/system/pgp/<fpr>', methods=["GET"])
|
@app.route('/system/pgp/<fpr>', methods=["GET"])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def get_key(fpr):
|
def get_key(fpr):
|
||||||
from pgp import get_key, key_representation
|
from pgp import get_key, key_representation
|
||||||
k = get_key(fpr)
|
k = get_key(fpr)
|
||||||
|
@ -1015,7 +1025,7 @@ def get_key(fpr):
|
||||||
|
|
||||||
|
|
||||||
@app.route('/system/pgp/<fpr>', methods=["DELETE"])
|
@app.route('/system/pgp/<fpr>', methods=["DELETE"])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def delete_key(fpr):
|
def delete_key(fpr):
|
||||||
from pgp import delete_key
|
from pgp import delete_key
|
||||||
from wkd import parse_wkd_list, build_wkd
|
from wkd import parse_wkd_list, build_wkd
|
||||||
|
@ -1030,7 +1040,7 @@ def delete_key(fpr):
|
||||||
|
|
||||||
|
|
||||||
@app.route('/system/pgp/<fpr>/export', methods=["GET"])
|
@app.route('/system/pgp/<fpr>/export', methods=["GET"])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def export_key(fpr):
|
def export_key(fpr):
|
||||||
from pgp import export_key
|
from pgp import export_key
|
||||||
exp = export_key(fpr)
|
exp = export_key(fpr)
|
||||||
|
@ -1040,7 +1050,7 @@ def export_key(fpr):
|
||||||
|
|
||||||
|
|
||||||
@app.route('/system/pgp/import', methods=["POST"])
|
@app.route('/system/pgp/import', methods=["POST"])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def import_key():
|
def import_key():
|
||||||
from pgp import import_key
|
from pgp import import_key
|
||||||
from wkd import build_wkd
|
from wkd import build_wkd
|
||||||
|
@ -1065,7 +1075,7 @@ def import_key():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/system/pgp/wkd', methods=["GET"])
|
@app.route('/system/pgp/wkd', methods=["GET"])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def get_wkd_status():
|
def get_wkd_status():
|
||||||
from pgp import get_daemon_key, get_imported_keys, key_representation
|
from pgp import get_daemon_key, get_imported_keys, key_representation
|
||||||
from wkd import get_user_fpr_maps, get_wkd_config
|
from wkd import get_user_fpr_maps, get_wkd_config
|
||||||
|
@ -1099,7 +1109,7 @@ def get_wkd_status():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/system/pgp/wkd', methods=["POST"])
|
@app.route('/system/pgp/wkd', methods=["POST"])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def update_wkd():
|
def update_wkd():
|
||||||
from wkd import update_wkd_config, build_wkd
|
from wkd import update_wkd_config, build_wkd
|
||||||
update_wkd_config(request.form)
|
update_wkd_config(request.form)
|
||||||
|
@ -1108,7 +1118,7 @@ def update_wkd():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/system/default-quota', methods=["GET"])
|
@app.route('/system/default-quota', methods=["GET"])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def default_quota_get():
|
def default_quota_get():
|
||||||
if request.values.get('text'):
|
if request.values.get('text'):
|
||||||
return get_default_quota(env)
|
return get_default_quota(env)
|
||||||
|
@ -1119,7 +1129,7 @@ def default_quota_get():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/system/default-quota', methods=["POST"])
|
@app.route('/system/default-quota', methods=["POST"])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def default_quota_set():
|
def default_quota_set():
|
||||||
config = utils.load_settings(env)
|
config = utils.load_settings(env)
|
||||||
try:
|
try:
|
||||||
|
@ -1137,7 +1147,7 @@ def default_quota_set():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/munin/')
|
@app.route('/munin/')
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only()
|
||||||
def munin_start():
|
def munin_start():
|
||||||
# Munin pages, static images, and dynamically generated images are served
|
# Munin pages, static images, and dynamically generated images are served
|
||||||
# outside of the AJAX API. We'll start with a 'start' API that sets a cookie
|
# outside of the AJAX API. We'll start with a 'start' API that sets a cookie
|
||||||
|
|
|
@ -135,6 +135,13 @@
|
||||||
Monitoring</a></li>
|
Monitoring</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item me-1 me-xl-4 dropdown if-logged-in-not-admin">
|
||||||
|
<button class="btn dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">Your Account</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="#manage-password" onclick="return show_panel(this);">Manage Password</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#mfa" onclick="return show_panel(this);">Two-Factor Authentication</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
<li class="nav-item me-1 me-xl-4 btn if-logged-in-not-admin" type="button" href="#mail-guide"
|
<li class="nav-item me-1 me-xl-4 btn if-logged-in-not-admin" type="button" href="#mail-guide"
|
||||||
onclick="return show_panel(this);">
|
onclick="return show_panel(this);">
|
||||||
Mail Guide
|
Mail Guide
|
||||||
|
@ -198,6 +205,10 @@
|
||||||
{% include "wkd.html" %}
|
{% include "wkd.html" %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="panel_manage-password" class="admin_panel">
|
||||||
|
{% include "manage-password.html" %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="panel_mfa" class="admin_panel">
|
<div id="panel_mfa" class="admin_panel">
|
||||||
{% include "mfa.html" %}
|
{% include "mfa.html" %}
|
||||||
</div>
|
</div>
|
||||||
|
|
57
management/templates/manage-password.html
Normal file
57
management/templates/manage-password.html
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
<div>
|
||||||
|
<h2>Manage Password</h2>
|
||||||
|
<p>Here you can change your account password. The new password is then valid for both this panel and your email.</p>
|
||||||
|
<p>If you have client emails configured, you'll then need to update the configuration with the new password. See the <a href="#mail-guide" onclick="return show_panel(this);">Mail Guide</a> for more information about this.</p>
|
||||||
|
|
||||||
|
<form class="form-horizontal" role="form" onsubmit="set_password_self(); return false;">
|
||||||
|
<div class="col-lg-10 col-xl-8 mb-3">
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="manage-password-new" class="input-group-text col-3">New Password</label>
|
||||||
|
<input type="password" placeholder="password" class="form-control" id="manage-password-new">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-10 col-xl-8 mb-3">
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="manage-password-confirm" class="input-group-text col-3">Confirm Password</label>
|
||||||
|
<input type="password" placeholder="password" class="form-control" id="manage-password-confirm">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<button id="manage-password-submit" type="submit" class="btn btn-primary">Save</button>
|
||||||
|
</div>
|
||||||
|
<small>After changing your password, you'll be logged out from the account and will need to log in again.</small>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function set_password_self() {
|
||||||
|
if ($('#manage-password-new').val() !== $('#manage-password-confirm').val()) {
|
||||||
|
show_modal_error("Set Password", 'Passwords do not match!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let password = $('#manage-password-new').val()
|
||||||
|
|
||||||
|
api(
|
||||||
|
"/mail/users/password",
|
||||||
|
"POST",
|
||||||
|
{
|
||||||
|
email: api_credentials.username,
|
||||||
|
password: password
|
||||||
|
},
|
||||||
|
function (r) {
|
||||||
|
// Responses are multiple lines of pre-formatted text.
|
||||||
|
show_modal_error("Set Password", $("<pre/>").text(r), () => {
|
||||||
|
do_logout()
|
||||||
|
$('#manage-password-new').val("")
|
||||||
|
$('#manage-password-confirm').val("")
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function (r) {
|
||||||
|
show_modal_error("Set Password", r);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -212,12 +212,11 @@ cp ${RCM_PLUGIN_DIR}/password/config.inc.php.dist \
|
||||||
${RCM_PLUGIN_DIR}/password/config.inc.php
|
${RCM_PLUGIN_DIR}/password/config.inc.php
|
||||||
|
|
||||||
management/editconf.py ${RCM_PLUGIN_DIR}/password/config.inc.php -c "//" \
|
management/editconf.py ${RCM_PLUGIN_DIR}/password/config.inc.php -c "//" \
|
||||||
|
"\$config['password_driver'] = 'miab';" \
|
||||||
"\$config['password_minimum_length'] = 8;" \
|
"\$config['password_minimum_length'] = 8;" \
|
||||||
"\$config['password_db_dsn'] = 'sqlite:///$STORAGE_ROOT/mail/users.sqlite';" \
|
"\$config['password_miab_url'] = 'http://127.0.0.1:10222/';" \
|
||||||
"\$config['password_query'] = 'UPDATE users SET password=%P WHERE email=%u';" \
|
"\$config['password_miab_user'] = '';" \
|
||||||
"\$config['password_algorithm'] = 'sha512-crypt';" \
|
"\$config['password_miab_pass'] = '';"
|
||||||
"\$config['password_algorithm_prefix'] = '{SHA512-CRYPT}';" \
|
|
||||||
"\$config['password_dovecotpw_with_method'] = false;"
|
|
||||||
|
|
||||||
# so PHP can use doveadm, for the password changing plugin
|
# so PHP can use doveadm, for the password changing plugin
|
||||||
usermod -a -G dovecot www-data
|
usermod -a -G dovecot www-data
|
||||||
|
|
Loading…
Reference in a new issue