auth.py 3.24 KB
Newer Older
1 2 3 4
import base64, os, os.path

from flask import make_response

5 6 7
import utils
from mailconfig import get_mail_user_privileges

8 9 10 11 12 13 14 15 16 17
DEFAULT_KEY_PATH   = '/var/lib/mailinabox/api.key'
DEFAULT_AUTH_REALM = 'Mail-in-a-Box Management Server'

class KeyAuthService:
	"""Generate an API key for authenticating clients

	Clients must read the key from the key file and send the key with all HTTP
	requests. The key is passed as the username field in the standard HTTP
	Basic Auth header.
	"""
18
	def __init__(self):
19 20
		self.auth_realm = DEFAULT_AUTH_REALM
		self.key = self._generate_key()
21
		self.key_path = DEFAULT_KEY_PATH
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42

	def write_key(self):
		"""Write key to file so authorized clients can get the key

		The key file is created with mode 0640 so that additional users can be
		authorized to access the API by granting group/ACL read permissions on
		the key file.
		"""
		def create_file_with_mode(path, mode):
			# Based on answer by A-B-B: http://stackoverflow.com/a/15015748
			old_umask = os.umask(0)
			try:
				return os.fdopen(os.open(path, os.O_WRONLY | os.O_CREAT, mode), 'w')
			finally:
				os.umask(old_umask)

		os.makedirs(os.path.dirname(self.key_path), exist_ok=True)

		with create_file_with_mode(self.key_path, 0o640) as key_file:
			key_file.write(self.key + '\n')

43 44 45 46
	def is_authenticated(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."""
47 48

		def decode(s):
49
			return base64.b64decode(s.encode('ascii')).decode('ascii')
50

51
		def parse_basic_auth(header):
52
			if " " not in header:
53
				return None, None
54 55
			scheme, credentials = header.split(maxsplit=1)
			if scheme != 'Basic':
56
				return None, None
57

58 59
			credentials = decode(credentials)
			if ":" not in credentials:
60
				return None, None
61
			username, password = credentials.split(':', maxsplit=1)
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
			return username, password

		header = request.headers.get('Authorization')
		if not header:
			return "No authorization header provided."

		username, password = parse_basic_auth(header)

		if username in (None, ""):
			return "Authorization header invalid."
		elif username == self.key:
			return "OK"
		else:
			return self.check_imap_login( username, password, env)

	def check_imap_login(self, email, pw, env):
		# Validate a user's credentials.

		# Sanity check.
		if email == "" or pw == "":
			return "Enter an email address and password."

		# Authenticate.
		try:
			# Use doveadm 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
				])
		except:
			# Login failed.
			return "Invalid email address or password."

		# Authorize.
		# (This call should never fail on a valid user.)
		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"
106 107 108 109

	def _generate_key(self):
		raw_key = os.urandom(32)
		return base64.b64encode(raw_key).decode('ascii')