auth.py 5.14 KB
Newer Older
1
from mailu import models, utils
2
from mailu.internal import internal, nginx
3
from flask import current_app as app
4
5
6
7

import flask
import flask_login
import base64
8
import sqlalchemy.exc
Michael Wyraz's avatar
Michael Wyraz committed
9

10
11
12
13
@internal.route("/auth/email")
def nginx_authentication():
    """ Main authentication endpoint for Nginx email server
    """
kaiyou's avatar
kaiyou committed
14
    client_ip = flask.request.headers["Client-Ip"]
15
16
17
18
19
20
21
    headers = flask.request.headers
    if headers["Auth-Port"] == '25' and headers['Auth-Method'] == 'plain':
        response = flask.Response()
        response.headers['Auth-Status'] = 'AUTH not supported'
        response.headers['Auth-Error-Code'] = '502 5.5.1'
        utils.limiter.rate_limit_ip(client_ip)
        return response
Florent Daigniere's avatar
Florent Daigniere committed
22
23
    is_from_webmail = headers['Auth-Port'] in ['10143', '10025']
    if not is_from_webmail and utils.limiter.should_rate_limit_ip(client_ip):
Florent Daigniere's avatar
Florent Daigniere committed
24
        status, code = nginx.get_status(flask.request.headers['Auth-Protocol'], 'ratelimit')
kaiyou's avatar
kaiyou committed
25
        response = flask.Response()
Florent Daigniere's avatar
Florent Daigniere committed
26
27
        response.headers['Auth-Status'] = status
        response.headers['Auth-Error-Code'] = code
kaiyou's avatar
kaiyou committed
28
29
30
        if int(flask.request.headers['Auth-Login-Attempt']) < 10:
            response.headers['Auth-Wait'] = '3'
        return response
31
32
33
34
    headers = nginx.handle_authentication(flask.request.headers)
    response = flask.Response()
    for key, value in headers.items():
        response.headers[key] = str(value)
Florent Daigniere's avatar
Florent Daigniere committed
35
    is_valid_user = False
Alexander Graf's avatar
Alexander Graf committed
36
    if response.headers.get("Auth-User-Exists") == "True":
Florent Daigniere's avatar
Florent Daigniere committed
37
38
39
40
41
42
43
44
45
46
47
        username = response.headers["Auth-User"]
        if utils.limiter.should_rate_limit_user(username, client_ip):
            # FIXME could be done before handle_authentication()
            status, code = nginx.get_status(flask.request.headers['Auth-Protocol'], 'ratelimit')
            response = flask.Response()
            response.headers['Auth-Status'] = status
            response.headers['Auth-Error-Code'] = code
            if int(flask.request.headers['Auth-Login-Attempt']) < 10:
                response.headers['Auth-Wait'] = '3'
            return response
        is_valid_user = True
Florent Daigniere's avatar
Florent Daigniere committed
48
    if headers.get("Auth-Status") == "OK":
Florent Daigniere's avatar
Florent Daigniere committed
49
        utils.limiter.exempt_ip_from_ratelimits(client_ip)
Florent Daigniere's avatar
Florent Daigniere committed
50
51
    elif is_valid_user:
        utils.limiter.rate_limit_user(username, client_ip)
Florent Daigniere's avatar
Florent Daigniere committed
52
    elif not is_from_webmail:
53
        utils.limiter.rate_limit_ip(client_ip)
54
55
56
57
58
59
60
61
62
63
64
65
    return response

@internal.route("/auth/admin")
def admin_authentication():
    """ Fails if the user is not an authenticated admin.
    """
    if (not flask_login.current_user.is_anonymous
        and flask_login.current_user.global_admin
        and flask_login.current_user.enabled):
        return ""
    return flask.abort(403)

66
67
68
69
70
71
72
@internal.route("/auth/user")
def user_authentication():
    """ Fails if the user is not authenticated.
    """
    if (not flask_login.current_user.is_anonymous
        and flask_login.current_user.enabled):
        response = flask.Response()
Florent Daigniere's avatar
Florent Daigniere committed
73
74
75
        email = flask_login.current_user.get_id()
        response.headers["X-User"] = models.IdnaEmail.process_bind_param(flask_login, email, "")
        response.headers["X-User-Token"] = utils.gen_temp_token(email, flask.session)
76
77
78
        return response
    return flask.abort(403)

79
80
81
82
83

@internal.route("/auth/basic")
def basic_authentication():
    """ Tries to authenticate using the Authorization header.
    """
Florent Daigniere's avatar
Florent Daigniere committed
84
    client_ip = flask.request.headers.get('X-Real-IP', flask.request.remote_addr)
Florent Daigniere's avatar
Florent Daigniere committed
85
86
87
88
89
    if utils.limiter.should_rate_limit_ip(client_ip):
        response = flask.Response(status=401)
        response.headers["WWW-Authenticate"] = 'Basic realm="Authentication rate limit from one source exceeded"'
        response.headers['Retry-After'] = '60'
        return response
90
91
92
    authorization = flask.request.headers.get("Authorization")
    if authorization and authorization.startswith("Basic "):
        encoded = authorization.replace("Basic ", "")
93
        user_email, password = base64.b64decode(encoded).split(b":", 1)
Florent Daigniere's avatar
Florent Daigniere committed
94
95
96
97
98
99
        user_email = user_email.decode("utf8")
        if utils.limiter.should_rate_limit_user(user_email, client_ip):
            response = flask.Response(status=401)
            response.headers["WWW-Authenticate"] = 'Basic realm="Authentication rate limit for this username exceeded"'
            response.headers['Retry-After'] = '60'
            return response
100
101
102
103
104
105
106
107
108
109
110
111
112
        try:
            user = models.User.query.get(user_email) if '@' in user_email else None
        except sqlalchemy.exc.StatementError as exc:
            exc = str(exc).split('\n', 1)[0]
            app.logger.warn(f'Invalid user {user_email!r}: {exc}')
        else:
            if user is not None and nginx.check_credentials(user, password.decode('utf-8'), client_ip, "web"):
                response = flask.Response()
                response.headers["X-User"] = models.IdnaEmail.process_bind_param(flask_login, user.email, "")
                utils.limiter.exempt_ip_from_ratelimits(client_ip)
                return response
            # We failed check_credentials
            utils.limiter.rate_limit_user(user_email, client_ip) if user else utils.limiter.rate_limit_ip(client_ip)
113
114
115
    response = flask.Response(status=401)
    response.headers["WWW-Authenticate"] = 'Basic realm="Login Required"'
    return response