From dbbec1902a714a78cee20db9e9f002dae0e3e2f7 Mon Sep 17 00:00:00 2001 From: Kev-Roche Date: Tue, 14 May 2024 09:54:25 +0200 Subject: [PATCH 1/3] [14.0][ADD] impersonate_login --- impersonate_login/README.rst | 96 ++++ impersonate_login/__init__.py | 1 + impersonate_login/__manifest__.py | 30 ++ impersonate_login/models/__init__.py | 6 + impersonate_login/models/impersonate_log.py | 26 ++ impersonate_login/models/ir_http.py | 20 + impersonate_login/models/mail_message.py | 52 +++ impersonate_login/models/mail_thread.py | 30 ++ impersonate_login/models/model.py | 24 + impersonate_login/models/res_users.py | 87 ++++ impersonate_login/readme/CONTRIBUTORS.rst | 1 + impersonate_login/readme/DESCRIPTION.rst | 7 + impersonate_login/readme/USAGE.rst | 2 + impersonate_login/security/group.xml | 13 + .../security/ir.model.access.csv | 2 + .../static/description/index.html | 437 ++++++++++++++++++ impersonate_login/static/src/css/navbar.scss | 23 + .../static/src/js/abstract_web_client.js | 18 + impersonate_login/static/src/js/user_menu.js | 66 +++ .../static/src/xml/user_menu.xml | 22 + impersonate_login/views/assets.xml | 24 + impersonate_login/views/impersonate_log.xml | 36 ++ impersonate_login/views/res_users.xml | 22 + 23 files changed, 1045 insertions(+) create mode 100644 impersonate_login/README.rst create mode 100644 impersonate_login/__init__.py create mode 100644 impersonate_login/__manifest__.py create mode 100644 impersonate_login/models/__init__.py create mode 100644 impersonate_login/models/impersonate_log.py create mode 100644 impersonate_login/models/ir_http.py create mode 100644 impersonate_login/models/mail_message.py create mode 100644 impersonate_login/models/mail_thread.py create mode 100644 impersonate_login/models/model.py create mode 100644 impersonate_login/models/res_users.py create mode 100644 impersonate_login/readme/CONTRIBUTORS.rst create mode 100644 impersonate_login/readme/DESCRIPTION.rst create mode 100644 impersonate_login/readme/USAGE.rst create mode 100644 impersonate_login/security/group.xml create mode 100644 impersonate_login/security/ir.model.access.csv create mode 100644 impersonate_login/static/description/index.html create mode 100644 impersonate_login/static/src/css/navbar.scss create mode 100644 impersonate_login/static/src/js/abstract_web_client.js create mode 100644 impersonate_login/static/src/js/user_menu.js create mode 100644 impersonate_login/static/src/xml/user_menu.xml create mode 100644 impersonate_login/views/assets.xml create mode 100644 impersonate_login/views/impersonate_log.xml create mode 100644 impersonate_login/views/res_users.xml diff --git a/impersonate_login/README.rst b/impersonate_login/README.rst new file mode 100644 index 0000000000..5df334246f --- /dev/null +++ b/impersonate_login/README.rst @@ -0,0 +1,96 @@ +================= +Impersonate Login +================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:1fca331cbc5f2dcb804e5612e5669a9ab4998d80f22d46d6683266580f9ca40f + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--auth-lightgray.png?logo=github + :target: https://github.com/OCA/server-auth/tree/14.0/impersonate_login + :alt: OCA/server-auth +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/server-auth-14-0/server-auth-14-0-impersonate_login + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/server-auth&target_branch=14.0 + :alt: Try me on Runboat + +|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. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +1. On the top right corner, click my user and "switch login" +2. Same place to "return to my login" + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Akretion + +Contributors +~~~~~~~~~~~~ + +* Kévin Roche + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-Kev-Roche| image:: https://github.com/Kev-Roche.png?size=40px + :target: https://github.com/Kev-Roche + :alt: Kev-Roche + +Current `maintainer `__: + +|maintainer-Kev-Roche| + +This module is part of the `OCA/server-auth `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/impersonate_login/__init__.py b/impersonate_login/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/impersonate_login/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/impersonate_login/__manifest__.py b/impersonate_login/__manifest__.py new file mode 100644 index 0000000000..61c00eb90e --- /dev/null +++ b/impersonate_login/__manifest__.py @@ -0,0 +1,30 @@ +# Copyright 2024 Akretion (https://www.akretion.com). +# @author Kévin Roche +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Impersonate Login", + "summary": "tools", + "version": "14.0.1.0.0", + "category": "Tools", + "website": "https://github.com/OCA/server-auth", + "author": "Akretion, Odoo Community Association (OCA)", + "maintainers": ["Kev-Roche"], + "license": "AGPL-3", + "application": False, + "installable": True, + "depends": [ + "web", + "mail", + ], + "data": [ + "views/assets.xml", + "views/res_users.xml", + "views/impersonate_log.xml", + "security/group.xml", + "security/ir.model.access.csv", + ], + "qweb": [ + "static/src/xml/user_menu.xml", + ], +} diff --git a/impersonate_login/models/__init__.py b/impersonate_login/models/__init__.py new file mode 100644 index 0000000000..debb66e9c1 --- /dev/null +++ b/impersonate_login/models/__init__.py @@ -0,0 +1,6 @@ +from . import res_users +from . import ir_http +from . import mail_thread +from . import mail_message +from . import impersonate_log +from . import model diff --git a/impersonate_login/models/impersonate_log.py b/impersonate_login/models/impersonate_log.py new file mode 100644 index 0000000000..31ab131f63 --- /dev/null +++ b/impersonate_login/models/impersonate_log.py @@ -0,0 +1,26 @@ +# Copyright (C) 2024 Akretion (). +# @author Kévin Roche +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +from odoo import fields, models + + +class ImpersonateLog(models.Model): + _name = "impersonate.log" + _description = "Impersonate Logs" + + user_id = fields.Many2one( + comodel_name="res.partner", + string="User", + ) + impersonated_partner_id = fields.Many2one( + comodel_name="res.partner", + string="Logged as", + ) + date_start = fields.Datetime( + string="Start Date", + ) + date_end = fields.Datetime( + string="End Date", + ) diff --git a/impersonate_login/models/ir_http.py b/impersonate_login/models/ir_http.py new file mode 100644 index 0000000000..f01aa613d2 --- /dev/null +++ b/impersonate_login/models/ir_http.py @@ -0,0 +1,20 @@ +# Copyright (C) 2024 Akretion (). +# @author Kévin Roche +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models +from odoo.http import request + + +class Http(models.AbstractModel): + _inherit = "ir.http" + + def session_info(self): + session_info = super().session_info() + session_info.update( + { + "is_impersonate_user": request.env.user._is_impersonate_user(), + "impersonate_from_uid": request.session.impersonate_from_uid, + } + ) + return session_info diff --git a/impersonate_login/models/mail_message.py b/impersonate_login/models/mail_message.py new file mode 100644 index 0000000000..332d117ee3 --- /dev/null +++ b/impersonate_login/models/mail_message.py @@ -0,0 +1,52 @@ +# Copyright (C) 2024 Akretion (). +# @author Kévin Roche +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.http import request + + +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, + ) + + body = fields.Html( + compute="_compute_message_body", + store=True, + ) + + @api.depends("author_id") + def _compute_impersonated_author_id(self): + for rec in self: + if request and request.session.impersonate_from_uid: + rec.impersonated_author_id = ( + self.env["res.users"] + .browse(request.session.impersonate_from_uid) + .partner_id.id + ) + else: + rec.impersonated_author_id = False + + @api.depends("author_id", "impersonated_author_id") + def _compute_message_body(self): + for rec in self: + additional_info = "" + if ( + request + and request.session.impersonate_from_uid + and rec.impersonated_author_id + ): + current_partner = ( + self.env["res.users"].browse(request.session.uid).partner_id + ) + additional_info = _(f"Logged as {current_partner.name}") + if rec.body and additional_info: + rec.body = f"{additional_info}
{rec.body}" + else: + rec.body = additional_info diff --git a/impersonate_login/models/mail_thread.py b/impersonate_login/models/mail_thread.py new file mode 100644 index 0000000000..1c1f80da83 --- /dev/null +++ b/impersonate_login/models/mail_thread.py @@ -0,0 +1,30 @@ +# Copyright (C) 2024 Akretion (). +# @author Kévin Roche +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models +from odoo.http import request + + +class MailThread(models.AbstractModel): + _inherit = "mail.thread" + + def _message_compute_author( + self, author_id=None, email_from=None, raise_exception=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 + ) diff --git a/impersonate_login/models/model.py b/impersonate_login/models/model.py new file mode 100644 index 0000000000..61f6e6655d --- /dev/null +++ b/impersonate_login/models/model.py @@ -0,0 +1,24 @@ +# Copyright (C) 2024 Akretion (). +# @author Kévin Roche +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, 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 write(self, vals): + res = super().write(vals) + if request and request.session.impersonate_from_uid: + self.write_uid = request.session.impersonate_from_uid + return res diff --git a/impersonate_login/models/res_users.py b/impersonate_login/models/res_users.py new file mode 100644 index 0000000000..f7e8cea4b0 --- /dev/null +++ b/impersonate_login/models/res_users.py @@ -0,0 +1,87 @@ +# Copyright 2024 Akretion (https://www.akretion.com). +# @author Kévin Roche +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import logging + +from odoo import _, api, fields, models +from odoo.exceptions import UserError +from odoo.http import request +from odoo.service import security + +logger = logging.getLogger(__name__) + + +class Users(models.Model): + _inherit = "res.users" + + def _get_partner_name(self, user_id): + return self.env["res.users"].browse(user_id).partner_id.name + + def _is_impersonate_user(self): + self.ensure_one() + return self.has_group("impersonate_login.group_impersonate_login") + + 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.uid: + raise UserError(_("It's you.")) + if request.env.user._is_impersonate_user(): + target_uid = self.id + request.session.impersonate_from_uid = self._uid + request.session.uid = target_uid + impersonate_log = ( + self.env["impersonate.log"] + .sudo() + .create( + { + "user_id": self.env["res.users"] + .browse(self._uid) + .partner_id.id, + "impersonated_partner_id": self.env["res.users"] + .browse(target_uid) + .partner_id.id, + "date_start": fields.datetime.now(), + } + ) + ) + request.session.impersonate_log_id = impersonate_log.id + logger.info( + f"IMPERSONATE: {self._get_partner_name(self._uid)} " + f"Login as {self._get_partner_name(self.id)}" + ) + + request.env["res.users"].clear_caches() + request.session.session_token = security.compute_session_token( + request.session, request.env + ) + return { + "type": "ir.actions.client", + "tag": "reload", + } + + @api.model + def back_to_origin_login(self): + if request: + from_uid = request.session.impersonate_from_uid + if from_uid: + request.session.uid = from_uid + self.env["impersonate.log"].sudo().browse( + request.session.impersonate_log_id + ).write( + { + "date_end": fields.datetime.now(), + } + ) + request.env["res.users"].clear_caches() + request.session.impersonate_from_uid = False + request.session.impersonate_log_id = False + request.session.session_token = security.compute_session_token( + request.session, request.env + ) + logger.info( + f"IMPERSONATE: {self._get_partner_name(from_uid)} " + f"Logout as {self._get_partner_name(self._uid)}" + ) diff --git a/impersonate_login/readme/CONTRIBUTORS.rst b/impersonate_login/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..dcae277c8c --- /dev/null +++ b/impersonate_login/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Kévin Roche diff --git a/impersonate_login/readme/DESCRIPTION.rst b/impersonate_login/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..7ffc13f484 --- /dev/null +++ b/impersonate_login/readme/DESCRIPTION.rst @@ -0,0 +1,7 @@ +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. diff --git a/impersonate_login/readme/USAGE.rst b/impersonate_login/readme/USAGE.rst new file mode 100644 index 0000000000..d0109247f9 --- /dev/null +++ b/impersonate_login/readme/USAGE.rst @@ -0,0 +1,2 @@ +1. On the top right corner, click my user and "switch login" +2. Same place to "return to my login" diff --git a/impersonate_login/security/group.xml b/impersonate_login/security/group.xml new file mode 100644 index 0000000000..e996175066 --- /dev/null +++ b/impersonate_login/security/group.xml @@ -0,0 +1,13 @@ + + + + + Impersonate Users + + + diff --git a/impersonate_login/security/ir.model.access.csv b/impersonate_login/security/ir.model.access.csv new file mode 100644 index 0000000000..3a5c10c53d --- /dev/null +++ b/impersonate_login/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_impersonate_log,impersonate logs,model_impersonate_log,base.group_user,1,1,0,0 diff --git a/impersonate_login/static/description/index.html b/impersonate_login/static/description/index.html new file mode 100644 index 0000000000..f56e86fcd8 --- /dev/null +++ b/impersonate_login/static/description/index.html @@ -0,0 +1,437 @@ + + + + + + +Impersonate Login + + + +
+

