Process review comments (PR2464)

main
Dimitri Huisman 2 years ago committed by Alexander Graf
parent afb224e796
commit 61d092922c
No known key found for this signature in database
GPG Key ID: B8A9DC143E075629

@ -75,7 +75,7 @@ def create_app_from_config(config):
app.register_blueprint(internal.internal, url_prefix='/internal') app.register_blueprint(internal.internal, url_prefix='/internal')
app.register_blueprint(sso.sso, url_prefix='/sso') app.register_blueprint(sso.sso, url_prefix='/sso')
if app.config.get('API_TOKEN'): if app.config.get('API_TOKEN'):
api.register(app, web_api=app.config.get('WEB_API')) api.register(app, web_api_root=app.config.get('WEB_API'))
return app return app

@ -1,24 +1,20 @@
from flask import redirect, url_for from flask import redirect, url_for
from flask_restx import apidoc from flask_restx import apidoc
from . import v1 from . import v1 as APIv1
def register(app, web_api): def register(app, web_api_root):
ACTIVE=v1 APIv1.app = app
ROOT=web_api
v1.app = app
# register api bluprint(s) # register api bluprint(s)
apidoc.apidoc.url_prefix = f'{ROOT}/v{int(v1.VERSION)}' apidoc.apidoc.url_prefix = f'{web_api_root}/v{int(APIv1.VERSION)}'
v1.api_token = app.config['API_TOKEN'] APIv1.api_token = app.config['API_TOKEN']
app.register_blueprint(v1.blueprint, url_prefix=f'{ROOT}/v{int(v1.VERSION)}') app.register_blueprint(APIv1.blueprint, url_prefix=f'{web_api_root}/v{int(APIv1.VERSION)}')
# add redirect to current api version # add redirect to current api version
@app.route(f'{ROOT}/') @app.route(f'{web_api_root}/')
def redir(): def redir():
return redirect(url_for(f'{ACTIVE.blueprint.name}.root')) return redirect(url_for(f'{APIv1.blueprint.name}.root'))
# swagger ui config # swagger ui config
app.config.SWAGGER_UI_DOC_EXPANSION = 'list' app.config.SWAGGER_UI_DOC_EXPANSION = 'list'

@ -5,13 +5,15 @@ import flask
import hmac import hmac
from functools import wraps from functools import wraps
from flask_restx import abort from flask_restx import abort
from sqlalchemy.sql.expression import label
def fqdn_in_use(*names): def fqdn_in_use(name):
for name in names: d = models.db.session.query(label('name', models.Domain.name))
for model in models.Domain, models.Alternative, models.Relay: a = models.db.session.query(label('name', models.Alternative.name))
if model.query.get(name): r = models.db.session.query(label('name', models.Relay.name))
return model if d.union_all(a).union_all(r).filter_by(name=name).count() > 0:
return None return True
return False
""" Decorator for validating api token for authentication """ """ Decorator for validating api token for authentication """
def api_token_authorization(func): def api_token_authorization(func):
@ -20,14 +22,12 @@ def api_token_authorization(func):
client_ip = flask.request.headers.get('X-Real-IP', flask.request.remote_addr) client_ip = flask.request.headers.get('X-Real-IP', flask.request.remote_addr)
if utils.limiter.should_rate_limit_ip(client_ip): if utils.limiter.should_rate_limit_ip(client_ip):
abort(429, 'Too many attempts from your IP (rate-limit)' ) abort(429, 'Too many attempts from your IP (rate-limit)' )
if (request.args.get('api_token') == '' or if not request.headers.get('Authorization'):
request.args.get('api_token') == None): abort(401, 'A valid API token is expected which is provided as request header')
abort(401, 'A valid API token is expected as query string parameter') if not hmac.compare_digest(request.headers.get('Authorization'), v1.api_token):
if not hmac.compare_digest(request.args.get('api_token'), v1.api_token):
utils.limiter.rate_limit_ip(client_ip) utils.limiter.rate_limit_ip(client_ip)
flask.current_app.logger.warn(f'Invalid API token provided by {client_ip}.') flask.current_app.logger.warn(f'Invalid API token provided by {client_ip}.')
abort(403, 'A valid API token is expected as query string parameter') abort(403, 'A valid API token is expected which is provided as request header')
else:
flask.current_app.logger.info(f'Valid API token provided by {client_ip}.') flask.current_app.logger.info(f'Valid API token provided by {client_ip}.')
return func(*args, **kwds) return func(*args, **kwds)
return decorated_function return decorated_function

