karni-impersonation
"Log in as user" for support, done safely, for Karni projects. A support person with the explicit permission acts as a customer to see what they see — with a banner on every page, a session record, both identities in the audit trail, a time limit and a hard block on dangerous actions.
Install
pip install karni-impersonation
karni add impersonation
karni migrate
Then wire it twice (the module cannot do this for you; karni check fails
with IMP007 until both are in place):
# config/settings.py — right after AuthenticationMiddleware
MIDDLEWARE.insert(
MIDDLEWARE.index("django.contrib.auth.middleware.AuthenticationMiddleware") + 1,
"karni_impersonation.middleware.ImpersonationMiddleware",
)
# config/urls.py — the banner's exit
path("impersonation/", include("karni_impersonation.urls")),
Grant the permission to the role that does support:
KARNI_ROLES = {
...
"support": {"grants": {"karni_impersonation.impersonationsession:impersonate": "all", ...}},
}
Keep the Celery worker and beat running: the target's notification is a task and an hourly sweep closes abandoned sessions.
Threat model
What it allows. A user holding karni_impersonation.impersonationsession:impersonate
(the actor) acts as another user (the target) for at most
KARNI_IMPERSONATION_MAX_MINUTES (60): every request is evaluated as the
target — their roles, their row scope, their field visibility — through the
admin (session auth) or the API (session or JWT). The actor sees what the
target sees and may change what the target may change, minus the blocked
list.
What it blocks.
- Starting without the permission (
IMP001). Staff and superuser status never imply it; only a role with the grant (superusers hold every grant by Karni's convention — treat superuser accounts accordingly). - Privilege escalation (
IMP002). The target may not be a superuser or staff unless the actor is, may not hold any role grant at a scope the actor does not hold (allcovers all), and may not hold any Django permission the actor lacks. Checked inservices.start, before anything is written, whoever calls it. - Impersonating yourself, an inactive account, or starting a second
session (
IMP004); starting without a reason whenKARNI_IMPERSONATION_REQUIRE_REASONis on (IMP003, the default). - Dangerous actions during the session (
IMP006), whatever the target may do:KARNI_IMPERSONATION_BLOCKEDlists viewset and admin action names,app.modellabels,app.model:actionpermission keys and URL paths; by default deleting (destroy,*:delete,delete_selected, …), exporting and importing, everything inkarni_payments, and the credential paths (/api/auth/password/*,/api/auth/email/*,/api/auth/2fa/*,/api/auth/register/). Enforced through Karni's veto registry at the API gate and the admin's permissions and action list, and by the middleware for paths — not in a UI. - Outliving the limit. In session mode the middleware ends the session
on the first request past
expires_at; in JWT mode the tokens issued for the session expire with it and the middleware refuses a token whose session has ended. The hourly sweep closes what no request touched. - Silent use. The target is emailed on start (
KARNI_IMPERSONATION_NOTIFY_TARGET, default on).
What it logs. An ImpersonationSession row per session: actor, target,
reason, start, limit, end and why it ended (stopped, expired, logout,
swept), IP and user agent. The row is itself audited (karni.audit). At
start the module opens a karni.audit ChangeBatch of kind
impersonation whose actor is the impersonator; every request of the
session runs inside it, so every change made while impersonating has
actor = the target (the row's history) and batch → the
impersonator — both identities, on every write.
How to tell after the fact whether it was used.
- Admin → Karni impersonation → Impersonation sessions (read-only; nobody can delete a record through the admin or the API).
ImpersonationSession.objects.filter(target=user)— who acted as this user, when, why.ChangeLog.objects.filter(batch__kind="impersonation")— every change made under impersonation;.filter(batch__actor=support_user)— by whom; a row's history showsbatchon each entry.ImpersonationSession.objects.filter(ended_at__isnull=True)— sessions open right now.
What it does not do. It does not hide the actor: the banner names both
people on every page and GET /api/impersonation-sessions/current/ says so
to a SPA. It does not grant the actor anything the target lacks, and it
does not grant the target anything. It cannot block an action a project
exposes outside Karni's gates (a plain Django view that writes); list its
path in KARNI_IMPERSONATION_BLOCKED.
Where it shows
| Surface | What |
|---|---|
| Admin → user change page → Impersonate (django-unfold) / Impersonation sessions → Impersonate a user | the reason form, then the site as the target with the banner |
| the banner (every HTML page, session mode) | both identities, minutes left, Stop impersonating |
POST /api/impersonation-sessions/start/ {"user": <id or email>, "reason"} |
the session; JWT mode adds access/refresh tokens for the target, bounded by the limit |
POST /api/impersonation-sessions/stop/ |
ends it; JWT mode returns fresh tokens for the actor |
GET /api/impersonation-sessions/current/ |
the session this request belongs to, or null |
GET /api/impersonation-sessions/ |
the log (needs the view grant; an actor sees their own) |
@karni/impersonation-ui |
<ImpersonationBanner/> and useImpersonation() for SPAs |
Settings
| Setting | Env var | Default | Meaning |
|---|---|---|---|
KARNI_IMPERSONATION_MAX_MINUTES |
same | 60 |
hard limit on a session |
KARNI_IMPERSONATION_BLOCKED |
same (comma list) | deletes, exports/imports, karni_payments.*, credential paths |
what is blocked while impersonating |
KARNI_IMPERSONATION_REQUIRE_REASON |
same | True |
a reason is required to start |
KARNI_IMPERSONATION_NOTIFY_TARGET |
same | True |
email the impersonated user on start |
KARNI_IMPERSONATION_FROM_EMAIL |
same | "" |
sender; "" uses DEFAULT_FROM_EMAIL |
KARNI_IMPERSONATION_BANNER |
same | True |
inject the banner into HTML pages |
KARNI_IMPERSONATION_LANDING |
same | / |
where the admin's start page sends the impersonator |
Escape hatches
- Every setting above;
KARNI_IMPERSONATION_BLOCKEDnarrows or widens what is blocked per project (never empty in production). services.start,stop,can_impersonate,privilege_gap,is_blocked,sweepare plain functions for your own views and tasks;karni_impersonation.middleware.current_session(request)tells a view whether it runs impersonated.- The banner template
karni_impersonation/banner.htmland the start pagekarni_impersonation/start.htmlcan be overridden in the project's templates. services.find_userresolves the start form's input; subclass the viewset (auto_register = False) to change the API.- This module ships an explicit
admin.py(the session log and the start page) and, when the user admin is a django-unfold one, adds an Impersonate button to it.
Metadata
Release files for karni-impersonation 0.3.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| karni_impersonation-0.3.1.tar.gz | 24.7 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| karni_impersonation-0.3.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 58.5 kB
Release files / karni_impersonation-0.3.1.tar.gz
| Download URL | karni_impersonation-0.3.1.tar.gz |
|---|---|
| Size | 24.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
42b14dba4f6719ad0a56f0c46d2fce554c91dde234a2a7a873f7542d812f7429
|
|
BLAKE2b-256 checksum How to use checksums |
9d8c1a6da539b3c4b6e951bb4f403d162478db33551e434f097eb21e9c25e153
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Release files / karni_impersonation-0.3.1-py3-none-any.whl
| Download URL | karni_impersonation-0.3.1-py3-none-any.whl |
|---|---|
| Size | 33.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6a5ec30cb0af19c9f1c62f4c4e04e38759d4ca98bc85764d4e7d914070dfa205
|
|
BLAKE2b-256 checksum How to use checksums |
efa7268fbf62c35188d4973ca22d2f27cfaf13329561008572438ab524de097f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Security for karni-impersonation 0.3.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.