Skip to content

Commit ca0e948

Browse files
committed
fix: impersonation is now a POST action
1 parent a068f48 commit ca0e948

8 files changed

Lines changed: 372 additions & 261 deletions

File tree

CHANGES.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
[0.3.7] - Unreleased
2+
--------------------
3+
4+
Fixed
5+
^^^^^
6+
- User impersonation was a ``GET`` request, thus not covered by the CSRF protection. It is now confirmed with a form, and the ``/impersonate/<user>`` endpoint is removed.
7+
18
[0.3.6] - 2026-08-04
29
--------------------
310

canaille/core/endpoints/account/__init__.py

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -801,6 +801,27 @@ def _handle_lock_actions(user, edited_user, action):
801801
return _handle_profile_settings_edit(user, edited_user)
802802

803803

804+
def _handle_impersonate_actions(user, edited_user, action):
805+
if not user.can_impersonate_users or edited_user.id == user.id:
806+
abort(403)
807+
808+
if edited_user.locked:
809+
abort(403, _("Locked users cannot be impersonated."))
810+
811+
if action == "impersonate-confirm":
812+
return render_template(
813+
"core/modals/impersonate-user.html", edited_user=edited_user
814+
)
815+
816+
login_user(edited_user, remember=False)
817+
current_app.logger.security(f"User {user.id} impersonated {edited_user.id}")
818+
flash(
819+
_("Connection successful. Welcome %(user)s", user=edited_user.name),
820+
"success",
821+
)
822+
return redirect(url_for("core.account.index"))
823+
824+
804825
@bp.route("/profile/<user:edited_user>/settings", methods=("GET", "POST"))
805826
@user_needed()
806827
def profile_settings(user, edited_user):
@@ -828,6 +849,9 @@ def profile_settings(user, edited_user):
828849
):
829850
return _handle_lock_actions(user, edited_user, action)
830851

852+
if action in ("impersonate-confirm", "impersonate-execute"):
853+
return _handle_impersonate_actions(user, edited_user, action)
854+
831855
abort(400, f"bad form action: {action}")
832856

833857

@@ -886,21 +910,6 @@ def _handle_profile_settings_edit(editor, edited_user):
886910
)
887911

888912

889-
@bp.route("/impersonate/<user:puppet>")
890-
@user_needed("impersonate_users")
891-
def impersonate(user, puppet):
892-
if puppet.locked:
893-
abort(403, _("Locked users cannot be impersonated."))
894-
895-
login_user(puppet, remember=False)
896-
current_app.logger.security(f"User {user.id} impersonated {puppet.id}")
897-
flash(
898-
_("Connection successful. Welcome %(user)s", user=puppet.name),
899-
"success",
900-
)
901-
return redirect(url_for("core.account.index"))
902-
903-
904913
@bp.app_template_filter()
905914
def photo_url(user, external=False):
906915
serializer = URLSafeSerializer(current_app.config["SECRET_KEY"], salt="photo")

canaille/templates/core/account/settings.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,9 @@ <h2 class="ui center aligned header">
114114
{% endif %}
115115

116116
{% if user.can_impersonate_users and user.identifier != edited_user.identifier and not edited_user.locked %}
117-
<a href="{{ url_for('core.account.impersonate', puppet=edited_user) }}" class="ui right floated basic button" name="action" value="impersonate" id="impersonate" hx-boost="false">
117+
<button type="submit" class="ui right floated basic button confirm" name="action" value="impersonate-confirm" id="impersonate" formnovalidate>
118118
{{ _("Impersonate") }}
119-
</a>
119+
</button>
120120
{% endif %}
121121

122122
<button type="submit" class="ui right floated primary button" name="action" value="edit-settings" id="edit-settings">
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{% extends theme('base.html') %}
2+
3+
{% block content %}
4+
<div id="modal-impersonate" class="ui info message">
5+
<form method="post" action="{{ request.url }}">
6+
<input type="hidden" name="csrf_token" value="{{ request.form.get("csrf_token") }}">
7+
<div class="ui icon header">
8+
<i class="mask icon"></i>
9+
{% trans %}Account impersonation{% endtrans %}
10+
</div>
11+
<div class="content">
12+
<p>
13+
{% trans user_name=(edited_user.formatted_name or edited_user.identifier) %}
14+
Are you sure you want to open a session as {{ user_name }}? Every action you will perform will be attributed to them. Log out to get back to your own session.
15+
{% endtrans %}
16+
</p>
17+
</div>
18+
<div class="ui center aligned container">
19+
<div class="ui stackable buttons">
20+
<a class="ui cancel button" href="{{ request.url }}">{% trans %}Cancel{% endtrans %}</a>
21+
<button type="submit" name="action" value="impersonate-execute" class="ui primary approve button">{% trans %}Impersonate{% endtrans %}</button>
22+
</div>
23+
</div>
24+
</form>
25+
</div>
26+
{% endblock %}

0 commit comments

Comments
 (0)