@ -8,20 +8,19 @@ api_token = None
blueprint = Blueprint(f'api_v{int(VERSION)}', __name__) blueprint = Blueprint(f'api_v{int(VERSION)}', __name__)
authorization = { authorization = {
'apikey': { 'Bearer': {
'type': 'apiKey', 'type': 'apiKey',
'in': 'query', 'in': 'header',
'name': 'api_token' 'name': 'Authorization'
} }
} }
api = Api( api = Api(
blueprint, version=f'{VERSION:.1f}', blueprint, version=f'{VERSION:.1f}',
title='Mailu API', default_label='Mailu', title='Mailu API', default_label='Mailu',
validate=True, validate=True,
authorizations=authorization, authorizations=authorization,
security='apikey', security='Bearer',
doc='/swaggerui/' doc='/swaggerui/'
) )

@ -24,7 +24,7 @@ alias_fields = alias.inherit('Alias',alias_fields_update, {
class Aliases(Resource): class Aliases(Resource):
@alias.doc('list_alias') @alias.doc('list_alias')
@alias.marshal_with(alias_fields, as_list=True, skip_none=True, mask=None) @alias.marshal_with(alias_fields, as_list=True, skip_none=True, mask=None)
@alias.doc(security='apikey') @alias.doc(security='Bearer')
@common.api_token_authorization @common.api_token_authorization
def get(self): def get(self):
""" List aliases """ """ List aliases """
@ -35,7 +35,7 @@ class Aliases(Resource):
@alias.response(200, 'Success', response_fields) @alias.response(200, 'Success', response_fields)
@alias.response(400, 'Input validation exception', response_fields) @alias.response(400, 'Input validation exception', response_fields)
@alias.response(409, 'Duplicate alias', response_fields) @alias.response(409, 'Duplicate alias', response_fields)
@alias.doc(security='apikey') @alias.doc(security='Bearer')
@common.api_token_authorization @common.api_token_authorization
def post(self): def post(self):
""" Create a new alias """ """ Create a new alias """
@ -60,7 +60,7 @@ class Alias(Resource):
@alias.doc('find_alias') @alias.doc('find_alias')
@alias.response(200, 'Success', alias_fields) @alias.response(200, 'Success', alias_fields)
@alias.response(404, 'Alias not found', response_fields) @alias.response(404, 'Alias not found', response_fields)
@alias.doc(security='apikey') @alias.doc(security='Bearer')
@common.api_token_authorization @common.api_token_authorization
def get(self, alias): def get(self, alias):
""" Find alias """ """ Find alias """
@ -75,7 +75,7 @@ class Alias(Resource):
@alias.response(200, 'Success', response_fields) @alias.response(200, 'Success', response_fields)
@alias.response(404, 'Alias not found', response_fields) @alias.response(404, 'Alias not found', response_fields)
@alias.response(400, 'Input validation exception', response_fields) @alias.response(400, 'Input validation exception', response_fields)
@alias.doc(security='apikey') @alias.doc(security='Bearer')
@common.api_token_authorization @common.api_token_authorization
def put(self, alias): def put(self, alias):
""" Update alias """ """ Update alias """
@ -97,7 +97,7 @@ class Alias(Resource):
@alias.doc('delete_alias') @alias.doc('delete_alias')
@alias.response(200, 'Success', response_fields) @alias.response(200, 'Success', response_fields)
@alias.response(404, 'Alias not found', response_fields) @alias.response(404, 'Alias not found', response_fields)
@alias.doc(security='apikey') @alias.doc(security='Bearer')
@common.api_token_authorization @common.api_token_authorization
def delete(self, alias): def delete(self, alias):
""" Delete alias """ """ Delete alias """
@ -113,7 +113,7 @@ class AliasWithDest(Resource):
@alias.doc('find_alias_filter_domain') @alias.doc('find_alias_filter_domain')
@alias.response(200, 'Success', alias_fields) @alias.response(200, 'Success', alias_fields)
@alias.response(404, 'Alias or domain not found', response_fields) @alias.response(404, 'Alias or domain not found', response_fields)
@alias.doc(security='apikey') @alias.doc(security='Bearer')
@common.api_token_authorization @common.api_token_authorization
def get(self, domain): def get(self, domain):
""" Find aliases of domain """ """ Find aliases of domain """

@ -78,7 +78,7 @@ alternative_fields = api.model('AlternativeDomain', {
class Domains(Resource): class Domains(Resource):
@dom.doc('list_domain') @dom.doc('list_domain')
@dom.marshal_with(domain_fields_get, as_list=True, skip_none=True, mask=None) @dom.marshal_with(domain_fields_get, as_list=True, skip_none=True, mask=None)
@dom.doc(security='apikey') @dom.doc(security='Bearer')
@common.api_token_authorization @common.api_token_authorization
def get(self): def get(self):
""" List domains """ """ List domains """
@ -89,7 +89,7 @@ class Domains(Resource):
@dom.response(200, 'Success', response_fields) @dom.response(200, 'Success', response_fields)
@dom.response(400, 'Input validation exception', response_fields) @dom.response(400, 'Input validation exception', response_fields)
@dom.response(409, 'Duplicate domain/alternative name', response_fields) @dom.response(409, 'Duplicate domain/alternative name', response_fields)
@dom.doc(security='apikey') @dom.doc(security='Bearer')
@common.api_token_authorization @common.api_token_authorization
def post(self): def post(self):
""" Create a new domain """ """ Create a new domain """
@ -133,7 +133,7 @@ class Domain(Resource):
@dom.doc('find_domain') @dom.doc('find_domain')
@dom.response(200, 'Success', domain_fields) @dom.response(200, 'Success', domain_fields)
@dom.response(404, 'Domain not found', response_fields) @dom.response(404, 'Domain not found', response_fields)
@dom.doc(security='apikey') @dom.doc(security='Bearer')
@common.api_token_authorization @common.api_token_authorization
def get(self, domain): def get(self, domain):
""" Find domain by name """ """ Find domain by name """
@ -150,7 +150,7 @@ class Domain(Resource):
@dom.response(400, 'Input validation exception', response_fields) @dom.response(400, 'Input validation exception', response_fields)
@dom.response(404, 'Domain not found', response_fields) @dom.response(404, 'Domain not found', response_fields)
@dom.response(409, 'Duplicate domain/alternative name', response_fields) @dom.response(409, 'Duplicate domain/alternative name', response_fields)
@dom.doc(security='apikey') @dom.doc(security='Bearer')
@common.api_token_authorization @common.api_token_authorization
def put(self, domain): def put(self, domain):
""" Update an existing domain """ """ Update an existing domain """
@ -194,7 +194,7 @@ class Domain(Resource):
@dom.response(200, 'Success', response_fields) @dom.response(200, 'Success', response_fields)
@dom.response(400, 'Input validation exception', response_fields) @dom.response(400, 'Input validation exception', response_fields)
@dom.response(404, 'Domain not found', response_fields) @dom.response(404, 'Domain not found', response_fields)
@dom.doc(security='apikey') @dom.doc(security='Bearer')
@common.api_token_authorization @common.api_token_authorization
def delete(self, domain): def delete(self, domain):
""" Delete domain """ """ Delete domain """
@ -213,7 +213,7 @@ class Domain(Resource):
@dom.response(200, 'Success', response_fields) @dom.response(200, 'Success', response_fields)
@dom.response(400, 'Input validation exception', response_fields) @dom.response(400, 'Input validation exception', response_fields)
@dom.response(404, 'Domain not found', response_fields) @dom.response(404, 'Domain not found', response_fields)
@dom.doc(security='apikey') @dom.doc(security='Bearer')
@common.api_token_authorization @common.api_token_authorization
def post(self, domain): def post(self, domain):
""" Generate new DKIM/DMARC keys for domain """ """ Generate new DKIM/DMARC keys for domain """
@ -232,7 +232,7 @@ class Manager(Resource):
@dom.marshal_with(manager_fields, as_list=True, skip_none=True, mask=None) @dom.marshal_with(manager_fields, as_list=True, skip_none=True, mask=None)
@dom.response(400, 'Input validation exception', response_fields) @dom.response(400, 'Input validation exception', response_fields)
@dom.response(404, 'domain not found', response_fields) @dom.response(404, 'domain not found', response_fields)
@dom.doc(security='apikey') @dom.doc(security='Bearer')
@common.api_token_authorization @common.api_token_authorization
def get(self, domain): def get(self, domain):
""" List managers of domain """ """ List managers of domain """
@ -249,7 +249,7 @@ class Manager(Resource):
@dom.response(400, 'Input validation exception', response_fields) @dom.response(400, 'Input validation exception', response_fields)
@dom.response(404, 'User or domain not found', response_fields) @dom.response(404, 'User or domain not found', response_fields)
@dom.response(409, 'Duplicate domain manager', response_fields) @dom.response(409, 'Duplicate domain manager', response_fields)
@dom.doc(security='apikey') @dom.doc(security='Bearer')
@common.api_token_authorization @common.api_token_authorization
def post(self, domain): def post(self, domain):
""" Create a new domain manager """ """ Create a new domain manager """
@ -275,7 +275,7 @@ class Domain(Resource):
@dom.doc('find_manager') @dom.doc('find_manager')
@dom.response(200, 'Success', manager_fields) @dom.response(200, 'Success', manager_fields)
@dom.response(404, 'Manager not found', response_fields) @dom.response(404, 'Manager not found', response_fields)
@dom.doc(security='apikey') @dom.doc(security='Bearer')
@common.api_token_authorization @common.api_token_authorization
def get(self, domain, email): def get(self, domain, email):
""" Find manager by email address """ """ Find manager by email address """
@ -301,7 +301,7 @@ class Domain(Resource):
@dom.response(200, 'Success', response_fields) @dom.response(200, 'Success', response_fields)
@dom.response(400, 'Input validation exception', response_fields) @dom.response(400, 'Input validation exception', response_fields)
@dom.response(404, 'Manager not found', response_fields) @dom.response(404, 'Manager not found', response_fields)
@dom.doc(security='apikey') @dom.doc(security='Bearer')
@common.api_token_authorization @common.api_token_authorization
def delete(self, domain, email): def delete(self, domain, email):
if not validators.email(email): if not validators.email(email):
@ -327,7 +327,7 @@ class User(Resource):
@dom.marshal_with(user.user_fields_get, as_list=True, skip_none=True, mask=None) @dom.marshal_with(user.user_fields_get, as_list=True, skip_none=True, mask=None)
@dom.response(400, 'Input validation exception', response_fields) @dom.response(400, 'Input validation exception', response_fields)
@dom.response(404, 'Domain not found', response_fields) @dom.response(404, 'Domain not found', response_fields)
@dom.doc(security='apikey') @dom.doc(security='Bearer')
@common.api_token_authorization @common.api_token_authorization
def get(self, domain): def get(self, domain):
""" List users from domain """ """ List users from domain """
@ -343,7 +343,7 @@ class Alternatives(Resource):
@alt.doc('list_alternative') @alt.doc('list_alternative')
@alt.marshal_with(alternative_fields, as_list=True, skip_none=True, mask=None) @alt.marshal_with(alternative_fields, as_list=True, skip_none=True, mask=None)
@alt.doc(security='apikey') @alt.doc(security='Bearer')
@common.api_token_authorization @common.api_token_authorization
def get(self): def get(self):
""" List alternatives """ """ List alternatives """
@ -356,7 +356,7 @@ class Alternatives(Resource):
@alt.response(400, 'Input validation exception', response_fields) @alt.response(400, 'Input validation exception', response_fields)
@alt.response(404, 'Domain not found or missing', response_fields) @alt.response(404, 'Domain not found or missing', response_fields)
@alt.response(409, 'Duplicate alternative domain name', response_fields) @alt.response(409, 'Duplicate alternative domain name', response_fields)
@alt.doc(security='apikey') @alt.doc(security='Bearer')
@common.api_token_authorization @common.api_token_authorization
def post(self): def post(self):
""" Create new alternative (for domain) """ """ Create new alternative (for domain) """
@ -379,7 +379,7 @@ class Alternatives(Resource):
@alt.route('/<string:alt>') @alt.route('/<string:alt>')
class Alternative(Resource): class Alternative(Resource):
@alt.doc('find_alternative') @alt.doc('find_alternative')
@alt.doc(security='apikey') @alt.doc(security='Bearer')
@common.api_token_authorization @common.api_token_authorization
def get(self, alt): def get(self, alt):
""" Find alternative (of domain) """ """ Find alternative (of domain) """
@ -395,7 +395,7 @@ class Alternative(Resource):
@alt.response(400, 'Input validation exception', response_fields) @alt.response(400, 'Input validation exception', response_fields)
@alt.response(404, 'Alternative/Domain not found or missing', response_fields) @alt.response(404, 'Alternative/Domain not found or missing', response_fields)
@alt.response(409, 'Duplicate domain name', response_fields) @alt.response(409, 'Duplicate domain name', response_fields)
@alt.doc(security='apikey') @alt.doc(security='Bearer')
@common.api_token_authorization @common.api_token_authorization
def delete(self, alt): def delete(self, alt):
""" Delete alternative (for domain) """ """ Delete alternative (for domain) """

@ -24,7 +24,7 @@ relay_fields_update = api.model('RelayUpdate', {
class Relays(Resource): class Relays(Resource):
@relay.doc('list_relays') @relay.doc('list_relays')
@relay.marshal_with(relay_fields, as_list=True, skip_none=True, mask=None) @relay.marshal_with(relay_fields, as_list=True, skip_none=True, mask=None)
@relay.doc(security='apikey') @relay.doc(security='Bearer')
@common.api_token_authorization @common.api_token_authorization
def get(self): def get(self):
"List relays" "List relays"
@ -35,7 +35,7 @@ class Relays(Resource):
@relay.response(200, 'Success', response_fields) @relay.response(200, 'Success', response_fields)
@relay.response(400, 'Input validation exception') @relay.response(400, 'Input validation exception')
@relay.response(409, 'Duplicate relay', response_fields) @relay.response(409, 'Duplicate relay', response_fields)
@relay.doc(security='apikey') @relay.doc(security='Bearer')
@common.api_token_authorization @common.api_token_authorization
def post(self): def post(self):
""" Create relay """ """ Create relay """
@ -61,7 +61,7 @@ class Relay(Resource):
@relay.doc('find_relay') @relay.doc('find_relay')
@relay.response(400, 'Input validation exception', response_fields) @relay.response(400, 'Input validation exception', response_fields)
@relay.response(404, 'Relay not found', response_fields) @relay.response(404, 'Relay not found', response_fields)
@relay.doc(security='apikey') @relay.doc(security='Bearer')
@common.api_token_authorization @common.api_token_authorization
def get(self, name): def get(self, name):
""" Find relay """ """ Find relay """
@ -79,7 +79,7 @@ class Relay(Resource):
@relay.response(400, 'Input validation exception', response_fields) @relay.response(400, 'Input validation exception', response_fields)
@relay.response(404, 'Relay not found', response_fields) @relay.response(404, 'Relay not found', response_fields)
@relay.response(409, 'Duplicate relay', response_fields) @relay.response(409, 'Duplicate relay', response_fields)
@relay.doc(security='apikey') @relay.doc(security='Bearer')
@common.api_token_authorization @common.api_token_authorization
def put(self, name): def put(self, name):
""" Update relay """ """ Update relay """
@ -105,7 +105,7 @@ class Relay(Resource):
@relay.response(200, 'Success', response_fields) @relay.response(200, 'Success', response_fields)
@relay.response(400, 'Input validation exception', response_fields) @relay.response(400, 'Input validation exception', response_fields)
@relay.response(404, 'Relay not found', response_fields) @relay.response(404, 'Relay not found', response_fields)
@relay.doc(security='apikey') @relay.doc(security='Bearer')
@common.api_token_authorization @common.api_token_authorization
def delete(self, name): def delete(self, name):
""" Delete relay """ """ Delete relay """

@ -64,7 +64,7 @@ user_fields_put = api.model('UserUpdate', {
class Users(Resource): class Users(Resource):
@user.doc('list_users') @user.doc('list_users')
@user.marshal_with(user_fields_get, as_list=True, skip_none=True, mask=None) @user.marshal_with(user_fields_get, as_list=True, skip_none=True, mask=None)
@user.doc(security='apikey') @user.doc(security='Bearer')
@common.api_token_authorization @common.api_token_authorization
def get(self): def get(self):
"List users" "List users"
@ -75,7 +75,7 @@ class Users(Resource):
@user.response(200, 'Success', response_fields) @user.response(200, 'Success', response_fields)
@user.response(400, 'Input validation exception') @user.response(400, 'Input validation exception')
@user.response(409, 'Duplicate user', response_fields) @user.response(409, 'Duplicate user', response_fields)
@user.doc(security='apikey') @user.doc(security='Bearer')
@common.api_token_authorization @common.api_token_authorization
def post(self): def post(self):
""" Create user """ """ Create user """
@ -141,7 +141,7 @@ class User(Resource):
@user.doc('find_user') @user.doc('find_user')
@user.response(400, 'Input validation exception', response_fields) @user.response(400, 'Input validation exception', response_fields)
@user.response(404, 'User not found', response_fields) @user.response(404, 'User not found', response_fields)
@user.doc(security='apikey') @user.doc(security='Bearer')
@common.api_token_authorization @common.api_token_authorization
def get(self, email): def get(self, email):
""" Find user """ """ Find user """
@ -159,7 +159,7 @@ class User(Resource):
@user.response(400, 'Input validation exception', response_fields) @user.response(400, 'Input validation exception', response_fields)
@user.response(404, 'User not found', response_fields) @user.response(404, 'User not found', response_fields)
@user.response(409, 'Duplicate user', response_fields) @user.response(409, 'Duplicate user', response_fields)
@user.doc(security='apikey') @user.doc(security='Bearer')
@common.api_token_authorization @common.api_token_authorization
def put(self, email): def put(self, email):
""" Update user """ """ Update user """
@ -222,7 +222,7 @@ class User(Resource):
@user.response(200, 'Success', response_fields) @user.response(200, 'Success', response_fields)
@user.response(400, 'Input validation exception', response_fields) @user.response(400, 'Input validation exception', response_fields)
@user.response(404, 'User not found', response_fields) @user.response(404, 'User not found', response_fields)
@user.doc(security='apikey') @user.doc(security='Bearer')
@common.api_token_authorization @common.api_token_authorization
def delete(self, email): def delete(self, email):
""" Delete user """ """ Delete user """

@ -71,7 +71,7 @@ DEFAULT_CONFIG = {
'LOGO_BACKGROUND': None, 'LOGO_BACKGROUND': None,
'API': False, 'API': False,
# Advanced settings # Advanced settings
'API' : 'false', 'API' : False,
'WEB_API' : '/api', 'WEB_API' : '/api',
'API_TOKEN': None, 'API_TOKEN': None,
'LOG_LEVEL': 'WARNING', 'LOG_LEVEL': 'WARNING',

@ -203,14 +203,13 @@ Depending on your particular deployment you most probably will want to change th
Advanced settings Advanced settings
----------------- -----------------
The ``API`` (default: False) setting controls if the API endpoint is publicly The ``API`` (default: False) setting controls if the API endpoint is reachable.
reachable.
The ``WEB_API`` (default: /api) setting configures the endpoint that the API The ``WEB_API`` (default: /api) setting configures the endpoint that the API
listens on publicly&interally. The path must always start with a leading slash. listens on publicly&interally. The path must always start with a leading slash.
The ``API_TOKEN`` (default: None) enables the API endpoint. This token must be The ``API_TOKEN`` (default: None) enables the API endpoint. This token must be
passed as query parameter with requests to the API as authentication token. passed as request header to the API as authentication token.
The ``CREDENTIAL_ROUNDS`` (default: 12) setting is the number of rounds used by the The ``CREDENTIAL_ROUNDS`` (default: 12) setting is the number of rounds used by the
password hashing scheme. The number of rounds can be reduced in case faster password hashing scheme. The number of rounds can be reduced in case faster

Loading…
Cancel
Save