diff --git a/frappe/core/doctype/role/role.js b/frappe/core/doctype/role/role.js index d943e7ba07..acf6f7ce81 100644 --- a/frappe/core/doctype/role/role.js +++ b/frappe/core/doctype/role/role.js @@ -17,13 +17,78 @@ frappe.ui.form.on("Role", { frm.set_df_property("is_custom", "read_only", frappe.session.user !== "Administrator"); - frm.add_custom_button("Role Permissions Manager", function () { - frappe.route_options = { role: frm.doc.name }; - frappe.set_route("permission-manager"); - }); - frm.add_custom_button("Show Users", function () { - frappe.route_options = { role: frm.doc.name }; - frappe.set_route("List", "User", "Report"); - }); + frm.add_custom_button( + __("Role Permissions Manager"), + function () { + frappe.route_options = { role: frm.doc.name }; + frappe.set_route("permission-manager"); + }, + __("View") + ); + + frm.add_custom_button( + __("Show Users"), + function () { + frappe.route_options = { role: frm.doc.name }; + frappe.set_route("List", "User", "Report"); + }, + __("View") + ); + + if (frappe.user.has_role("System Manager")) { + frm.add_custom_button( + __("Replicate Role"), + function () { + replicate_role(frm); + }, + __("Action") + ); + } }, }); + +function replicate_role(frm) { + const dialog = new frappe.ui.Dialog({ + title: __("Replicate Role"), + fields: [ + { + label: __("New Role Name"), + fieldname: "new_role_name", + fieldtype: "Data", + default: frm.doc.name, + reqd: 1, + }, + ], + freeze: true, + freeze_message: __("Replicating Role..."), + primary_action_label: __("Replicate"), + primary_action: function (values) { + dialog.hide(); + frappe.call({ + method: "replicate_role", + doc: frm.doc, + args: { + cur_role: frm.doc.name, + new_role: values.new_role_name, + }, + callback: function (r) { + if (r.message) { + frappe.set_route("Form", "Role", r.message); + frappe.show_alert({ + message: __("New role created successfully."), + indicator: "green", + }); + } else if (r.exc) { + JSON.parse(r.exc).forEach((err) => { + frappe.show_alert({ + message: __(err), + indicator: "red", + }); + }); + } + }, + }); + }, + }); + dialog.show(); +} diff --git a/frappe/core/doctype/role/role.py b/frappe/core/doctype/role/role.py index fbaabb4579..09a089bba4 100644 --- a/frappe/core/doctype/role/role.py +++ b/frappe/core/doctype/role/role.py @@ -2,6 +2,7 @@ # License: MIT. See LICENSE import frappe +from frappe.core.page.permission_manager.permission_manager import get_permissions from frappe.model.document import Document from frappe.website.path_resolver import validate_path from frappe.website.router import clear_routing_cache @@ -86,6 +87,32 @@ class Role(Document): if user_type != user.user_type: user.save() + @frappe.whitelist() + def replicate_role(self, cur_role: str, new_role: str) -> str: + frappe.only_for("System Manager") + + if frappe.db.get_value("Role", new_role, "name"): + return frappe.errprint(f"Role {new_role} already exist.") + + new_role = frappe.get_doc({"doctype": "Role", "role_name": new_role}).insert().name + + perms = get_permissions(role=cur_role) + for perm in perms: + perm.update( + { + "name": None, + "creation": None, + "modified": None, + "modified_by": None, + "owner": None, + "linked_doctypes": None, + "role": new_role, + } + ) + frappe.get_doc({"doctype": "Custom DocPerm", **perm}).insert() + + return new_role + def get_info_based_on_role(role, field="email", ignore_permissions=False): """Get information of all users that have been assigned this role""" diff --git a/frappe/core/doctype/role_replication/__init__.py b/frappe/core/doctype/role_replication/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/core/doctype/role_replication/role_replication.js b/frappe/core/doctype/role_replication/role_replication.js deleted file mode 100644 index eef305cbfd..0000000000 --- a/frappe/core/doctype/role_replication/role_replication.js +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) 2024, Frappe Technologies and contributors -// For license information, please see license.txt - -frappe.ui.form.on("Role Replication", { - refresh(frm) { - frm.disable_save(); - frm.page.set_primary_action(__("Replicate"), ($btn) => { - $btn.text(__("Replicating...")); - frappe.run_serially([ - () => frappe.dom.freeze("Replicating..."), - () => frm.call("replicate_role"), - () => frappe.dom.unfreeze(), - () => frappe.msgprint(__("Replication completed.")), - () => $btn.text(__("Replicate")), - ]); - }); - }, -}); diff --git a/frappe/core/doctype/role_replication/role_replication.json b/frappe/core/doctype/role_replication/role_replication.json deleted file mode 100644 index 77ef317e41..0000000000 --- a/frappe/core/doctype/role_replication/role_replication.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "actions": [], - "creation": "2024-06-24 18:25:23.163914", - "doctype": "DocType", - "engine": "InnoDB", - "field_order": [ - "existing_role", - "column_break_ydyj", - "new_role" - ], - "fields": [ - { - "fieldname": "existing_role", - "fieldtype": "Link", - "label": "Existing Role", - "options": "Role" - }, - { - "fieldname": "column_break_ydyj", - "fieldtype": "Column Break" - }, - { - "description": "Input existing role name if you would like to extend it with access of another role.", - "fieldname": "new_role", - "fieldtype": "Data", - "label": "New Role" - } - ], - "index_web_pages_for_search": 1, - "issingle": 1, - "links": [], - "modified": "2024-06-24 19:26:54.279801", - "modified_by": "Administrator", - "module": "Core", - "name": "Role Replication", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "sort_field": "creation", - "sort_order": "DESC", - "states": [] -} \ No newline at end of file diff --git a/frappe/core/doctype/role_replication/role_replication.py b/frappe/core/doctype/role_replication/role_replication.py deleted file mode 100644 index 3a75263677..0000000000 --- a/frappe/core/doctype/role_replication/role_replication.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright (c) 2024, Frappe Technologies and contributors -# For license information, please see license.txt - -import frappe -from frappe.core.page.permission_manager.permission_manager import get_permissions -from frappe.model.document import Document -from frappe.permissions import setup_custom_perms - - -class RoleReplication(Document): - # begin: auto-generated types - # This code is auto-generated. Do not modify anything in this block. - - from typing import TYPE_CHECKING - - if TYPE_CHECKING: - from frappe.types import DF - - existing_role: DF.Link | None - new_role: DF.Data | None - # end: auto-generated types - - @frappe.whitelist() - def replicate_role(self): - frappe.only_for("System Manager") - - new_role = frappe.db.get_value("Role", self.new_role, "name") - if not new_role: - new_role = frappe.get_doc({"doctype": "Role", "role_name": self.new_role}).insert().name - - perms = get_permissions(role=self.existing_role) - - doctypes_with_custom_perms_setup = set() - for perm in perms: - doctype = perm.get("parent") - if doctype and doctype not in doctypes_with_custom_perms_setup: - # if no Custom DocPerm exists for the doctype, move standard permissions to Custom DocPerm - # before creating first Custom DocPerm for the new role - setup_custom_perms(doctype) - doctypes_with_custom_perms_setup.add(doctype) - - # Create Custom DocPerm for the new role - frappe.get_doc( - { - "doctype": "Custom DocPerm", - **perm, - "name": None, - "creation": None, - "modified": None, - "modified_by": None, - "owner": None, - "linked_doctypes": None, - "role": new_role, - } - ).insert() diff --git a/frappe/core/doctype/role_replication/test_role_replication.py b/frappe/core/doctype/role_replication/test_role_replication.py deleted file mode 100644 index 99fb7f7682..0000000000 --- a/frappe/core/doctype/role_replication/test_role_replication.py +++ /dev/null @@ -1,98 +0,0 @@ -# Copyright (c) 2024, Frappe Technologies and Contributors -# See license.txt - -import frappe -from frappe.permissions import get_all_perms -from frappe.tests import IntegrationTestCase - - -class TestRoleReplication(IntegrationTestCase): - def setUp(self): - # Create a test role with permissions - self.test_role_name = "_Test Role For Replication" - self.new_role_name = "_Test Replicated Role" - - # Clean up any existing test roles and permissions - self._cleanup_test_data() - - # Create the test role - self.test_role = frappe.get_doc({"doctype": "Role", "role_name": self.test_role_name}).insert() - - # Add a DocPerm permission (simulating standard permission) - # We use a doctype that doesn't have Custom DocPerm to simulate the bug scenario - self.test_doctype = "User" - - # First ensure no Custom DocPerm exists for this doctype - frappe.db.delete("Custom DocPerm", {"parent": self.test_doctype}) - - # Add DocPerm for the test role - self.test_perm = frappe.get_doc( - { - "doctype": "DocPerm", - "parent": self.test_doctype, - "parenttype": "DocType", - "parentfield": "permissions", - "role": self.test_role_name, - "permlevel": 0, - "read": 1, - "write": 1, - "create": 0, - } - ).insert() - - def _cleanup_test_data(self): - """Clean up test roles and permissions.""" - for role_name in [self.test_role_name, self.new_role_name]: - frappe.db.delete("Custom DocPerm", {"role": role_name}) - frappe.db.delete("DocPerm", {"role": role_name}) - if frappe.db.exists("Role", role_name): - frappe.delete_doc("Role", role_name, force=True) - - def tearDown(self): - self._cleanup_test_data() - - def test_replicate_role_preserves_original_permissions(self): - """ - Test that replicating a role does not erase the original role's permissions. - This is a regression test for https://github.com/frappe/frappe/issues/34605 - """ - # Get original permissions count before replication using get_all_perms - # (this is what the Role Permissions Manager UI uses) - original_perms_before = get_all_perms(self.test_role_name) - self.assertTrue( - len(original_perms_before) > 0, "Test role should have permissions before replication" - ) - - # Perform role replication - role_replication = frappe.get_doc( - { - "doctype": "Role Replication", - "existing_role": self.test_role_name, - "new_role": self.new_role_name, - } - ) - role_replication.replicate_role() - - # Verify new role was created - self.assertTrue(frappe.db.exists("Role", self.new_role_name), "New role should be created") - - # Verify new role has permissions - new_role_perms = get_all_perms(self.new_role_name) - self.assertTrue(len(new_role_perms) > 0, "New role should have permissions after replication") - - # Verify original role still has its permissions visible via get_all_perms - original_perms_after = get_all_perms(self.test_role_name) - self.assertEqual( - len(original_perms_before), - len(original_perms_after), - "Original role should retain all its permissions after replication", - ) - - # Verify the original role now has Custom DocPerm entries - original_custom_perms = frappe.get_all( - "Custom DocPerm", filters={"role": self.test_role_name}, fields=["parent", "read", "write"] - ) - self.assertTrue( - len(original_custom_perms) > 0, - "Original role should have Custom DocPerm entries after replication to preserve visibility", - )