diff --git a/plain-admin/plain/admin/README.md b/plain-admin/plain/admin/README.md
index 72a174400a..c28c684ed9 100644
--- a/plain-admin/plain/admin/README.md
+++ b/plain-admin/plain/admin/README.md
@@ -480,6 +480,8 @@ The impersonate feature lets admin users log in as another user to debug issues
To start impersonating, visit a user's detail page in the admin and click the "Impersonate" link. The admin toolbar will show who you're impersonating and provide a link to stop.
+Inside the admin itself, the account menu in the top right keeps showing *your* avatar while you impersonate — it acts on your account, not the impersonated user's — and it lists who you're impersonating with a "Stop impersonating" item.
+
By default, users with `is_admin=True` can impersonate other users. Admin users cannot be impersonated (for security). You can customize who can impersonate by defining `IMPERSONATE_ALLOWED` in your settings:
```python
diff --git a/plain-admin/plain/admin/templates/admin/_header.html b/plain-admin/plain/admin/templates/admin/_header.html
index 3505b71100..81e09ee2bd 100644
--- a/plain-admin/plain/admin/templates/admin/_header.html
+++ b/plain-admin/plain/admin/templates/admin/_header.html
@@ -55,11 +55,15 @@
aria-controls="user-menu-popover"
class="flex items-center cursor-pointer"
>
- {% set avatar_url = user.get_avatar_url() if user.get_avatar_url is defined else None %}
+ {# While impersonating, this menu still acts on the real
+ account (App Settings, Log out), so it shows the
+ impersonator — not the user being impersonated. #}
+ {% set account_user = impersonator or user %}
+ {% set avatar_url = account_user.get_avatar_url() if account_user.get_avatar_url is defined else None %}
{% if avatar_url %}
{% else %}
@@ -90,6 +94,16 @@
{% endif %}
+ {% if impersonator %}
+
+ Impersonating {{ user }}
+
+
+
+ Stop impersonating
+
+
+ {% endif %}
{% include "admin/user_menu_items.html" ignore missing %}
diff --git a/plain-admin/plain/admin/views/base.py b/plain-admin/plain/admin/views/base.py
index f221ecad50..c07a07a823 100644
--- a/plain-admin/plain/admin/views/base.py
+++ b/plain-admin/plain/admin/views/base.py
@@ -11,6 +11,7 @@
from plain.urls import reverse
from plain.utils import timezone
+from ..impersonate import get_request_impersonator
from ..models import PinnedNavItem
from .registry import registry, track_recent_nav
from .types import Img
@@ -118,6 +119,9 @@ def get_template_context(self) -> dict[str, Any]:
)
)
context["preflight_counts"] = get_check_counts()
+ # The real, logged-in user when impersonation is active (None otherwise).
+ # The header's account menu belongs to *them*, not the impersonated user.
+ context["impersonator"] = get_request_impersonator(self.request)
context["admin_url"] = registry.get_url
return context
diff --git a/plain-admin/tests/app/users/models.py b/plain-admin/tests/app/users/models.py
index b2b083d55b..4c3e03c883 100644
--- a/plain-admin/tests/app/users/models.py
+++ b/plain-admin/tests/app/users/models.py
@@ -12,6 +12,10 @@ class User(postgres.Model):
query: postgres.QuerySet[User] = postgres.QuerySet()
+ def get_avatar_url(self) -> str:
+ """The admin header renders this when the user model provides it."""
+ return f"https://avatars.example.com/{self.username}.png"
+
@property
def username_upper(self) -> str:
"""A computed (non-column) field, to exercise in-memory sorting."""
diff --git a/plain-admin/tests/public/test_impersonate.py b/plain-admin/tests/public/test_impersonate.py
index fe80a6ca8a..8e70ab9b65 100644
--- a/plain-admin/tests/public/test_impersonate.py
+++ b/plain-admin/tests/public/test_impersonate.py
@@ -72,3 +72,24 @@ def test_admin_users_cannot_be_impersonated(db):
# After the refusal the marker is cleared, so normal requests resume.
assert client.get("/whoami").user.id == admin.id
+
+
+def test_admin_header_shows_the_impersonator_not_the_impersonated_user(db):
+ """The account menu acts on the real account, so it shows that avatar."""
+ admin = User.query.create(username="admin", is_admin=True)
+ target = User.query.create(username="target", is_admin=False)
+
+ client = Client()
+ client.force_login(admin)
+ client.get(f"/admin/impersonate/start/{target.id}")
+
+ # Land on a real admin page (the index redirects to the first list view).
+ admin_page = client.get(client.get("/admin").url)
+ assert admin_page.status_code == 200
+
+ content = admin_page.content.decode()
+ assert "https://avatars.example.com/admin.png" in content
+ assert "https://avatars.example.com/target.png" not in content
+
+ # ...and the menu says who is being impersonated, with a way out.
+ assert "Stop impersonating" in content