Merge pull request #74 from mkropat/mgmt-auth
Add authentication to mailinabox-daemon; resolves #67
This commit is contained in:
commit
8076ce4ab9
6 changed files with 112 additions and 7 deletions
|
@ -58,7 +58,7 @@ For instance, in my case, I could tell my domain name registrar that `ns1.box.oc
|
||||||
|
|
||||||
Optionally, to activate DNSSEC, you'll need to get a DS record from the box. While logged in on the box, run:
|
Optionally, to activate DNSSEC, you'll need to get a DS record from the box. While logged in on the box, run:
|
||||||
|
|
||||||
curl http://localhost:10222/dns/ds
|
sudo bash -c 'curl --user $(</var/lib/mailinabox/api.key): http://localhost:10222/dns/ds'
|
||||||
|
|
||||||
This will print DS records for the domains that your box thinks it is the authoritative name server for. Copy the DS record and follow the DS record instructions provided by your domain name registrar.
|
This will print DS records for the domains that your box thinks it is the authoritative name server for. Copy the DS record and follow the DS record instructions provided by your domain name registrar.
|
||||||
|
|
||||||
|
|
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):
|
||||||
|
self.auth_realm = DEFAULT_AUTH_REALM
|
||||||
|
self.key = self._generate_key()
|
||||||
|
self.key_path = 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
|
import os, os.path, re
|
||||||
|
|
||||||
from flask import Flask, request, render_template
|
from flask import Flask, request, render_template, abort
|
||||||
app = Flask(__name__)
|
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
|
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()
|
env = utils.load_environment()
|
||||||
|
|
||||||
|
auth_service = auth.KeyAuthService()
|
||||||
|
|
||||||
|
@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('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
return render_template('index.html')
|
return render_template('index.html')
|
||||||
|
@ -101,4 +112,13 @@ if __name__ == '__main__':
|
||||||
if not app.debug:
|
if not app.debug:
|
||||||
app.logger.addHandler(utils.create_syslog_handler())
|
app.logger.addHandler(utils.create_syslog_handler())
|
||||||
|
|
||||||
|
# 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)
|
app.run(port=10222)
|
||||||
|
|
||||||
|
|
|
@ -68,7 +68,7 @@ cat > /etc/cron.daily/mailinabox-dnssec << EOF;
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Mail-in-a-Box
|
# Mail-in-a-Box
|
||||||
# Re-sign any DNS zones with DNSSEC because the signatures expire periodically.
|
# Re-sign any DNS zones with DNSSEC because the signatures expire periodically.
|
||||||
curl -d GO http://localhost:10222/dns/update
|
curl -d GO --user \$(</var/lib/mailinabox/api.key): http://localhost:10222/dns/update
|
||||||
EOF
|
EOF
|
||||||
chmod +x /etc/cron.daily/mailinabox-dnssec
|
chmod +x /etc/cron.daily/mailinabox-dnssec
|
||||||
|
|
||||||
|
|
|
@ -133,8 +133,8 @@ EOF
|
||||||
|
|
||||||
# Write the DNS and nginx configuration files.
|
# Write the DNS and nginx configuration files.
|
||||||
sleep 5 # wait for the daemon to start
|
sleep 5 # wait for the daemon to start
|
||||||
curl -s -d POSTDATA http://127.0.0.1:10222/dns/update
|
curl -s -d POSTDATA --user $(</var/lib/mailinabox/api.key): http://127.0.0.1:10222/dns/update
|
||||||
curl -s -d POSTDATA http://127.0.0.1:10222/web/update
|
curl -s -d POSTDATA --user $(</var/lib/mailinabox/api.key): http://127.0.0.1:10222/web/update
|
||||||
|
|
||||||
# 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 "`tools/mail.py user`" ]; then
|
||||||
|
|
|
@ -3,7 +3,11 @@
|
||||||
import sys, getpass, urllib.request, urllib.error
|
import sys, getpass, urllib.request, urllib.error
|
||||||
|
|
||||||
def mgmt(cmd, data=None):
|
def mgmt(cmd, data=None):
|
||||||
req = urllib.request.Request('http://localhost:10222' + cmd, urllib.parse.urlencode(data).encode("utf8") if data else None)
|
mgmt_uri = 'http://localhost:10222'
|
||||||
|
|
||||||
|
setup_key_auth(mgmt_uri)
|
||||||
|
|
||||||
|
req = urllib.request.Request(mgmt_uri + cmd, urllib.parse.urlencode(data).encode("utf8") if data else None)
|
||||||
try:
|
try:
|
||||||
response = urllib.request.urlopen(req)
|
response = urllib.request.urlopen(req)
|
||||||
except urllib.error.HTTPError as e:
|
except urllib.error.HTTPError as e:
|
||||||
|
@ -20,6 +24,18 @@ def read_password():
|
||||||
second = getpass.getpass(' (again): ')
|
second = getpass.getpass(' (again): ')
|
||||||
return first
|
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:
|
if len(sys.argv) < 2:
|
||||||
print("Usage: ")
|
print("Usage: ")
|
||||||
print(" tools/mail.py user (lists users)")
|
print(" tools/mail.py user (lists users)")
|
||||||
|
|
Loading…
Reference in a new issue