2542: Implement header authentication via external proxy r=mergify[bot] a=nextgens

## What type of PR?

Feature

## What does this PR do?

Implement header authentication via external proxy

### Related issue(s)
- closes #1972
- closes #2183

## Prerequisites
Before we can consider review and merge, please make sure the following list is done and checked.
If an entry in not applicable, you can check it or remove it from the list.

- [x] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


2559: Turns out that php81-ctype is required by roundcube r=mergify[bot] a=nextgens

## What type of PR?

bug-fix

## What does this PR do?

It solves:
```
[25-Nov-2022 08:19:20] WARNING: [pool php] child 335 said into stderr: "NOTICE: PHP message: PHP Fatal error:  Uncaught Error: Call to undefined function Masterminds\HTML5\Parser\ctype_alpha() in /var/www/roundcube/vendor/masterminds/html5/src/HTML5/Parser/Tokenizer.php:140"
[25-Nov-2022 08:19:20] WARNING: [pool php] child 335 said into stderr: "Stack trace:"
[25-Nov-2022 08:19:20] WARNING: [pool php] child 335 said into stderr: "#0 /var/www/roundcube/vendor/masterminds/html5/src/HTML5/Parser/Tokenizer.php(82): Masterminds\HTML5\Parser\Tokenizer->consumeData()"
[25-Nov-2022 08:19:20] WARNING: [pool php] child 335 said into stderr: "#1 /var/www/roundcube/vendor/masterminds/html5/src/HTML5.php(161): Masterminds\HTML5\Parser\Tokenizer->parse()"
[25-Nov-2022 08:19:20] WARNING: [pool php] child 335 said into stderr: "#2 /var/www/roundcube/vendor/masterminds/html5/src/HTML5.php(89): Masterminds\HTML5->parse('<html>\n    <hea...', Array)"
[25-Nov-2022 08:19:20] WARNING: [pool php] child 335 said into stderr: "#3 /var/www/roundcube/program/lib/Roundcube/rcube_washtml.php(700): Masterminds\HTML5->loadHTML('<html>\n    <hea...')"
[25-Nov-2022 08:19:20] WARNING: [pool php] child 335 said into stderr: "#4 /var/www/roundcube/program/actions/mail/index.php(975): rcube_washtml->wash('<html>\n    <hea...')"
[25-Nov-2022 08:19:20] WARNING: [pool php] child 335 said into stderr: "#5 /var/www/roundcube/program/actions/mail/index.php(1019): rcmail_action_mail_index::wash_html('<!doctype html>...', Array, Array)"
[25-Nov-2022 08:19:20] WARNING: [pool php] child 335 said into stderr: "#6 /var/www/roundcube/program/actions/mail/show.php(720): rcmail_action_mail_index::pr..."
```

