Skip to content

Add impersonation feature for admins to webmail #3163

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion core/admin/mailu/internal/views/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,15 @@ def user_authentication():
if (not flask_login.current_user.is_anonymous
and flask_login.current_user.enabled):
response = flask.Response()
email = flask_login.current_user.get_id()
original_email = flask_login.current_user.get_id()

email = flask.request.headers.get('X-User-Email', original_email)
if (email != original_email and not utils.is_owner(original_email, email)):
return flask.abort(403)

response.headers["X-User"] = models.IdnaEmail.process_bind_param(flask_login, email, "")
response.headers["X-User-Token"] = utils.gen_temp_token(email, flask.session)
response.headers["X-Original-User"] = models.IdnaEmail.process_bind_param(flask_login, original_email, "")
return response
return flask.abort(403)

Expand Down
4 changes: 4 additions & 0 deletions core/admin/mailu/ui/templates/user/list.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,14 @@
</td>
<td>
<a href="{{ url_for('.user_settings', user_email=user.email) }}" title="{% trans %}Settings{% endtrans %}"><i class="fa fa-wrench"></i></a>&nbsp;
<a href="{{ url_for('.token_list', user_email=user.email) }}" title="{% trans %}Authentication tokens{% endtrans %}"><i class="fa fa-ticket-alt"></i></a>&nbsp;
<a href="{{ url_for('.user_reply', user_email=user.email) }}" title="{% trans %}Auto-reply{% endtrans %}"><i class="fa fa-plane"></i></a>&nbsp;
{%- if config["FETCHMAIL_ENABLED"] -%}
<a href="{{ url_for('.fetch_list', user_email=user.email) }}" title="{% trans %}Fetched accounts{% endtrans %}"><i class="fa fa-download"></i></a>&nbsp;
{%- endif -%}
{%- if config["WEBMAIL"] != "none" -%}
<a href="{{ config["WEB_ADMIN"] }}/webmail-as/{{ user.email }}/" title="{% trans %}Webmail{% endtrans %}" target="_blank"><i class="fa fa-envelope"></i></a>&nbsp;
{%- endif -%}
</td>
<td>{{ user }}</td>
<td data-sort="{{ user.allow_spoofing*4 + user.enable_imap*2 + user.enable_pop }}">
Expand Down
16 changes: 13 additions & 3 deletions core/admin/mailu/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import time

from multiprocessing import Value
from mailu import limiter
from mailu import limiter, models
from flask import current_app as app

import flask
Expand Down Expand Up @@ -499,14 +499,24 @@ def init_app(self, app):
cleaned = Value('i', False)
session = MailuSessionExtension()

def is_owner(owner_email, user_email):
owner = models.User.query.get(owner_email)
user = models.User.query.get(user_email)
if (not owner or not user):
return False

return (
user.email == owner.email
or user.domain in owner.get_managed_domains()
)

# this is used by the webmail to authenticate IMAP/SMTP
def verify_temp_token(email, token):
try:
if token.startswith('token-'):
if sessid := app.session_store.get(token):
session = MailuSession(sessid, app)
if session.get('_user_id', '') == email:
return True
return is_owner(session.get('_user_id'), email)
except:
pass

