mirror of
https://github.com/mail-in-a-box/mailinabox.git
synced 2024-11-24 02:37:05 +00:00
Add key-based authentication to management service
Intended to be the simplest auth possible: every time the service starts, a random key is written to `/var/lib/mailinabox/api.key`. In order to authenticate to the service, the client must pass the contents of `api.key` in an HTTP basic auth header. In this way, users who do not have read access to that file are not able to communicate with the service.
This commit is contained in:
parent
326cc2a451
commit
067052d4ea
69
management/auth.py
Normal file
69
management/auth.py
Normal file
@ -0,0 +1,69 @@
|
||||
import base64, os, os.path
|
||||
|
||||
from flask import make_response
|
||||
|
||||
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.
|
||||
"""
|
||||
def __init__(self, env):
|
||||
self.auth_realm = DEFAULT_AUTH_REALM
|
||||
self.key = self._generate_key()
|
||||
self.key_path = env.get('API_KEY_FILE') or DEFAULT_KEY_PATH
|
||||
|
||||
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')
|
||||
|
||||
def is_authenticated(self, request):
|
||||
"""Test if the client key passed in HTTP header matches the service key"""
|
||||
|
||||
def decode(s):
|
||||
return base64.b64decode(s.encode('utf-8')).decode('ascii')
|
||||
|
||||
def parse_api_key(header):
|
||||
if header is None:
|
||||
return
|
||||
|
||||
scheme, credentials = header.split(maxsplit=1)
|
||||
if scheme != 'Basic':
|
||||
return
|
||||
|
||||
username, password = decode(credentials).split(':', maxsplit=1)
|
||||
return username
|
||||
|
||||
request_key = parse_api_key(request.headers.get('Authorization'))
|
||||
|
||||
return request_key == self.key
|
||||
|
||||
def make_unauthorized_response(self):
|
||||
return make_response(
|
||||
'You must pass the API key from "{0}" as the username\n'.format(self.key_path),
|
||||
401,
|
||||
{ 'WWW-Authenticate': 'Basic realm="{0}"'.format(self.auth_realm) })
|
||||
|
||||
def _generate_key(self):
|
||||
raw_key = os.urandom(32)
|
||||
return base64.b64encode(raw_key).decode('ascii')
|
@ -2,14 +2,25 @@
|
||||
|
||||
import os, os.path, re
|
||||
|
||||
from flask import Flask, request, render_template
|
||||
from flask import Flask, request, render_template, abort
|
||||
app = Flask(__name__)
|
||||
|
||||
import utils
|
||||
import auth, utils
|
||||
from mailconfig import get_mail_users, add_mail_user, set_mail_password, remove_mail_user, get_mail_aliases, get_mail_domains, add_mail_alias, remove_mail_alias
|
||||
|
||||
env = utils.load_environment()
|
||||
|
||||
auth_service = auth.KeyAuthService(env)
|
||||
|
||||
@app.before_request
|
||||
def require_auth_key():
|
||||
if not auth_service.is_authenticated(request):
|
||||
abort(401)
|
||||
|
||||
@app.errorhandler(401)
|
||||
def unauthorized(error):
|
||||
return auth_service.make_unauthorized_response()
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
@ -97,4 +108,15 @@ def do_updates():
|
||||
|
||||
if __name__ == '__main__':
|
||||
if "DEBUG" in os.environ: app.debug = True
|
||||
|
||||
# For testing on the command line, you can use `curl` like so:
|
||||
# curl --user $(</var/lib/mailinabox/api.key): http://localhost:10222/mail/users
|
||||
auth_service.write_key()
|
||||
|
||||
# For testing in the browser, you can copy the API key that's output to the
|
||||
# debug console and enter that as the username
|
||||
app.logger.info('API key: ' + auth_service.key)
|
||||
|
||||
|
||||
app.run(port=10222)
|
||||
|
||||
|
@ -118,6 +118,7 @@ PUBLIC_HOSTNAME=$PUBLIC_HOSTNAME
|
||||
PUBLIC_IP=$PUBLIC_IP
|
||||
PUBLIC_IPV6=$PUBLIC_IPV6
|
||||
CSR_COUNTRY=$CSR_COUNTRY
|
||||
API_KEY_FILE=/var/lib/mailinabox/api.key
|
||||
EOF
|
||||
|
||||
# Start service configuration.
|
||||
|
Loading…
Reference in New Issue
Block a user