Skip to content

Commit

Permalink
[MIG] impersonate_login: migration to V17
Browse files Browse the repository at this point in the history
  • Loading branch information
astirpe committed Aug 29, 2024
1 parent 744c8f3 commit e40698a
Show file tree
Hide file tree
Showing 22 changed files with 508 additions and 234 deletions.
37 changes: 29 additions & 8 deletions impersonate_login/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,23 +28,41 @@ Impersonate Login

|badge1| |badge2| |badge3| |badge4| |badge5|

This module allows to login as another user. In the chatter, the user
who is logged as another user is displayed. The mails and messages are
sent from the orignal user. A table diplays the impersonated logins in
technical. The user can return to his own user by clicking on the button
"Return to my user". This module is very useful for the support team. An
alternative module will be auth_admin_passkey.
This module allows one user (for example, a member of the support team)
to log in as another user. The impersonation session can be exited by
clicking on the button "Back to Original User".

To ensure that any abuse of this feature will not go unnoticed, the
following measures are in place:

- In the chatter, it is displayed who is the user that is logged as
another user.
- Mails and messages are sent from the original user.
- Impersonated logins are logged and can be consulted through the
Settings -> Technical menu.
-

There is an alternative module to allow logins as another user
(auth_admin_passkey), but it does not support these security mechanisms.

**Table of contents**

.. contents::
:local:

Configuration
=============

The impersonating user must belong to group "Impersonate Users".

Usage
=====

1. On the top right corner, click my user and "switch login"
2. Same place to "return to my login"
1. In the menu that is displayed when clicking on the user avatar on the
top right corner, or in the res.users list, click "Switch Login" to
impersonate another user.
2. On the top-right corner, the button "Back to Original User" is
displayed in case the current user is being impersonated.

Bug Tracker
===========
Expand All @@ -68,6 +86,9 @@ Contributors
------------

- Kévin Roche <[email protected]>
- `360ERP <https://www.360erp.com>`__:

- Andrea Stirpe