see https://github.com/roundcube/roundcubemail/issues/7049


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
main
bors[bot] 2 years ago committed by GitHub
commit 033889dc95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -80,6 +80,9 @@ DEFAULT_CONFIG = {
'TLS_PERMISSIVE': True,
'TZ': 'Etc/UTC',
'DEFAULT_SPAM_THRESHOLD': 80,
'PROXY_AUTH_WHITELIST': '',
'PROXY_AUTH_HEADER': 'X-Auth-Email',
'PROXY_AUTH_CREATE': False,
# Host settings
'HOST_IMAP': 'imap',
'HOST_LMTP': 'imap:2525',
@ -171,6 +174,7 @@ class ConfigManager:
self.config['HOSTNAMES'] = ','.join(hostnames)
self.config['HOSTNAME'] = hostnames[0]
self.config['DEFAULT_SPAM_THRESHOLD'] = int(self.config['DEFAULT_SPAM_THRESHOLD'])
self.config['PROXY_AUTH_WHITELIST'] = set(ipaddress.ip_network(cidr, False) for cidr in (cidr.strip() for cidr in self.config['PROXY_AUTH_WHITELIST'].split(',')) if cidr)
# update the app config
app.config.update(self.config)

@ -6,6 +6,8 @@ from mailu.ui import access
from flask import current_app as app
import flask
import flask_login
import secrets
import ipaddress
@sso.route('/login', methods=['GET', 'POST'])
def login():
@ -57,3 +59,41 @@ def logout():
flask.session.destroy()
return flask.redirect(flask.url_for('.login'))
@sso.route('/proxy', methods=['GET'])
@sso.route('/proxy/<target>', methods=['GET'])
def proxy(target='webmail'):
ip = ipaddress.ip_address(flask.request.remote_addr)
if not any(ip in cidr for cidr in app.config['PROXY_AUTH_WHITELIST']):
return flask.abort(500, '%s is not on PROXY_AUTH_WHITELIST' % flask.request.remote_addr)
email = flask.request.headers.get(app.config['PROXY_AUTH_HEADER'])
if not email:
return flask.abort(500, 'No %s header' % app.config['PROXY_AUTH_HEADER'])
user = models.User.get(email)
if user:
flask.session.regenerate()
flask_login.login_user(user)
return flask.redirect(app.config['WEB_ADMIN'] if target=='admin' else app.config['WEB_WEBMAIL'])
if not app.config['PROXY_AUTH_CREATE']:
return flask.abort(500, 'You don\'t exist. Go away! (%s)' % email)
client_ip = flask.request.headers.get('X-Real-IP', flask.request.remote_addr)
try:
localpart, desireddomain = email.rsplit('@')
except Exception as e:
flask.current_app.logger.error('Error creating a new user via proxy for %s from %s: %s' % (email, client_ip, str(e)), e)
return flask.abort(500, 'You don\'t exist. Go away! (%s)' % email)
domain = models.Domain.query.get(desireddomain) or flask.abort(500, 'You don\'t exist. Go away! (domain=%s)' % desireddomain)
if not domain.max_users == -1 and len(domain.users) >= domain.max_users:
flask.current_app.logger.warning('Too many users for domain %s' % domain)
return flask.abort(500, 'Too many users in (domain=%s)' % domain)
user = models.User(localpart=localpart, domain=domain)
user.set_password(secrets.token_urlsafe())
models.db.session.add(user)
models.db.session.commit()
user.send_welcome()
flask.current_app.logger.info(f'Login succeeded by proxy created user: {user} from {client_ip} through {flask.request.remote_addr}.')
return flask.redirect(app.config['WEB_ADMIN'] if target=='admin' else app.config['WEB_WEBMAIL'])

@ -362,3 +362,15 @@ It can be configured with the following option:
When ``POSTFIX_LOG_FILE`` is enabled, the logrotate program will automatically rotate the
logs every week and keep 52 logs. To override the logrotate configuration, create the file logrotate.conf
with the desired configuration in the :ref:`Postfix overrides folder<override-label>`.
Header authentication using an external proxy
---------------------------------------------
The ``PROXY_AUTH_WHITELIST`` (default: unset/disabled) option allows you to configure a comma separated list of CIDRs of proxies to trust for authentication. This list is separate from ``REAL_IP_FROM`` and any entry in ``PROXY_AUTH_WHITELIST`` should also appear in ``REAL_IP_FROM``.
Use ``PROXY_AUTH_HEADER`` (default: 'X-Auth-Email') to customize which HTTP header the email address of the user to authenticate as should be and ``PROXY_AUTH_CREATE`` (default: False) to control whether non-existing accounts should be auto-created. Please note that Mailu doesn't currently support creating new users for non-existing domains; you do need to create all the domains that may be used manually.
Once configured, any request to /sso/proxy will be redirected to the webmail and /sso/proxy/admin to the admin panel. Please check issue `1972` for more details.
.. _`1972`: https://github.com/Mailu/Mailu/issues/1972

@ -0,0 +1 @@
Implement Header authentication via external proxy

@ -12,7 +12,7 @@ RUN set -euxo pipefail \
; apk add --no-cache \
nginx gpg gpg-agent \
php81 php81-fpm php81-mbstring php81-zip php81-xml php81-simplexml php81-pecl-apcu \
php81-dom php81-curl php81-exif gd php81-gd php81-iconv php81-intl php81-openssl \
php81-dom php81-curl php81-exif gd php81-gd php81-iconv php81-intl php81-openssl php81-ctype \
php81-pdo_sqlite php81-pdo_mysql php81-pdo_pgsql php81-pdo php81-sodium libsodium php81-tidy php81-pecl-uuid \
php81-pspell php81-pecl-imagick php81-opcache php81-session php81-sockets php81-fileinfo \
aspell-uk aspell-ru aspell-fr aspell-de aspell-en \

Loading…
Cancel
Save