Expand Down
58 changes: 58 additions & 0 deletions core/nginx/conf/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,12 @@ http {
rewrite ^({{ WEB_WEBMAIL }})$ $1/ permanent;
rewrite ^{{ WEB_WEBMAIL }}/(.*) /$1 break;
{% endif %}
{% if WEB_WEBMAIL == '/' %}
proxy_set_header X-Remote-Base-Url {{ WEB_WEBMAIL }};
{% else %}
proxy_cookie_path / {{ WEB_WEBMAIL }}/;
proxy_set_header X-Remote-Base-Url {{ WEB_WEBMAIL }}/;
{% endif %}
include /etc/nginx/proxy.conf;
auth_request /internal/auth/user;
error_page 403 @sso_login;
Expand All @@ -220,8 +226,16 @@ http {
auth_request /internal/auth/user;
auth_request_set $user $upstream_http_x_user;
auth_request_set $token $upstream_http_x_user_token;
auth_request_set $original_user $upstream_http_x_original_user;
proxy_set_header X-Remote-User $user;
proxy_set_header X-Remote-User-Token $token;
proxy_set_header X-Remote-Original-User $original_user;
{% if WEB_WEBMAIL == '/' %}
proxy_set_header X-Remote-Base-Url {{ WEB_WEBMAIL }};
{% else %}
proxy_cookie_path / {{ WEB_WEBMAIL }}/;
proxy_set_header X-Remote-Base-Url {{ WEB_WEBMAIL }}/;
{% endif %}
error_page 403 @sso_login;
proxy_pass http://$webmail;
}
Expand All @@ -242,6 +256,44 @@ http {
proxy_pass http://$antispam;
error_page 403 @sso_login;
}

{% if WEBMAIL != 'none' %}
location ~ ^{{ WEB_ADMIN }}/webmail-as/([^/]+)/sso.php$ {
set $user_email $1;

rewrite ^{{ WEB_ADMIN }}/webmail-as/[^/]+/(.*) /$1 break;

include /etc/nginx/proxy.conf;
auth_request /internal/auth/user;
auth_request_set $user $upstream_http_x_user;
auth_request_set $token $upstream_http_x_user_token;
auth_request_set $original_user $upstream_http_x_original_user;
proxy_set_header X-Remote-User $user;
proxy_set_header X-Remote-User-Token $token;
proxy_set_header X-Remote-Original-User $original_user;

proxy_set_header X-Remote-Base-Url {{ WEB_ADMIN }}/webmail-as/$user_email;
proxy_cookie_path / {{ WEB_ADMIN }}/webmail-as/$user_email;

error_page 403 @sso_login;
proxy_pass http://$webmail;
}

location ~ ^{{ WEB_ADMIN }}/webmail-as/([^/]+)/ {
set $user_email $1;

rewrite ^{{ WEB_ADMIN }}/webmail-as/[^/]+/(.*) /$1 break;

include /etc/nginx/proxy.conf;
auth_request /internal/auth/user;

proxy_set_header X-Remote-Base-Url {{ WEB_ADMIN }}/webmail-as/$user_email;
proxy_cookie_path / {{ WEB_ADMIN }}/webmail-as/$user_email;

error_page 403 @sso_login;
proxy_pass http://$webmail;
}
{% endif %}
{% endif %}

{% if WEBDAV != 'none' %}
Expand Down Expand Up @@ -273,6 +325,12 @@ http {

proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Authorization $http_authorization;

if ($user_email = false) {
set $user_email '';
}
proxy_set_header X-User-Email $user_email;

proxy_pass_header Authorization;
proxy_pass http://$admin;
proxy_pass_request_body off;
Expand Down
6 changes: 6 additions & 0 deletions docs/webadministration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ Click the submit button to apply settings. With the default polling interval, fe
Make sure ``FETCHMAIL_ENABLED`` is set to ``true`` in ``mailu.env`` to enable fetching and showing fetchmail in the admin interface.


.. _webadministration_authentication_tokens:

Authentication tokens
---------------------

Expand Down Expand Up @@ -320,10 +322,14 @@ This page is also accessible for domain managers. On the users page new users ca

* Settings. Access the settings page of the user. See :ref:`the settings page <webadministration_settings>` for more information.

* Authentication tokens. Access the authentication tokens page of the user. See :ref:`the authentication tokens page <webadministration_authentication_tokens>` for more information.

* Auto-reply. Access the auto-reply page of the user. See the :ref:`auto-reply page <webadministration_auto-reply>` for more information.

* Fetched accounts. Access the fetched accounts page of the user. See the :ref:`fetched accounts page <webadministration_fetched_accounts>` for more information.

* Webmail. Login to webmail as the respective user to access the user's mailbox.

This page also shows an overview of the following settings of an user:

* Email. The email address of the user.
Expand Down
1 change: 1 addition & 0 deletions towncrier/newsfragments/3106.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add impersonation feature for admins to webmail
6 changes: 1 addition & 5 deletions webmails/nginx-webmail.conf
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,7 @@ server {

fastcgi_pass unix:/var/run/php8-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
{% if WEB_WEBMAIL == '/' %}
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
{% else %}
fastcgi_param SCRIPT_NAME {{WEB_WEBMAIL}}/$fastcgi_script_name;
{% endif %}
fastcgi_param SCRIPT_NAME $http_x_remote_base_url$fastcgi_script_name;
}

location ~ (^|/)\. {
Expand Down
2 changes: 1 addition & 1 deletion webmails/roundcube/config/config.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
$config['spellcheck_engine'] = 'pspell';
$config['spellcheck_languages'] = array('en'=>'English (US)', 'uk'=>'English (UK)', 'de'=>'Deutsch', 'fr'=>'French', 'ru'=>'Russian');
$config['session_lifetime'] = {{ (((PERMANENT_SESSION_LIFETIME | default(10800)) | int)/3600) | int }};
$config['request_path'] = '{{ WEB_WEBMAIL or "none" }}';
$config['request_path'] = $_SERVER['HTTP_X_REMOTE_BASE_URL'];
$config['trusted_host_patterns'] = [ {{ HOSTNAMES.split(",") | map("tojson") | join(',') }}];

{% if (FULL_TEXT_SEARCH or '').lower() not in ['off', 'false', '0'] %}
Expand Down
2 changes: 2 additions & 0 deletions webmails/roundcube/login/localization/en_US.inc
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<?php
$labels = [];
$labels['mailu'] = 'Mailu';

$messages['impersonationwarning'] = 'You are currently logged in as <strong>$email</strong>.';
?>
22 changes: 21 additions & 1 deletion webmails/roundcube/login/mailu.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,27 @@ function startup($args)
);
}
// sso
if (empty($_SESSION['user_id'])) {
if (empty($_SESSION['username']) || (!empty($_SERVER['HTTP_X_REMOTE_USER']) && $_SESSION['username'] !== $_SERVER['HTTP_X_REMOTE_USER'])) {
$args['task'] = 'login';
$args['action'] = 'login';
}

if (!$rcmail->output->framed && !empty($_SESSION['mailu_original_username']) && $_SESSION['mailu_original_username'] !== $_SESSION['username']) {
$currentUser = $_SESSION['username'];
$originalUser = $_SESSION['mailu_original_username'];

$this->api->output->add_header(html::tag(
'div',
'boxwarning',
$this->gettext([
'name' => 'impersonationwarning',
'vars' => [
'email' => html::quote($currentUser),
],
]),
));
}

return $args;
}

Expand All @@ -55,6 +73,8 @@ function authenticate($args)
$args['user'] = $_SERVER['HTTP_X_REMOTE_USER'];
$args['pass'] = $_SERVER['HTTP_X_REMOTE_USER_TOKEN'];

$_SESSION['mailu_original_username'] = $_SERVER['HTTP_X_REMOTE_ORIGINAL_USER'];

$args['cookiecheck'] = false;
$args['valid'] = true;

Expand Down