Maintainers
-----------
Expand Down
1 change: 1 addition & 0 deletions impersonate_login/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from . import models
from .hooks import pre_init_hook
16 changes: 9 additions & 7 deletions impersonate_login/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
{
"name": "Impersonate Login",
"summary": "tools",
"version": "14.0.1.0.0",
"version": "17.0.1.0.0",
"category": "Tools",
"website": "https://github.com/OCA/server-auth",
"author": "Akretion, Odoo Community Association (OCA)",
Expand All @@ -18,13 +18,15 @@
"mail",
],
"data": [
"views/assets.xml",
"views/res_users.xml",
"views/impersonate_log.xml",
"security/group.xml",
"security/ir.model.access.csv",
"views/res_users.xml",
"views/impersonate_log.xml",
],
"qweb": [
"static/src/xml/user_menu.xml",
],
"assets": {
"web.assets_backend": [
"impersonate_login/static/src/js/user_menu.esm.js",
],
},
"pre_init_hook": "pre_init_hook",
}
19 changes: 19 additions & 0 deletions impersonate_login/hooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright 2024 360ERP (<https://www.360erp.com>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

import logging


def pre_init_hook(env):
"""
Pre-create the impersonated_author_id column in the mail_message table
to prevent the ORM from invoking its compute method on a large volume
of existing mail messages.
"""
logger = logging.getLogger(__name__)
logger.info("Add mail_message.impersonated_author_id column if not exists")
env.cr.execute(
"ALTER TABLE mail_message "
"ADD COLUMN IF NOT EXISTS "
"impersonated_author_id INTEGER"
)
1 change: 0 additions & 1 deletion impersonate_login/models/impersonate_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ class ImpersonateLog(models.Model):

user_id = fields.Many2one(
comodel_name="res.partner",
string="User",
)
impersonated_partner_id = fields.Many2one(
comodel_name="res.partner",
Expand Down
6 changes: 4 additions & 2 deletions impersonate_login/models/mail_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@

from odoo import _, api, fields, models
from odoo.http import request
from odoo.tools import html_escape


class Message(models.Model):
_inherit = "mail.message"

impersonated_author_id = fields.Many2one(
comodel_name="res.partner",
string="Impersonated Author",
compute="_compute_impersonated_author_id",
store=True,
)
Expand Down Expand Up @@ -45,7 +45,9 @@ def _compute_message_body(self):
current_partner = (
self.env["res.users"].browse(request.session.uid).partner_id
)
additional_info = _(f"Logged as {current_partner.name}")
additional_info = _("Logged in as {}").format(
html_escape(current_partner.name)
)
if rec.body and additional_info:
rec.body = f"<b>{additional_info}</b><br/>{rec.body}"
else:
Expand Down
34 changes: 17 additions & 17 deletions impersonate_login/models/mail_thread.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,21 @@ class MailThread(models.AbstractModel):
_inherit = "mail.thread"

def _message_compute_author(
self, author_id=None, email_from=None, raise_exception=True
self, author_id=None, email_from=None, raise_on_email=True
):
if (
request
and request.session.impersonate_from_uid
and author_id in [request.session.uid, None]
):
author = (
self.env["res.users"]
.browse(request.session.impersonate_from_uid)
.partner_id
)
email = author.email_formatted
return author.id, email
else:
return super()._message_compute_author(
author_id, email_from, raise_exception
)
if request and request.session.impersonate_from_uid:
author = self.env["res.users"].browse(request.session.uid).partner_id
if author_id == author.id or author_id is None:
impersonate_from_author = (
self.env["res.users"]
.browse(request.session.impersonate_from_uid)
.partner_id
)
email = impersonate_from_author.email_formatted
return impersonate_from_author.id, email

return super()._message_compute_author(
author_id=author_id,
email_from=email_from,
raise_on_email=raise_on_email,
)
28 changes: 18 additions & 10 deletions impersonate_login/models/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,31 @@
# @author Kévin Roche <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from odoo import api, models
from odoo import models
from odoo.http import request


class BaseModel(models.AbstractModel):
_inherit = "base"

@api.model_create_multi
def _create(self, data_list):
res = super()._create(data_list)
if request and request.session.impersonate_from_uid:
for rec in res:
rec.create_uid = request.session.impersonate_from_uid
return res
def _prepare_create_values(self, vals_list):
result_vals_list = super()._prepare_create_values(vals_list)
if (
request
and request.session.impersonate_from_uid
and "create_uid" in self._fields
):
for vals in result_vals_list:
vals["create_uid"] = request.session.impersonate_from_uid
return result_vals_list

def write(self, vals):
"""Overwrite the write_uid with the impersonating user"""
res = super().write(vals)
if request and request.session.impersonate_from_uid:
self.write_uid = request.session.impersonate_from_uid
if (
request
and request.session.impersonate_from_uid
and "write_uid" in self._fields
):
self._fields["write_uid"].write(self, request.session.impersonate_from_uid)
return res
49 changes: 41 additions & 8 deletions impersonate_login/models/res_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,16 @@ def _is_impersonate_user(self):
def impersonate_login(self):
if request:
if request.session.impersonate_from_uid:
raise UserError(_("You are already Logged as another user."))
if self.id == request.session.impersonate_from_uid:
return self.back_to_origin_login()
else:
raise UserError(_("You are already Logged as another user."))
if self.id == request.session.uid:
raise UserError(_("It's you."))
if request.env.user._is_impersonate_user():
if (
request.env.user._is_impersonate_user()
and request.env.user._is_internal()
):
target_uid = self.id
request.session.impersonate_from_uid = self._uid
request.session.uid = target_uid
Expand All @@ -37,9 +43,7 @@ def impersonate_login(self):
.sudo()
.create(
{
"user_id": self.env["res.users"]
.browse(self._uid)
.partner_id.id,
"user_id": self._uid,
"impersonated_partner_id": self.env["res.users"]
.browse(target_uid)
.partner_id.id,
Expand All @@ -52,16 +56,36 @@ def impersonate_login(self):
f"IMPERSONATE: {self._get_partner_name(self._uid)} "
f"Login as {self._get_partner_name(self.id)}"
)

request.env["res.users"].clear_caches()
# invalidate session token cache as we've changed the uid
request.env.registry.clear_cache()
request.session.session_token = security.compute_session_token(
request.session, request.env
)

# reload the client; open the first available root menu
menu = self.env["ir.ui.menu"].search([("parent_id", "=", False)])[:1]
return {
"type": "ir.actions.client",
"tag": "reload",
"params": {"menu_id": menu.id},
}

@api.model
def action_impersonate_login(self):
if request:
from_uid = request.session.impersonate_from_uid
if not from_uid:
action = self.env["ir.actions.act_window"]._for_xml_id(
"base.action_res_users"
)
action["views"] = [[self.env.ref("base.view_users_tree").id, "tree"]]
action["domain"] = [
("id", "!=", self.env.user.id),
("share", "=", False),
]
action["target"] = "new"
return action

@api.model
def back_to_origin_login(self):
if request:
Expand All @@ -75,7 +99,8 @@ def back_to_origin_login(self):
"date_end": fields.datetime.now(),
}
)
request.env["res.users"].clear_caches()
# invalidate session token cache as we've changed the uid
request.env.registry.clear_cache()
request.session.impersonate_from_uid = False
request.session.impersonate_log_id = False
request.session.session_token = security.compute_session_token(
Expand All @@ -85,3 +110,11 @@ def back_to_origin_login(self):
f"IMPERSONATE: {self._get_partner_name(from_uid)} "
f"Logout as {self._get_partner_name(self._uid)}"
)

# reload the client; open the first available root menu
menu = self.env["ir.ui.menu"].search([("parent_id", "=", False)])[:1]
return {
"type": "ir.actions.client",
"tag": "reload",
"params": {"menu_id": menu.id},
}
1 change: 1 addition & 0 deletions impersonate_login/readme/CONFIGURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The impersonating user must belong to group "Impersonate Users".
2 changes: 2 additions & 0 deletions impersonate_login/readme/CONTRIBUTORS.md
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
- Kévin Roche \<<[email protected]>\>
- [360ERP](https://www.360erp.com):
- Andrea Stirpe
17 changes: 11 additions & 6 deletions impersonate_login/readme/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
This module allows to login as another user. In the chatter, the user
who is logged as another user is displayed. The mails and messages are
sent from the orignal user. A table diplays the impersonated logins in
technical. The user can return to his own user by clicking on the button
"Return to my user". This module is very useful for the support team. An
alternative module will be auth_admin_passkey.
This module allows one user (for example, a member of the support team) to log in as another user.
The impersonation session can be exited by clicking on the button "Back to Original User".

To ensure that any abuse of this feature will not go unnoticed, the following measures are in place:

* In the chatter, it is displayed who is the user that is logged as another user.
* Mails and messages are sent from the original user.
* Impersonated logins are logged and can be consulted through the Settings -> Technical menu.
*
There is an alternative module to allow logins as another user (auth_admin_passkey),
but it does not support these security mechanisms.
6 changes: 4 additions & 2 deletions impersonate_login/readme/USAGE.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
1. On the top right corner, click my user and "switch login"
2. Same place to "return to my login"
1. In the menu that is displayed when clicking on the user avatar on the top right corner,
or in the res.users list, click "Switch Login" to impersonate another user.
2. On the top-right corner, the button "Back to Original User" is displayed in case the current
user is being impersonated.
Loading

0 comments on commit e40698a

Please sign in to comment.