Impersonate Login

+ + +

Beta License: AGPL-3 OCA/server-auth Translate me on Weblate Try me on Runboat

+

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.

+

Table of contents

+ +
+

Usage

+
    +
  1. On the top right corner, click my user and “switch login”
  2. +
  3. Same place to “return to my login”
  4. +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Akretion
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

Kev-Roche

+

This module is part of the OCA/server-auth project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/impersonate_login/static/src/css/navbar.scss b/impersonate_login/static/src/css/navbar.scss new file mode 100644 index 0000000000..556a38a5ee --- /dev/null +++ b/impersonate_login/static/src/css/navbar.scss @@ -0,0 +1,23 @@ +body.o_is_impersonated .o_menu_systray { + background: repeating-linear-gradient( + 135deg, + #32d804, + #32d804 10px, + #373435 10px, + #373435 20px + ); + border-bottom-left-radius: 20px; + + > li { + > a, + > label { + &:hover { + background-color: fade_out($o-navbar-inverse-link-hover-bg, 0.5); + } + } + } + + .show .dropdown-toggle { + background-color: fade_out($o-navbar-inverse-link-hover-bg, 0.5); + } +} diff --git a/impersonate_login/static/src/js/abstract_web_client.js b/impersonate_login/static/src/js/abstract_web_client.js new file mode 100644 index 0000000000..1c3ef6d444 --- /dev/null +++ b/impersonate_login/static/src/js/abstract_web_client.js @@ -0,0 +1,18 @@ +// Copyright 2024 Akretion (https://www.akretion.com). +// @author Kévin Roche +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +odoo.define("impersonate_login.AbstractWebClient", function (require) { + "use strict"; + + var AbstractWebClient = require("web.AbstractWebClient"); + var session = require("web.session"); + + AbstractWebClient.include({ + _onWebClientStarted: function () { + this._super.apply(this, arguments); + if (session.impersonate_from_uid) { + this.$el.addClass("o_is_impersonated"); + } + }, + }); +}); diff --git a/impersonate_login/static/src/js/user_menu.js b/impersonate_login/static/src/js/user_menu.js new file mode 100644 index 0000000000..ad3672ac1b --- /dev/null +++ b/impersonate_login/static/src/js/user_menu.js @@ -0,0 +1,66 @@ +// Copyright 2024 Akretion (https://www.akretion.com). +// @author Kévin Roche +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +odoo.define("impersonate_login.UserMenu", function (require) { + "use strict"; + + var UserMenu = require("web.UserMenu"); + var core = require("web.core"); + var session = require("web.session"); + var _t = core._t; + + UserMenu.include({ + start: function () { + this.toggleImpersonationLinks(); + return this._super.apply(this, arguments); + }, + + _onMenuImpersonate: function () { + var self = this; + this._rpc({ + model: "ir.model.data", + method: "xmlid_to_res_model_res_id", + args: ["impersonate_login.impersonate_res_users_tree"], + }).then(function (data) { + self.do_action({ + type: "ir.actions.act_window", + name: _t("Users"), + res_model: "res.users", + target: "new", + view_mode: "list", + views: [[data[1], "list"]], + domain: [["share", "=", false]], + }); + }); + }, + + _onMenuOrigin_login: function () { + var self = this; + return self + ._rpc({ + model: "res.users", + method: "back_to_origin_login", + args: [], + }) + .then(function () { + location.reload(true); + }); + }, + + toggleImpersonationLinks: function () { + var returnToLogin = this.$('[data-menu="origin_login"]'); + var impersonateLogin = this.$('[data-menu="impersonate"]'); + if (session.impersonate_from_uid) { + returnToLogin.removeClass("d-none"); + impersonateLogin.addClass("d-none"); + } else if (session.is_impersonate_user) { + returnToLogin.addClass("d-none"); + impersonateLogin.removeClass("d-none"); + } else { + returnToLogin.addClass("d-none"); + impersonateLogin.addClass("d-none"); + } + }, + }); +}); diff --git a/impersonate_login/static/src/xml/user_menu.xml b/impersonate_login/static/src/xml/user_menu.xml new file mode 100644 index 0000000000..bce12c050c --- /dev/null +++ b/impersonate_login/static/src/xml/user_menu.xml @@ -0,0 +1,22 @@ + + + + + + 🔙 To my Login + 🔄 Switch Login + + + diff --git a/impersonate_login/views/assets.xml b/impersonate_login/views/assets.xml new file mode 100644 index 0000000000..cf15b77905 --- /dev/null +++ b/impersonate_login/views/assets.xml @@ -0,0 +1,24 @@ + + +