diff --git a/impersonate_login/README.rst b/impersonate_login/README.rst new file mode 100644 index 0000000000..ceef070224 --- /dev/null +++ b/impersonate_login/README.rst @@ -0,0 +1,113 @@ +================= +Impersonate Login +================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:501ad96751e358edef9d921d3cd01157ee6f2d22e37e4b576d135d77b3616f00 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/15.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-15-0/server-auth-15-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=15.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +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. +- To prevent users with "Administration: Settings" rights from being impersonated, + enable the restrict_impersonate_admin_settings field in the settings. + This will restrict the ability to impersonate users with administrative + access to the settings. + +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: + +Usage +===== + +The impersonating user must belong to group "Impersonate Users". +- 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" toi mpersonate another user. +- On the top-right corner, the button "Back to Original User" is displayed in case the current user is being impersonated. + +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 +- [360ERP](https://www.360erp.com): + - Andrea Stirpe +- `Ooops404 `_: + - Eduard Brahas + +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..6d58305f5d --- /dev/null +++ b/impersonate_login/__init__.py @@ -0,0 +1,2 @@ +from . import models +from .hooks import pre_init_hook diff --git a/impersonate_login/__manifest__.py b/impersonate_login/__manifest__.py new file mode 100644 index 0000000000..1ed60aed83 --- /dev/null +++ b/impersonate_login/__manifest__.py @@ -0,0 +1,33 @@ +# 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": "15.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/res_users.xml", + "views/impersonate_log.xml", + "views/res_config_settings.xml", + "security/group.xml", + "security/ir.model.access.csv", + ], + "assets": { + "web.assets_backend": [ + "impersonate_login/static/src/js/user_menu.esm.js", + ], + }, + "pre_init_hook": "pre_init_hook", +} diff --git a/impersonate_login/hooks.py b/impersonate_login/hooks.py new file mode 100644 index 0000000000..bbbe495bd2 --- /dev/null +++ b/impersonate_login/hooks.py @@ -0,0 +1,19 @@ +# Copyright 2024 360ERP () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + + +def pre_init_hook(cr): + """ + 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") + cr.execute( + "ALTER TABLE mail_message " + "ADD COLUMN IF NOT EXISTS " + "impersonated_author_id INTEGER" + ) diff --git a/impersonate_login/i18n/impersonate_login.pot b/impersonate_login/i18n/impersonate_login.pot new file mode 100644 index 0000000000..c0153ff8a9 --- /dev/null +++ b/impersonate_login/i18n/impersonate_login.pot @@ -0,0 +1,217 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * impersonate_login +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: impersonate_login +#: model:ir.model,name:impersonate_login.model_base +msgid "Base" +msgstr "" + +#. module: impersonate_login +#: model:ir.model,name:impersonate_login.model_res_config_settings +msgid "Config Settings" +msgstr "" + +#. module: impersonate_login +#: model:ir.model.fields,field_description:impersonate_login.field_mail_mail__body +#: model:ir.model.fields,field_description:impersonate_login.field_mail_message__body +msgid "Contents" +msgstr "" + +#. module: impersonate_login +#: model:ir.model.fields,field_description:impersonate_login.field_impersonate_log__create_uid +msgid "Created by" +msgstr "" + +#. module: impersonate_login +#: model:ir.model.fields,field_description:impersonate_login.field_impersonate_log__create_date +msgid "Created on" +msgstr "" + +#. module: impersonate_login +#: model:ir.model.fields,field_description:impersonate_login.field_impersonate_log__display_name +#: model:ir.model.fields,field_description:impersonate_login.field_ir_http__display_name +#: model:ir.model.fields,field_description:impersonate_login.field_mail_message__display_name +#: model:ir.model.fields,field_description:impersonate_login.field_mail_thread__display_name +#: model:ir.model.fields,field_description:impersonate_login.field_res_config_settings__display_name +#: model:ir.model.fields,field_description:impersonate_login.field_res_users__display_name +msgid "Display Name" +msgstr "" + +#. module: impersonate_login +#: model:ir.model,name:impersonate_login.model_mail_thread +msgid "Email Thread" +msgstr "" + +#. module: impersonate_login +#: model:ir.model.fields,field_description:impersonate_login.field_impersonate_log__date_end +msgid "End Date" +msgstr "" + +#. module: impersonate_login +#: model:ir.model,name:impersonate_login.model_ir_http +msgid "HTTP Routing" +msgstr "" + +#. module: impersonate_login +#: model:ir.model.fields,field_description:impersonate_login.field_impersonate_log__id +#: model:ir.model.fields,field_description:impersonate_login.field_ir_http__id +#: model:ir.model.fields,field_description:impersonate_login.field_mail_message__id +#: model:ir.model.fields,field_description:impersonate_login.field_mail_thread__id +#: model:ir.model.fields,field_description:impersonate_login.field_res_config_settings__id +#: model:ir.model.fields,field_description:impersonate_login.field_res_users__id +msgid "ID" +msgstr "" + +#. module: impersonate_login +#: model:ir.model.fields,help:impersonate_login.field_res_config_settings__restrict_impersonate_admin_settings +msgid "" +"If enabled, users with the 'Administration: Settings' access right cannot be" +" impersonated." +msgstr "" + +#. module: impersonate_login +#: model:ir.actions.act_window,name:impersonate_login.impersonate_log_action +msgid "Impersonate Login Logs" +msgstr "" + +#. module: impersonate_login +#: model:ir.model,name:impersonate_login.model_impersonate_log +msgid "Impersonate Logs" +msgstr "" + +#. module: impersonate_login +#: model:res.groups,name:impersonate_login.group_impersonate_login +msgid "Impersonate Users" +msgstr "" + +#. module: impersonate_login +#: model:ir.model.fields,field_description:impersonate_login.field_mail_mail__impersonated_author_id +#: model:ir.model.fields,field_description:impersonate_login.field_mail_message__impersonated_author_id +msgid "Impersonated Author" +msgstr "" + +#. module: impersonate_login +#: model:ir.ui.menu,name:impersonate_login.menu_impersonate_log +msgid "Impersonated Logs" +msgstr "" + +#. module: impersonate_login +#: model_terms:ir.ui.view,arch_db:impersonate_login.view_res_config_settings_impersonate +msgid "Impersonation Login" +msgstr "" + +#. module: impersonate_login +#: code:addons/impersonate_login/models/res_users.py:0 +#, python-format +msgid "It's you." +msgstr "" + +#. module: impersonate_login +#: model:ir.model.fields,field_description:impersonate_login.field_impersonate_log____last_update +#: model:ir.model.fields,field_description:impersonate_login.field_ir_http____last_update +#: model:ir.model.fields,field_description:impersonate_login.field_mail_message____last_update +#: model:ir.model.fields,field_description:impersonate_login.field_mail_thread____last_update +#: model:ir.model.fields,field_description:impersonate_login.field_res_config_settings____last_update +#: model:ir.model.fields,field_description:impersonate_login.field_res_users____last_update +msgid "Last Modified on" +msgstr "" + +#. module: impersonate_login +#: model:ir.model.fields,field_description:impersonate_login.field_impersonate_log__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: impersonate_login +#: model:ir.model.fields,field_description:impersonate_login.field_impersonate_log__write_date +msgid "Last Updated on" +msgstr "" + +#. module: impersonate_login +#: model:ir.model.fields,field_description:impersonate_login.field_impersonate_log__impersonated_partner_id +msgid "Logged as" +msgstr "" + +#. module: impersonate_login +#: code:addons/impersonate_login/models/mail_message.py:0 +#: code:addons/impersonate_login/models/mail_message.py:0 +#, python-format +msgid "Logged in as {}" +msgstr "" + +#. module: impersonate_login +#: model:ir.model,name:impersonate_login.model_mail_message +msgid "Message" +msgstr "" + +#. module: impersonate_login +#: model_terms:ir.ui.view,arch_db:impersonate_login.view_res_config_settings_impersonate +msgid "Restrict Impersonation Login" +msgstr "" + +#. module: impersonate_login +#: model:ir.model.fields,field_description:impersonate_login.field_res_config_settings__restrict_impersonate_admin_settings +#: model_terms:ir.ui.view,arch_db:impersonate_login.view_res_config_settings_impersonate +msgid "Restrict Impersonation of 'Administration: Settings' Users" +msgstr "" + +#. module: impersonate_login +#: model:ir.model.fields,field_description:impersonate_login.field_impersonate_log__date_start +msgid "Start Date" +msgstr "" + +#. module: impersonate_login +#: model_terms:ir.ui.view,arch_db:impersonate_login.impersonate_res_users_tree +msgid "Switch Login" +msgstr "" + +#. module: impersonate_login +#: model:ir.model.fields,field_description:impersonate_login.field_impersonate_log__user_id +msgid "User" +msgstr "" + +#. module: impersonate_login +#. openerp-web +#: code:addons/impersonate_login/static/src/js/user_menu.js:0 +#: model:ir.model,name:impersonate_login.model_res_users +#, python-format +msgid "Users" +msgstr "" + +#. module: impersonate_login +#: code:addons/impersonate_login/models/res_users.py:0 +#, python-format +msgid "You are already Logged as another user." +msgstr "" + +#. module: impersonate_login +#: code:addons/impersonate_login/models/res_users.py:0 +#, python-format +msgid "" +"You cannot impersonate users with 'Administration: Settings' access rights." +msgstr "" + +#. module: impersonate_login +#. openerp-web +#: code:addons/impersonate_login/static/src/xml/user_menu.xml:0 +#, python-format +msgid "🔄 Switch Login" +msgstr "" + +#. module: impersonate_login +#. openerp-web +#: code:addons/impersonate_login/static/src/xml/user_menu.xml:0 +#, python-format +msgid "🔙 To my Login" +msgstr "" diff --git a/impersonate_login/i18n/it.po b/impersonate_login/i18n/it.po new file mode 100644 index 0000000000..822b8713ee --- /dev/null +++ b/impersonate_login/i18n/it.po @@ -0,0 +1,223 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * impersonate_login +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-11-20 12:06+0000\n" +"Last-Translator: Francesco Foresti \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.6.2\n" + +#. module: impersonate_login +#: model:ir.model,name:impersonate_login.model_base +msgid "Base" +msgstr "Base" + +#. module: impersonate_login +#: model:ir.model,name:impersonate_login.model_res_config_settings +msgid "Config Settings" +msgstr "Impostazioni configurazione" + +#. module: impersonate_login +#: model:ir.model.fields,field_description:impersonate_login.field_mail_mail__body +#: model:ir.model.fields,field_description:impersonate_login.field_mail_message__body +msgid "Contents" +msgstr "Contenuti" + +#. module: impersonate_login +#: model:ir.model.fields,field_description:impersonate_login.field_impersonate_log__create_uid +msgid "Created by" +msgstr "Creato da" + +#. module: impersonate_login +#: model:ir.model.fields,field_description:impersonate_login.field_impersonate_log__create_date +msgid "Created on" +msgstr "Creato il" + +#. module: impersonate_login +#: model:ir.model.fields,field_description:impersonate_login.field_impersonate_log__display_name +#: model:ir.model.fields,field_description:impersonate_login.field_ir_http__display_name +#: model:ir.model.fields,field_description:impersonate_login.field_mail_message__display_name +#: model:ir.model.fields,field_description:impersonate_login.field_mail_thread__display_name +#: model:ir.model.fields,field_description:impersonate_login.field_res_config_settings__display_name +#: model:ir.model.fields,field_description:impersonate_login.field_res_users__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: impersonate_login +#: model:ir.model,name:impersonate_login.model_mail_thread +msgid "Email Thread" +msgstr "Discussione e-mail" + +#. module: impersonate_login +#: model:ir.model.fields,field_description:impersonate_login.field_impersonate_log__date_end +msgid "End Date" +msgstr "Data fine" + +#. module: impersonate_login +#: model:ir.model,name:impersonate_login.model_ir_http +msgid "HTTP Routing" +msgstr "Instradamento HTTP" + +#. module: impersonate_login +#: model:ir.model.fields,field_description:impersonate_login.field_impersonate_log__id +#: model:ir.model.fields,field_description:impersonate_login.field_ir_http__id +#: model:ir.model.fields,field_description:impersonate_login.field_mail_message__id +#: model:ir.model.fields,field_description:impersonate_login.field_mail_thread__id +#: model:ir.model.fields,field_description:impersonate_login.field_res_config_settings__id +#: model:ir.model.fields,field_description:impersonate_login.field_res_users__id +msgid "ID" +msgstr "ID" + +#. module: impersonate_login +#: model:ir.model.fields,help:impersonate_login.field_res_config_settings__restrict_impersonate_admin_settings +msgid "" +"If enabled, users with the 'Administration: Settings' access right cannot be " +"impersonated." +msgstr "" +"Se abilitato, gli utenti con diritto di accesso \"Amministrazione: " +"impostazioni\" non possono essere impersonati." + +#. module: impersonate_login +#: model:ir.actions.act_window,name:impersonate_login.impersonate_log_action +msgid "Impersonate Login Logs" +msgstr "Imita registri di accesso" + +#. module: impersonate_login +#: model:ir.model,name:impersonate_login.model_impersonate_log +msgid "Impersonate Logs" +msgstr "Imita registri" + +#. module: impersonate_login +#: model:res.groups,name:impersonate_login.group_impersonate_login +msgid "Impersonate Users" +msgstr "Imita utenti" + +#. module: impersonate_login +#: model:ir.model.fields,field_description:impersonate_login.field_mail_mail__impersonated_author_id +#: model:ir.model.fields,field_description:impersonate_login.field_mail_message__impersonated_author_id +msgid "Impersonated Author" +msgstr "Imita autore" + +#. module: impersonate_login +#: model:ir.ui.menu,name:impersonate_login.menu_impersonate_log +msgid "Impersonated Logs" +msgstr "Imita registri" + +#. module: impersonate_login +#: model_terms:ir.ui.view,arch_db:impersonate_login.view_res_config_settings_impersonate +msgid "Impersonation Login" +msgstr "Accesso con altro utente" + +#. module: impersonate_login +#: code:addons/impersonate_login/models/res_users.py:0 +#, python-format +msgid "It's you." +msgstr "Sei tu." + +#. module: impersonate_login +#: model:ir.model.fields,field_description:impersonate_login.field_impersonate_log____last_update +#: model:ir.model.fields,field_description:impersonate_login.field_ir_http____last_update +#: model:ir.model.fields,field_description:impersonate_login.field_mail_message____last_update +#: model:ir.model.fields,field_description:impersonate_login.field_mail_thread____last_update +#: model:ir.model.fields,field_description:impersonate_login.field_res_config_settings____last_update +#: model:ir.model.fields,field_description:impersonate_login.field_res_users____last_update +msgid "Last Modified on" +msgstr "Ultima modifica il" + +#. module: impersonate_login +#: model:ir.model.fields,field_description:impersonate_login.field_impersonate_log__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: impersonate_login +#: model:ir.model.fields,field_description:impersonate_login.field_impersonate_log__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: impersonate_login +#: model:ir.model.fields,field_description:impersonate_login.field_impersonate_log__impersonated_partner_id +msgid "Logged as" +msgstr "Registrato come" + +#. module: impersonate_login +#: code:addons/impersonate_login/models/mail_message.py:0 +#, python-format +msgid "Logged in as {}" +msgstr "Registrato come {}" + +#. module: impersonate_login +#: model:ir.model,name:impersonate_login.model_mail_message +msgid "Message" +msgstr "Messaggio" + +#. module: impersonate_login +#: model_terms:ir.ui.view,arch_db:impersonate_login.view_res_config_settings_impersonate +msgid "Restrict Impersonation Login" +msgstr "Limita accesso con altro utente" + +#. module: impersonate_login +#: model:ir.model.fields,field_description:impersonate_login.field_res_config_settings__restrict_impersonate_admin_settings +#: model_terms:ir.ui.view,arch_db:impersonate_login.view_res_config_settings_impersonate +msgid "Restrict Impersonation of 'Administration: Settings' Users" +msgstr "Limita accesso con utente \"Amministrazione: impostazioni\"" + +#. module: impersonate_login +#: model:ir.model.fields,field_description:impersonate_login.field_impersonate_log__date_start +msgid "Start Date" +msgstr "Data inizio" + +#. module: impersonate_login +#: model_terms:ir.ui.view,arch_db:impersonate_login.impersonate_res_users_tree +msgid "Switch Login" +msgstr "Scambia accesso" + +#. module: impersonate_login +#: model:ir.model.fields,field_description:impersonate_login.field_impersonate_log__user_id +msgid "User" +msgstr "Utente" + +#. module: impersonate_login +#. openerp-web +#: code:addons/impersonate_login/static/src/js/user_menu.js:0 +#: model:ir.model,name:impersonate_login.model_res_users +#, python-format +msgid "Users" +msgstr "Utenti" + +#. module: impersonate_login +#: code:addons/impersonate_login/models/res_users.py:0 +#, python-format +msgid "You are already Logged as another user." +msgstr "Si è già registrati come altro utente." + +#. module: impersonate_login +#: code:addons/impersonate_login/models/res_users.py:0 +#, python-format +msgid "" +"You cannot impersonate users with 'Administration: Settings' access rights." +msgstr "" +"Non puoi accedere agli utenti con diritto di accesso \"Amministrazione: " +"impostazioni\"." + +#. module: impersonate_login +#. openerp-web +#: code:addons/impersonate_login/static/src/xml/user_menu.xml:0 +#, python-format +msgid "🔄 Switch Login" +msgstr "🔄 Scambia accesso" + +#. module: impersonate_login +#. openerp-web +#: code:addons/impersonate_login/static/src/xml/user_menu.xml:0 +#, python-format +msgid "🔙 To my Login" +msgstr "🔙 Torna al mio utente" diff --git a/impersonate_login/models/__init__.py b/impersonate_login/models/__init__.py new file mode 100644 index 0000000000..d483c409d4 --- /dev/null +++ b/impersonate_login/models/__init__.py @@ -0,0 +1,7 @@ +from . import res_users +from . import ir_http +from . import mail_thread +from . import mail_message +from . import impersonate_log +from . import model +from . import res_config_settings diff --git a/impersonate_login/models/impersonate_log.py b/impersonate_login/models/impersonate_log.py new file mode 100644 index 0000000000..83089611a6 --- /dev/null +++ b/impersonate_login/models/impersonate_log.py @@ -0,0 +1,25 @@ +# 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.users", + ) + 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..e7bf2fd4dc --- /dev/null +++ b/impersonate_login/models/mail_message.py @@ -0,0 +1,79 @@ +# 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 +from odoo.tools import html_escape + + +class Message(models.Model): + _inherit = "mail.message" + + impersonated_author_id = fields.Many2one( + comodel_name="res.partner", + compute="_compute_impersonated_author_id", + store=True, + ) + + body = fields.Html( + compute="_compute_message_body", + inverse="_inverse_message_body", + store=True, + readonly=False, + ) + + @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 = _("Logged in as {}").format( + html_escape(current_partner.name) + ) + if rec.body and additional_info: + rec.body = f"{additional_info}
{rec.body}" + else: + rec.body = rec.body + + def _inverse_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 = _("Logged in as {}").format( + html_escape(current_partner.name) + ) + if additional_info: + start_with = f"{additional_info}
" + if rec.body and rec.body.startswith(start_with): + rec.body = rec.body + else: + rec.body = f"{start_with}{rec.body}" + else: + rec.body = rec.body diff --git a/impersonate_login/models/mail_thread.py b/impersonate_login/models/mail_thread.py new file mode 100644 index 0000000000..3fdd4a86d0 --- /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: + 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, + email_from, + raise_exception, + ) diff --git a/impersonate_login/models/model.py b/impersonate_login/models/model.py new file mode 100644 index 0000000000..16ac8a5a94 --- /dev/null +++ b/impersonate_login/models/model.py @@ -0,0 +1,44 @@ +# Copyright (C) 2024 Akretion (). +# @author Kévin Roche +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from psycopg2.extensions import AsIs + +from odoo import api, models +from odoo.http import request + + +class BaseModel(models.AbstractModel): + _inherit = "base" + + @api.model + def _create(self, data_list): + res = super()._create(data_list) + if ( + request + and request.session.impersonate_from_uid + and "create_uid" in self._fields + ): + self.env.cr.execute( + """ + UPDATE %(table)s + SET create_uid = %(impersonator_id)s + WHERE id IN %(record_ids)s + """, + { + "table": AsIs(self._table), + "impersonator_id": request.session.impersonate_from_uid, + "record_ids": tuple(rec.id for rec in res), + }, + ) + return res + + def write(self, vals): + res = super().write(vals) + 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 diff --git a/impersonate_login/models/res_config_settings.py b/impersonate_login/models/res_config_settings.py new file mode 100644 index 0000000000..5f687595d1 --- /dev/null +++ b/impersonate_login/models/res_config_settings.py @@ -0,0 +1,13 @@ +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + restrict_impersonate_admin_settings = fields.Boolean( + string="Restrict Impersonation of 'Administration: Settings' Users", + config_parameter="impersonate_login.restrict_impersonate_admin_settings", + help="If enabled, users with the 'Administration: Settings' access right" + " cannot be impersonated.", + default=False, + ) diff --git a/impersonate_login/models/res_users.py b/impersonate_login/models/res_users.py new file mode 100644 index 0000000000..071da91115 --- /dev/null +++ b/impersonate_login/models/res_users.py @@ -0,0 +1,133 @@ +# 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: + + config_restrict = ( + self.env["ir.config_parameter"] + .sudo() + .get_param("impersonate_login.restrict_impersonate_admin_settings") + ) + if config_restrict: + admin_settings_group = self.env.ref("base.group_system") + if admin_settings_group in self.groups_id: + raise UserError( + _( + "You cannot impersonate users with" + " 'Administration: Settings' access rights." + ) + ) + + if request.session.impersonate_from_uid: + 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(): + 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._uid, + "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)}" + ) + # invalidate session token cache as we've changed the uid + request.env["res.users"].clear_caches() + 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, "list"]] + action["domain"] = [ + ("id", "!=", self.env.user.id), + ("share", "=", False), + ] + action["target"] = "new" + return action + + @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(), + } + ) + # invalidate session token cache as we've changed the uid + 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)}" + ) + + # 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}, + } diff --git a/impersonate_login/readme/CONTRIBUTORS.rst b/impersonate_login/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..2198ac7c2a --- /dev/null +++ b/impersonate_login/readme/CONTRIBUTORS.rst @@ -0,0 +1,5 @@ +- Kévin Roche +- [360ERP](https://www.360erp.com): + - Andrea Stirpe +- `Ooops404 `_: + - Eduard Brahas diff --git a/impersonate_login/readme/DESCRIPTION.rst b/impersonate_login/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..3fb0e32b38 --- /dev/null +++ b/impersonate_login/readme/DESCRIPTION.rst @@ -0,0 +1,19 @@ +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. +- To prevent users with "Administration: Settings" rights from being impersonated, + enable the restrict_impersonate_admin_settings field in the settings. + This will restrict the ability to impersonate users with administrative + access to the settings. + +There is an alternative module to allow logins as another user +(auth_admin_passkey), but it does not support these security mechanisms. diff --git a/impersonate_login/readme/USAGE.rst b/impersonate_login/readme/USAGE.rst new file mode 100644 index 0000000000..52029ce033 --- /dev/null +++ b/impersonate_login/readme/USAGE.rst @@ -0,0 +1,3 @@ +The impersonating user must belong to group "Impersonate Users". +- 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" toi mpersonate another user. +- On the top-right corner, the button "Back to Original User" is displayed in case the current user is being impersonated. 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/icon.png b/impersonate_login/static/description/icon.png new file mode 100644 index 0000000000..3a0328b516 Binary files /dev/null and b/impersonate_login/static/description/icon.png differ diff --git a/impersonate_login/static/description/index.html b/impersonate_login/static/description/index.html new file mode 100644 index 0000000000..59d0ad8b0b --- /dev/null +++ b/impersonate_login/static/description/index.html @@ -0,0 +1,457 @@ + + + + + + +Impersonate Login + + + +
+

Impersonate Login

+ + +

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

+

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.
  • +
  • +
    To prevent users with “Administration: Settings” rights from being impersonated,
    +
    enable the restrict_impersonate_admin_settings field in the settings. +This will restrict the ability to impersonate users with administrative +access to the settings.
    +
    +
  • +
+

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

+ +
+

Usage

+

The impersonating user must belong to group “Impersonate Users”. +- 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” toi mpersonate another user. +- On the top-right corner, the button “Back to Original User” is displayed in case the current user is being impersonated.

+
+
+

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/js/user_menu.esm.js b/impersonate_login/static/src/js/user_menu.esm.js new file mode 100644 index 0000000000..b467c47f17 --- /dev/null +++ b/impersonate_login/static/src/js/user_menu.esm.js @@ -0,0 +1,45 @@ +/** @odoo-module **/ +// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import {_t} from "@web/core/l10n/translation"; +import {registry} from "@web/core/registry"; +import {session} from "@web/session"; + +export function impersonateLoginItem(env) { + return { + type: "item", + id: "impersonate_login", + description: _t("Switch Login"), + hide: session.impersonate_from_uid || !session.is_impersonate_user, + callback: async function () { + const actionImpersonateLogin = await env.services.orm.call( + "res.users", + "action_impersonate_login" + ); + env.services.action.doAction(actionImpersonateLogin); + }, + sequence: 55, + }; +} + +export function impersonateBackLoginItem(env) { + return { + type: "item", + id: "impersonate_back", + description: _t("Back to Original User"), + hide: !session.impersonate_from_uid, + callback: async function () { + const actionBackToOriginLogin = await env.services.orm.call( + "res.users", + "back_to_origin_login" + ); + env.services.action.doAction(actionBackToOriginLogin); + }, + sequence: 55, + }; +} + +registry + .category("user_menuitems") + .add("impersonate_login", impersonateLoginItem, {force: true}) + .add("impersonate_back", impersonateBackLoginItem, {force: true}); diff --git a/impersonate_login/tests/__init__.py b/impersonate_login/tests/__init__.py new file mode 100644 index 0000000000..d2f02c982b --- /dev/null +++ b/impersonate_login/tests/__init__.py @@ -0,0 +1 @@ +from . import test_impersonate_login diff --git a/impersonate_login/tests/test_impersonate_login.py b/impersonate_login/tests/test_impersonate_login.py new file mode 100644 index 0000000000..d84a22d5b7 --- /dev/null +++ b/impersonate_login/tests/test_impersonate_login.py @@ -0,0 +1,303 @@ +# Copyright 2024 360ERP () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +import json +from uuid import uuid4 + +from odoo.tests import HttpCase, tagged +from odoo.tools import mute_logger + + +@tagged("post_install", "-at_install") +class TestImpersonateLogin(HttpCase): + def setUp(self): + super().setUp() + self.admin_user = self.env.ref("base.user_admin") + self.demo_user = self.env.ref("base.user_demo") + + def _impersonate_user(self, user): + response = self.url_open( + "/web/dataset/call_button", + data=json.dumps( + { + "params": { + "model": "res.users", + "method": "impersonate_login", + "args": [user.id], + "kwargs": {}, + }, + } + ), + headers={"Content-Type": "application/json"}, + ) + self.assertEqual(response.status_code, 200) + return response.json() + + def _action_impersonate_login(self): + response = self.url_open( + "/web/dataset/call_button", + data=json.dumps( + { + "params": { + "model": "res.users", + "method": "action_impersonate_login", + "args": [], + "kwargs": {}, + }, + } + ), + headers={"Content-Type": "application/json"}, + ) + self.assertEqual(response.status_code, 200) + return response.json() + + def _get_session_info(self): + response = self.url_open( + "/web/session/get_session_info", + data=json.dumps(dict(jsonrpc="2.0", method="call", id=str(uuid4()))), + headers={"Content-Type": "application/json"}, + ) + self.assertEqual(response.status_code, 200) + return response.json() + + def test_01_admin_impersonates_user_demo(self): + """Admin user impersonates Demo user""" + # Login as admin + self.authenticate(user="admin", password="admin") + self.assertEqual(self.session.uid, self.admin_user.id) + + # Check get_session_info() + data = self._get_session_info() + result = data["result"] + self.assertEqual(result["username"], self.admin_user.login) + self.assertTrue(result["is_system"]) + self.assertTrue(result["is_admin"]) + self.assertTrue(result["is_impersonate_user"]) + self.assertFalse(result["impersonate_from_uid"]) + + # Switch Login button + data = self._action_impersonate_login() + result = data["result"] + self.assertEqual(result["target"], "new") + + # Impersonate demo user + data = self._impersonate_user(self.demo_user) + result = data["result"] + self.assertEqual(result["tag"], "reload") + + # Check get_session_info() + data = self._get_session_info() + result = data["result"] + self.assertEqual(result["username"], self.demo_user.login) + self.assertFalse(result["is_system"]) + self.assertFalse(result["is_admin"]) + self.assertFalse(result["is_impersonate_user"]) + self.assertEqual(result["impersonate_from_uid"], self.admin_user.id) + + # Check impersonate log + log1 = self.env["impersonate.log"].search([], order="id desc", limit=1) + self.assertTrue(log1.date_start) + self.assertFalse(log1.date_end) + + # Impersonate demo user again: error + with mute_logger("odoo.http"): + data = self._impersonate_user(self.demo_user) + result = data["error"] + self.assertEqual( + result["data"]["message"], "You are already Logged as another user." + ) + + # Back to original user + data = self._impersonate_user(self.admin_user) + result = data["result"] + self.assertEqual(result["tag"], "reload") + + # Check get_session_info() + data = self._get_session_info() + result = data["result"] + self.assertEqual(result["username"], self.admin_user.login) + self.assertTrue(result["is_system"]) + self.assertTrue(result["is_admin"]) + self.assertTrue(result["is_impersonate_user"]) + self.assertFalse(result["impersonate_from_uid"]) + + # Check impersonate log + log2 = self.env["impersonate.log"].search([], order="id desc", limit=1) + # Refresh the log1 after the attribute date_end is updated + log1.refresh() + self.assertEqual(log1, log2) + self.assertTrue(log1.date_start) + self.assertTrue(log1.date_end) + + def test_02_user_demo_impersonates_admin(self): + """Demo user impersonates Admin user""" + # Login as demo user + self.authenticate(user="demo", password="demo") + self.assertEqual(self.session.uid, self.demo_user.id) + + # Check get_session_info() + data = self._get_session_info() + result = data["result"] + self.assertFalse(result["is_impersonate_user"]) + self.assertFalse(result["impersonate_from_uid"]) + + # Impersonate demo user: is already current user + self.demo_user.groups_id += self.env.ref( + "impersonate_login.group_impersonate_login" + ) + with mute_logger("odoo.http"): + data = self._impersonate_user(self.demo_user) + result = data["error"] + self.assertEqual(result["data"]["message"], "It's you.") + + # Impersonate admin user + data = self._impersonate_user(self.admin_user) + result = data["result"] + self.assertEqual(result["tag"], "reload") + + # Check get_session_info() + data = self._get_session_info() + result = data["result"] + self.assertEqual(result["username"], self.admin_user.login) + self.assertTrue(result["is_system"]) + self.assertTrue(result["is_admin"]) + self.assertTrue(result["is_impersonate_user"]) + self.assertEqual(result["impersonate_from_uid"], self.demo_user.id) + + # Impersonate admin user again: error + with mute_logger("odoo.http"): + data = self._impersonate_user(self.admin_user) + result = data["error"] + self.assertEqual( + result["data"]["message"], "You are already Logged as another user." + ) + + # Back to original user + data = self._impersonate_user(self.demo_user) + result = data["result"] + self.assertEqual(result["tag"], "reload") + + # Check get_session_info() + data = self._get_session_info() + result = data["result"] + self.assertEqual(result["username"], self.demo_user.login) + self.assertFalse(result["is_system"]) + self.assertFalse(result["is_admin"]) + self.assertTrue(result["is_impersonate_user"]) + self.assertFalse(result["impersonate_from_uid"]) + + def test_03_create_uid(self): + """Check the create_uid of records created + during an impersonated session""" + # Login as admin + self.authenticate(user="admin", password="admin") + + # Impersonate demo user and create a contact + self._impersonate_user(self.demo_user) + + response = self.url_open( + "/web/dataset/call_kw/res.partner/create", + data=json.dumps( + { + "params": { + "model": "res.partner", + "method": "create", + "args": [ + { + "name": "Contact123", + }, + ], + "kwargs": {}, + }, + } + ), + headers={"Content-Type": "application/json"}, + ) + self.assertEqual(response.status_code, 200) + data = response.json() + contact_id = data["result"] + + contact = self.env["res.partner"].browse(contact_id) + self.assertEqual(contact.name, "Contact123") + self.assertEqual(contact.create_uid, self.admin_user) + + def test_04_write_uid(self): + """Check the write_uid of records created + during an impersonated session""" + # Login as admin + self.authenticate(user="admin", password="admin") + + # Create a contact + contact = self.env["res.partner"].create({"name": "ContactABC"}) + + # Impersonate demo user and modify a contact + self._impersonate_user(self.demo_user) + + response = self.url_open( + "/web/dataset/call_kw/res.partner/write", + data=json.dumps( + { + "params": { + "model": "res.partner", + "method": "write", + "args": [ + [contact.id], + { + "ref": "abc", + }, + ], + "kwargs": {}, + }, + } + ), + headers={"Content-Type": "application/json"}, + ) + self.assertEqual(response.status_code, 200) + data = response.json() + result = data["result"] + + # Refresh contact to reflect changes in the database + self.assertEqual(result, True) + contact.invalidate_cache() + self.assertEqual(contact.ref, "abc") + self.assertEqual(contact.write_uid, self.admin_user) + + def test_05_limit_access_to_admin(self): + """ + Test restriction on impersonating admin users + with 'Administration: Settings' access rights. + """ + # Enable the configuration setting via ResConfigSettings + config_settings = self.env["res.config.settings"].create( + {"restrict_impersonate_admin_settings": True} + ) + config_settings.execute() + + # Ensure the configuration parameter is set + config_restrict = ( + self.env["ir.config_parameter"] + .sudo() + .get_param("impersonate_login.restrict_impersonate_admin_settings") + ) + self.assertTrue(config_restrict) + + # Ensure the admin user has the 'Administration: Settings' group + admin_settings_group = self.env.ref("base.group_system") + self.admin_user.groups_id += admin_settings_group + + # Login as demo user + self.authenticate(user="demo", password="demo") + self.assertEqual(self.session.uid, self.demo_user.id) + + # Give demo user the impersonation group + self.demo_user.groups_id += self.env.ref( + "impersonate_login.group_impersonate_login" + ) + + with mute_logger("odoo.http"): + data = self._impersonate_user(self.admin_user) + # Validate the error message + self.assertEqual( + data["error"]["data"]["message"], + "You cannot impersonate users with 'Administration: Settings' access rights.", + ) diff --git a/impersonate_login/views/impersonate_log.xml b/impersonate_login/views/impersonate_log.xml new file mode 100644 index 0000000000..3e504f924e --- /dev/null +++ b/impersonate_login/views/impersonate_log.xml @@ -0,0 +1,36 @@ + + + + + impersonate.log.tree + impersonate.log + + + + + + + + + + + + + Impersonate Login Logs + impersonate.log + + tree + + + + + diff --git a/impersonate_login/views/res_config_settings.xml b/impersonate_login/views/res_config_settings.xml new file mode 100644 index 0000000000..8717e32c53 --- /dev/null +++ b/impersonate_login/views/res_config_settings.xml @@ -0,0 +1,33 @@ + + + res.config.settings.impersonate + res.config.settings + + + +
+

Impersonation Login

+
+
+
+ + +
+
+
+
+
+
+
+
diff --git a/impersonate_login/views/res_users.xml b/impersonate_login/views/res_users.xml new file mode 100644 index 0000000000..1cab54289f --- /dev/null +++ b/impersonate_login/views/res_users.xml @@ -0,0 +1,22 @@ + + + + + res.users + + tree + + +