mirror of
				https://github.com/mail-in-a-box/mailinabox.git
				synced 2025-11-03 19:30:54 +00:00 
			
		
		
		
	Merge remote-tracking branch 'fspoettel/admin-panel-2fa' into totp
# Conflicts: # management/daemon.py # management/mfa.py
This commit is contained in:
		
						commit
						a7370beae0
					
				@ -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 subimtted. 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
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										150
									
								
								management/cli.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										150
									
								
								management/cli.py
									
									
									
									
									
										Executable file
									
								
							@ -0,0 +1,150 @@
 | 
				
			|||||||
 | 
					#!/usr/bin/python3
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# This is a command-line script for calling management APIs
 | 
				
			||||||
 | 
					# on the Mail-in-a-Box control panel backend. The script
 | 
				
			||||||
 | 
					# reads /var/lib/mailinabox/api.key for the backend's
 | 
				
			||||||
 | 
					# root API key. This file is readable only by root, so this
 | 
				
			||||||
 | 
					# tool can only be used as root.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import sys, getpass, urllib.request, urllib.error, json, re, csv
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def mgmt(cmd, data=None, is_json=False):
 | 
				
			||||||
 | 
						# The base URL for the management daemon. (Listens on IPv4 only.)
 | 
				
			||||||
 | 
						mgmt_uri = 'http://127.0.0.1:10222'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						setup_key_auth(mgmt_uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						req = urllib.request.Request(mgmt_uri + cmd, urllib.parse.urlencode(data).encode("utf8") if data else None)
 | 
				
			||||||
 | 
						try:
 | 
				
			||||||
 | 
							response = urllib.request.urlopen(req)
 | 
				
			||||||
 | 
						except urllib.error.HTTPError as e:
 | 
				
			||||||
 | 
							if e.code == 401:
 | 
				
			||||||
 | 
								try:
 | 
				
			||||||
 | 
									print(e.read().decode("utf8"))
 | 
				
			||||||
 | 
								except:
 | 
				
			||||||
 | 
									pass
 | 
				
			||||||
 | 
								print("The management daemon refused access. The API key file may be out of sync. Try 'service mailinabox restart'.", file=sys.stderr)
 | 
				
			||||||
 | 
							elif hasattr(e, 'read'):
 | 
				
			||||||
 | 
								print(e.read().decode('utf8'), file=sys.stderr)
 | 
				
			||||||
 | 
							else:
 | 
				
			||||||
 | 
								print(e, file=sys.stderr)
 | 
				
			||||||
 | 
							sys.exit(1)
 | 
				
			||||||
 | 
						resp = response.read().decode('utf8')
 | 
				
			||||||
 | 
						if is_json: resp = json.loads(resp)
 | 
				
			||||||
 | 
						return resp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def read_password():
 | 
				
			||||||
 | 
					    while True:
 | 
				
			||||||
 | 
					        first = getpass.getpass('password: ')
 | 
				
			||||||
 | 
					        if len(first) < 8:
 | 
				
			||||||
 | 
					            print("Passwords must be at least eight characters.")
 | 
				
			||||||
 | 
					            continue
 | 
				
			||||||
 | 
					        second = getpass.getpass(' (again): ')
 | 
				
			||||||
 | 
					        if first != second:
 | 
				
			||||||
 | 
					            print("Passwords not the same. Try again.")
 | 
				
			||||||
 | 
					            continue
 | 
				
			||||||
 | 
					        break
 | 
				
			||||||
 | 
					    return first
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def setup_key_auth(mgmt_uri):
 | 
				
			||||||
 | 
						key = open('/var/lib/mailinabox/api.key').read().strip()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						auth_handler = urllib.request.HTTPBasicAuthHandler()
 | 
				
			||||||
 | 
						auth_handler.add_password(
 | 
				
			||||||
 | 
							realm='Mail-in-a-Box Management Server',
 | 
				
			||||||
 | 
							uri=mgmt_uri,
 | 
				
			||||||
 | 
							user=key,
 | 
				
			||||||
 | 
							passwd='')
 | 
				
			||||||
 | 
						opener = urllib.request.build_opener(auth_handler)
 | 
				
			||||||
 | 
						urllib.request.install_opener(opener)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if len(sys.argv) < 2:
 | 
				
			||||||
 | 
						print("""Usage:
 | 
				
			||||||
 | 
					  {cli} user                                     (lists users)
 | 
				
			||||||
 | 
					  {cli} user add user@domain.com [password]
 | 
				
			||||||
 | 
					  {cli} user password user@domain.com [password]
 | 
				
			||||||
 | 
					  {cli} user remove user@domain.com
 | 
				
			||||||
 | 
					  {cli} user make-admin user@domain.com
 | 
				
			||||||
 | 
					  {cli} user remove-admin user@domain.com
 | 
				
			||||||
 | 
					  {cli} user admins                              (lists admins)
 | 
				
			||||||
 | 
					  {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, multiple.people@other.domain.com'
 | 
				
			||||||
 | 
					  {cli} alias remove incoming.name@domain.com
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Removing a mail user does not delete their mail folders on disk. It only prevents IMAP/SMTP login.
 | 
				
			||||||
 | 
					""".format(
 | 
				
			||||||
 | 
						cli="management/cli.py"
 | 
				
			||||||
 | 
							))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					elif sys.argv[1] == "user" and len(sys.argv) == 2:
 | 
				
			||||||
 | 
						# Dump a list of users, one per line. Mark admins with an asterisk.
 | 
				
			||||||
 | 
						users = mgmt("/mail/users?format=json", is_json=True)
 | 
				
			||||||
 | 
						for domain in users:
 | 
				
			||||||
 | 
							for user in domain["users"]:
 | 
				
			||||||
 | 
								if user['status'] == 'inactive': continue
 | 
				
			||||||
 | 
								print(user['email'], end='')
 | 
				
			||||||
 | 
								if "admin" in user['privileges']:
 | 
				
			||||||
 | 
									print("*", end='')
 | 
				
			||||||
 | 
								print()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					elif sys.argv[1] == "user" and sys.argv[2] in ("add", "password"):
 | 
				
			||||||
 | 
						if len(sys.argv) < 5:
 | 
				
			||||||
 | 
							if len(sys.argv) < 4:
 | 
				
			||||||
 | 
								email = input("email: ")
 | 
				
			||||||
 | 
							else:
 | 
				
			||||||
 | 
								email = sys.argv[3]
 | 
				
			||||||
 | 
							pw = read_password()
 | 
				
			||||||
 | 
						else:
 | 
				
			||||||
 | 
							email, pw = sys.argv[3:5]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if sys.argv[2] == "add":
 | 
				
			||||||
 | 
							print(mgmt("/mail/users/add", { "email": email, "password": pw }))
 | 
				
			||||||
 | 
						elif sys.argv[2] == "password":
 | 
				
			||||||
 | 
							print(mgmt("/mail/users/password", { "email": email, "password": pw }))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					elif sys.argv[1] == "user" and sys.argv[2] == "remove" and len(sys.argv) == 4:
 | 
				
			||||||
 | 
						print(mgmt("/mail/users/remove", { "email": sys.argv[3] }))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					elif sys.argv[1] == "user" and sys.argv[2] in ("make-admin", "remove-admin") and len(sys.argv) == 4:
 | 
				
			||||||
 | 
						if sys.argv[2] == "make-admin":
 | 
				
			||||||
 | 
							action = "add"
 | 
				
			||||||
 | 
						else:
 | 
				
			||||||
 | 
							action = "remove"
 | 
				
			||||||
 | 
						print(mgmt("/mail/users/privileges/" + action, { "email": sys.argv[3], "privilege": "admin" }))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					elif sys.argv[1] == "user" and sys.argv[2] == "admins":
 | 
				
			||||||
 | 
						# Dump a list of admin users.
 | 
				
			||||||
 | 
						users = mgmt("/mail/users?format=json", is_json=True)
 | 
				
			||||||
 | 
						for domain in users:
 | 
				
			||||||
 | 
							for user in domain["users"]:
 | 
				
			||||||
 | 
								if "admin" in user['privileges']:
 | 
				
			||||||
 | 
									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:
 | 
				
			||||||
 | 
						print(mgmt("/mail/aliases"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					elif sys.argv[1] == "alias" and sys.argv[2] == "add" and len(sys.argv) == 5:
 | 
				
			||||||
 | 
						print(mgmt("/mail/aliases/add", { "address": sys.argv[3], "forwards_to": sys.argv[4] }))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					elif sys.argv[1] == "alias" and sys.argv[2] == "remove" and len(sys.argv) == 4:
 | 
				
			||||||
 | 
						print(mgmt("/mail/aliases/remove", { "address": sys.argv[3] }))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					else:
 | 
				
			||||||
 | 
						print("Invalid command-line arguments.")
 | 
				
			||||||
 | 
						sys.exit(1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -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, set_mail_display_name, remove_mail_user
 | 
					from mailconfig import get_mail_users, get_mail_users_ex, get_admins, add_mail_user, set_mail_password, set_mail_display_name, 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
 | 
				
			||||||
@ -409,15 +409,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
 | 
				
			||||||
 | 
						# field. But we don't include provisioning info since a user can
 | 
				
			||||||
 | 
						# 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": {
 | 
									"new_mfa": {
 | 
				
			||||||
			"totp": mfa_totp.provision(request.user_email, env)
 | 
										"totp": mfa_totp.provision(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
 | 
				
			||||||
@ -437,8 +449,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
 | 
				
			||||||
 | 
						# 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"
 | 
							return "OK"
 | 
				
			||||||
 | 
						else: # error
 | 
				
			||||||
 | 
							return ("Invalid user or MFA id.", 400)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# WEB
 | 
					# WEB
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -106,11 +106,11 @@ def disable_mfa(email, mfa_id, env):
 | 
				
			|||||||
	user = get_mfa_user(email, env)
 | 
						user = get_mfa_user(email, env)
 | 
				
			||||||
	if mfa_id is None:
 | 
						if mfa_id is None:
 | 
				
			||||||
		# Disable all MFA for a user.
 | 
							# Disable all MFA for a user.
 | 
				
			||||||
		mfa_totp.disable(user, None, env)
 | 
							return mfa_totp.disable(user, None, env)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	elif mfa_id.startswith("totp:"):
 | 
						elif mfa_id.startswith("totp:"):
 | 
				
			||||||
		# Disable a particular MFA mode for a user.
 | 
							# Disable a particular MFA mode for a user.
 | 
				
			||||||
		mfa_totp.disable(user, mfa_id, env)
 | 
							return mfa_totp.disable(user, mfa_id, env)
 | 
				
			||||||
			
 | 
								
 | 
				
			||||||
def validate_auth_mfa(email, request, env):
 | 
					def validate_auth_mfa(email, request, env):
 | 
				
			||||||
	# Validates that a login request satisfies any MFA modes
 | 
						# Validates that a login request satisfies any MFA modes
 | 
				
			||||||
 | 
				
			|||||||
@ -100,12 +100,13 @@ def disable(user, id, env):
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
		mods["objectClass"].remove("totpUser")	
 | 
							mods["objectClass"].remove("totpUser")	
 | 
				
			||||||
		open_database(env).modify_record(user, mods)
 | 
							open_database(env).modify_record(user, mods)
 | 
				
			||||||
 | 
							return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	else:
 | 
						else:
 | 
				
			||||||
		# Disable totp at the index specified
 | 
							# Disable totp at the index specified
 | 
				
			||||||
		idx = index_from_id(user, id)	
 | 
							idx = index_from_id(user, id)	
 | 
				
			||||||
		if idx<0 or idx>=len(user['totpSecret']):
 | 
							if idx<0 or idx>=len(user['totpSecret']):
 | 
				
			||||||
			raise ValueError('MFA/totp mru index is out of range')
 | 
								return False
 | 
				
			||||||
		mods = {
 | 
							mods = {
 | 
				
			||||||
			"objectClass": user["objectClass"].copy(),
 | 
								"objectClass": user["objectClass"].copy(),
 | 
				
			||||||
			"totpMruToken": user["totpMruToken"].copy(),
 | 
								"totpMruToken": user["totpMruToken"].copy(),
 | 
				
			||||||
@ -120,6 +121,7 @@ def disable(user, id, env):
 | 
				
			|||||||
		if len(mods["totpSecret"])==0:
 | 
							if len(mods["totpSecret"])==0:
 | 
				
			||||||
			mods['objectClass'].remove('totpUser')
 | 
								mods['objectClass'].remove('totpUser')
 | 
				
			||||||
		open_database(env).modify_record(user, mods)
 | 
							open_database(env).modify_record(user, mods)
 | 
				
			||||||
 | 
							return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def validate_secret(secret):
 | 
					def validate_secret(secret):
 | 
				
			||||||
 | 
				
			|||||||
@ -32,13 +32,13 @@
 | 
				
			|||||||
  <p class="text-danger">There are no users on this system! To make an administrative user,
 | 
					  <p class="text-danger">There are no users on this system! To make an administrative user,
 | 
				
			||||||
  log into this machine using SSH (like when you first set it up) and run:</p>
 | 
					  log into this machine using SSH (like when you first set it up) and run:</p>
 | 
				
			||||||
  <pre>cd mailinabox
 | 
					  <pre>cd mailinabox
 | 
				
			||||||
sudo tools/mail.py user add me@{{hostname}}
 | 
					sudo management/cli.py user add me@{{hostname}}
 | 
				
			||||||
sudo tools/mail.py user make-admin me@{{hostname}}</pre>
 | 
					sudo management/cli.py user make-admin me@{{hostname}}</pre>
 | 
				
			||||||
  {% else %}
 | 
					  {% else %}
 | 
				
			||||||
  <p class="text-danger">There are no administrative users on this system! To make an administrative user,
 | 
					  <p class="text-danger">There are no administrative users on this system! To make an administrative user,
 | 
				
			||||||
  log into this machine using SSH (like when you first set it up) and run:</p>
 | 
					  log into this machine using SSH (like when you first set it up) and run:</p>
 | 
				
			||||||
  <pre>cd mailinabox
 | 
					  <pre>cd mailinabox
 | 
				
			||||||
sudo tools/mail.py user make-admin me@{{hostname}}</pre>
 | 
					sudo management/cli.py user make-admin me@{{hostname}}</pre>
 | 
				
			||||||
  {% endif %}
 | 
					  {% endif %}
 | 
				
			||||||
  <hr>
 | 
					  <hr>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
				
			|||||||
@ -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');
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
# If there aren't any mail users yet, create one.
 | 
					# If there aren't any mail users yet, create one.
 | 
				
			||||||
if [ -z "`tools/mail.py user`" ]; then
 | 
					if [ -z "`management/cli.py user`" ]; then
 | 
				
			||||||
	# The outut of "tools/mail.py user" is a list of mail users. If there
 | 
						# The outut of "management/cli.py user" is a list of mail users. If there
 | 
				
			||||||
	# aren't any yet, it'll be empty.
 | 
						# aren't any yet, it'll be empty.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	# If we didn't ask for an email address at the start, do so now.
 | 
						# If we didn't ask for an email address at the start, do so now.
 | 
				
			||||||
@ -47,11 +47,11 @@ if [ -z "`tools/mail.py user`" ]; then
 | 
				
			|||||||
	fi
 | 
						fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	# Create the user's mail account. This will ask for a password if none was given above.
 | 
						# Create the user's mail account. This will ask for a password if none was given above.
 | 
				
			||||||
	tools/mail.py user add $EMAIL_ADDR ${EMAIL_PW:-}
 | 
						management/cli.py user add $EMAIL_ADDR ${EMAIL_PW:-}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	# Make it an admin.
 | 
						# Make it an admin.
 | 
				
			||||||
	hide_output tools/mail.py user make-admin $EMAIL_ADDR
 | 
						hide_output management/cli.py user make-admin $EMAIL_ADDR
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	# Create an alias to which we'll direct all automatically-created administrative aliases.
 | 
						# Create an alias to which we'll direct all automatically-created administrative aliases.
 | 
				
			||||||
	tools/mail.py alias add administrator@$PRIMARY_HOSTNAME $EMAIL_ADDR > /dev/null
 | 
						management/cli.py alias add administrator@$PRIMARY_HOSTNAME $EMAIL_ADDR > /dev/null
 | 
				
			||||||
fi
 | 
					fi
 | 
				
			||||||
 | 
				
			|||||||
@ -329,7 +329,7 @@ rm -f /etc/cron.hourly/mailinabox-owncloud
 | 
				
			|||||||
# and there's a lot they could mess up, so we don't make any users admins of Nextcloud.
 | 
					# and there's a lot they could mess up, so we don't make any users admins of Nextcloud.
 | 
				
			||||||
# But if we wanted to, we would do this:
 | 
					# But if we wanted to, we would do this:
 | 
				
			||||||
# ```
 | 
					# ```
 | 
				
			||||||
# for user in $(tools/mail.py user admins); do
 | 
					# for user in $(management/cli.py user admins); do
 | 
				
			||||||
#	 sqlite3 $STORAGE_ROOT/owncloud/owncloud.db "INSERT OR IGNORE INTO oc_group_user VALUES ('admin', '$user')"
 | 
					#	 sqlite3 $STORAGE_ROOT/owncloud/owncloud.db "INSERT OR IGNORE INTO oc_group_user VALUES ('admin', '$user')"
 | 
				
			||||||
# done
 | 
					# done
 | 
				
			||||||
# ```
 | 
					# ```
 | 
				
			||||||
 | 
				
			|||||||
@ -222,8 +222,8 @@ mgmt_mfa_status() {
 | 
				
			|||||||
	local user="$1"
 | 
						local user="$1"
 | 
				
			||||||
	local pw="$2"
 | 
						local pw="$2"
 | 
				
			||||||
	record "[Get MFA status]"
 | 
						record "[Get MFA status]"
 | 
				
			||||||
	if ! mgmt_rest_as_user "GET" "/admin/mfa/status" "$user" "$pw"; then
 | 
						if ! mgmt_rest_as_user "POST" "/admin/mfa/status" "$user" "$pw"; then
 | 
				
			||||||
		REST_ERROR="Failed: GET /admin/mfa/status: $REST_ERROR"
 | 
							REST_ERROR="Failed: POST /admin/mfa/status: $REST_ERROR"
 | 
				
			||||||
		return 1
 | 
							return 1
 | 
				
			||||||
	fi
 | 
						fi
 | 
				
			||||||
	# json is in REST_OUTPUT...
 | 
						# json is in REST_OUTPUT...
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										131
									
								
								tools/mail.py
									
									
									
									
									
								
							
							
						
						
									
										131
									
								
								tools/mail.py
									
									
									
									
									
								
							@ -1,128 +1,3 @@
 | 
				
			|||||||
#!/usr/bin/python3
 | 
					#!/bin/bash
 | 
				
			||||||
 | 
					# This script has moved.
 | 
				
			||||||
import sys, getpass, urllib.request, urllib.error, json, re
 | 
					management/cli.py "$@"
 | 
				
			||||||
 | 
					 | 
				
			||||||
def mgmt(cmd, data=None, is_json=False):
 | 
					 | 
				
			||||||
	# The base URL for the management daemon. (Listens on IPv4 only.)
 | 
					 | 
				
			||||||
	mgmt_uri = 'http://127.0.0.1:10222'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	setup_key_auth(mgmt_uri)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	req = urllib.request.Request(mgmt_uri + cmd, urllib.parse.urlencode(data).encode("utf8") if data else None)
 | 
					 | 
				
			||||||
	try:
 | 
					 | 
				
			||||||
		response = urllib.request.urlopen(req)
 | 
					 | 
				
			||||||
	except urllib.error.HTTPError as e:
 | 
					 | 
				
			||||||
		if e.code == 401:
 | 
					 | 
				
			||||||
			try:
 | 
					 | 
				
			||||||
				print(e.read().decode("utf8"))
 | 
					 | 
				
			||||||
			except:
 | 
					 | 
				
			||||||
				pass
 | 
					 | 
				
			||||||
			print("The management daemon refused access. The API key file may be out of sync. Try 'service mailinabox restart'.", file=sys.stderr)
 | 
					 | 
				
			||||||
		elif hasattr(e, 'read'):
 | 
					 | 
				
			||||||
			print(e.read().decode('utf8'), file=sys.stderr)
 | 
					 | 
				
			||||||
		else:
 | 
					 | 
				
			||||||
			print(e, file=sys.stderr)
 | 
					 | 
				
			||||||
		sys.exit(1)
 | 
					 | 
				
			||||||
	resp = response.read().decode('utf8')
 | 
					 | 
				
			||||||
	if is_json: resp = json.loads(resp)
 | 
					 | 
				
			||||||
	return resp
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def read_password():
 | 
					 | 
				
			||||||
    while True:
 | 
					 | 
				
			||||||
        first = getpass.getpass('password: ')
 | 
					 | 
				
			||||||
        if len(first) < 8:
 | 
					 | 
				
			||||||
            print("Passwords must be at least eight characters.")
 | 
					 | 
				
			||||||
            continue
 | 
					 | 
				
			||||||
        second = getpass.getpass(' (again): ')
 | 
					 | 
				
			||||||
        if first != second:
 | 
					 | 
				
			||||||
            print("Passwords not the same. Try again.")
 | 
					 | 
				
			||||||
            continue
 | 
					 | 
				
			||||||
        break
 | 
					 | 
				
			||||||
    return first
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def setup_key_auth(mgmt_uri):
 | 
					 | 
				
			||||||
	key = open('/var/lib/mailinabox/api.key').read().strip()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	auth_handler = urllib.request.HTTPBasicAuthHandler()
 | 
					 | 
				
			||||||
	auth_handler.add_password(
 | 
					 | 
				
			||||||
		realm='Mail-in-a-Box Management Server',
 | 
					 | 
				
			||||||
		uri=mgmt_uri,
 | 
					 | 
				
			||||||
		user=key,
 | 
					 | 
				
			||||||
		passwd='')
 | 
					 | 
				
			||||||
	opener = urllib.request.build_opener(auth_handler)
 | 
					 | 
				
			||||||
	urllib.request.install_opener(opener)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
if len(sys.argv) < 2:
 | 
					 | 
				
			||||||
	print("Usage: ")
 | 
					 | 
				
			||||||
	print("  tools/mail.py user  (lists users)")
 | 
					 | 
				
			||||||
	print("  tools/mail.py user add user@domain.com [password]")
 | 
					 | 
				
			||||||
	print("  tools/mail.py user password user@domain.com [password]")
 | 
					 | 
				
			||||||
	print("  tools/mail.py user remove user@domain.com")
 | 
					 | 
				
			||||||
	print("  tools/mail.py user make-admin user@domain.com")
 | 
					 | 
				
			||||||
	print("  tools/mail.py user remove-admin user@domain.com")
 | 
					 | 
				
			||||||
	print("  tools/mail.py user admins (lists admins)")
 | 
					 | 
				
			||||||
	print("  tools/mail.py alias  (lists aliases)")
 | 
					 | 
				
			||||||
	print("  tools/mail.py alias add incoming.name@domain.com sent.to@other.domain.com")
 | 
					 | 
				
			||||||
	print("  tools/mail.py alias add incoming.name@domain.com 'sent.to@other.domain.com, multiple.people@other.domain.com'")
 | 
					 | 
				
			||||||
	print("  tools/mail.py alias remove incoming.name@domain.com")
 | 
					 | 
				
			||||||
	print()
 | 
					 | 
				
			||||||
	print("Removing a mail user does not delete their mail folders on disk. It only prevents IMAP/SMTP login.")
 | 
					 | 
				
			||||||
	print()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
elif sys.argv[1] == "user" and len(sys.argv) == 2:
 | 
					 | 
				
			||||||
	# Dump a list of users, one per line. Mark admins with an asterisk.
 | 
					 | 
				
			||||||
	users = mgmt("/mail/users?format=json", is_json=True)
 | 
					 | 
				
			||||||
	for domain in users:
 | 
					 | 
				
			||||||
		for user in domain["users"]:
 | 
					 | 
				
			||||||
			if user['status'] == 'inactive': continue
 | 
					 | 
				
			||||||
			print(user['email'], end='')
 | 
					 | 
				
			||||||
			if "admin" in user['privileges']:
 | 
					 | 
				
			||||||
				print("*", end='')
 | 
					 | 
				
			||||||
			print()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
elif sys.argv[1] == "user" and sys.argv[2] in ("add", "password"):
 | 
					 | 
				
			||||||
	if len(sys.argv) < 5:
 | 
					 | 
				
			||||||
		if len(sys.argv) < 4:
 | 
					 | 
				
			||||||
			email = input("email: ")
 | 
					 | 
				
			||||||
		else:
 | 
					 | 
				
			||||||
			email = sys.argv[3]
 | 
					 | 
				
			||||||
		pw = read_password()
 | 
					 | 
				
			||||||
	else:
 | 
					 | 
				
			||||||
		email, pw = sys.argv[3:5]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if sys.argv[2] == "add":
 | 
					 | 
				
			||||||
		print(mgmt("/mail/users/add", { "email": email, "password": pw }))
 | 
					 | 
				
			||||||
	elif sys.argv[2] == "password":
 | 
					 | 
				
			||||||
		print(mgmt("/mail/users/password", { "email": email, "password": pw }))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
elif sys.argv[1] == "user" and sys.argv[2] == "remove" and len(sys.argv) == 4:
 | 
					 | 
				
			||||||
	print(mgmt("/mail/users/remove", { "email": sys.argv[3] }))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
elif sys.argv[1] == "user" and sys.argv[2] in ("make-admin", "remove-admin") and len(sys.argv) == 4:
 | 
					 | 
				
			||||||
	if sys.argv[2] == "make-admin":
 | 
					 | 
				
			||||||
		action = "add"
 | 
					 | 
				
			||||||
	else:
 | 
					 | 
				
			||||||
		action = "remove"
 | 
					 | 
				
			||||||
	print(mgmt("/mail/users/privileges/" + action, { "email": sys.argv[3], "privilege": "admin" }))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
elif sys.argv[1] == "user" and sys.argv[2] == "admins":
 | 
					 | 
				
			||||||
	# Dump a list of admin users.
 | 
					 | 
				
			||||||
	users = mgmt("/mail/users?format=json", is_json=True)
 | 
					 | 
				
			||||||
	for domain in users:
 | 
					 | 
				
			||||||
		for user in domain["users"]:
 | 
					 | 
				
			||||||
			if "admin" in user['privileges']:
 | 
					 | 
				
			||||||
				print(user['email'])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
elif sys.argv[1] == "alias" and len(sys.argv) == 2:
 | 
					 | 
				
			||||||
	print(mgmt("/mail/aliases"))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
elif sys.argv[1] == "alias" and sys.argv[2] == "add" and len(sys.argv) == 5:
 | 
					 | 
				
			||||||
	print(mgmt("/mail/aliases/add", { "address": sys.argv[3], "forwards_to": sys.argv[4] }))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
elif sys.argv[1] == "alias" and sys.argv[2] == "remove" and len(sys.argv) == 4:
 | 
					 | 
				
			||||||
	print(mgmt("/mail/aliases/remove", { "address": sys.argv[3] }))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
else:
 | 
					 | 
				
			||||||
	print("Invalid command-line arguments.")
 | 
					 | 
				
			||||||
	sys.exit(1)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user