diff --git a/cypress/integration/permissions.js b/cypress/integration/permissions.js new file mode 100644 index 0000000000..7a13239771 --- /dev/null +++ b/cypress/integration/permissions.js @@ -0,0 +1,57 @@ +context("Permissions API", () => { + before(() => { + cy.visit("/login"); + + cy.login("Administrator"); + cy.call("frappe.tests.ui_test_helpers.add_remove_role", { + action: "remove", + user: "frappe@example.com", + role: "System Manager", + }); + cy.call("logout"); + + cy.login("frappe@example.com"); + cy.visit("/app"); + }); + + it("Checks permissions via `has_perm` for Kanban Board DocType", () => { + cy.visit("/app/kanban-board/view/list"); + cy.window() + .its("frappe") + .then((frappe) => { + frappe.model.with_doctype("Kanban Board", function () { + // needed to make sure doc meta is loaded + expect(frappe.perm.has_perm("Kanban Board", 0, "read")).to.equal(true); + expect(frappe.perm.has_perm("Kanban Board", 0, "write")).to.equal(true); + expect(frappe.perm.has_perm("Kanban Board", 0, "print")).to.equal(false); + }); + }); + }); + + it("Checks permissions via `get_perm` for Kanban Board DocType", () => { + cy.visit("/app/kanban-board/view/list"); + cy.window() + .its("frappe") + .then((frappe) => { + frappe.model.with_doctype("Kanban Board", function () { + // needed to make sure doc meta is loaded + const perms = frappe.perm.get_perm("Kanban Board"); + expect(perms.read).to.equal(true); + expect(perms.write).to.equal(true); + expect(perms.rights_without_if_owner).to.include("read"); + }); + }); + }); + + after(() => { + cy.call("logout"); + + cy.login("Administrator"); + cy.call("frappe.tests.ui_test_helpers.add_remove_role", { + action: "add", + user: "frappe@example.com", + role: "System Manager", + }); + cy.call("logout"); + }); +}); diff --git a/frappe/public/js/frappe/model/perm.js b/frappe/public/js/frappe/model/perm.js index 6931a2e2e7..fdd915ebfc 100644 --- a/frappe/public/js/frappe/model/perm.js +++ b/frappe/public/js/frappe/model/perm.js @@ -37,18 +37,23 @@ $.extend(frappe.perm, { has_perm: (doctype, permlevel, ptype, doc) => { if (!permlevel) permlevel = 0; - if (!frappe.perm.doctype_perm[doctype]) { - frappe.perm.doctype_perm[doctype] = frappe.perm.get_perm(doctype, doc); - } - let perms = frappe.perm.doctype_perm[doctype]; - - if (!perms || !perms[permlevel]) return false; - - return !!perms[permlevel][ptype]; + const perms = frappe.perm.get_perm(doctype, doc); + return !!perms?.[permlevel]?.[ptype]; }, get_perm: (doctype, doc) => { + // if document object is passed, get fresh doc based perms + // (with ownership and user perms applied) else cached doctype perms + + if (doc && !doc.__islocal) { + return frappe.perm._get_perm(doctype, doc); + } + + return (frappe.perm.doctype_perm[doctype] ??= frappe.perm._get_perm(doctype)); + }, + + _get_perm: (doctype, doc) => { let perm = [{ read: 0, permlevel: 0 }]; let meta = frappe.get_doc("DocType", doctype); @@ -61,77 +66,83 @@ $.extend(frappe.perm, { if (!meta) return perm; perm = frappe.perm.get_role_permissions(meta); + const base_perm = perm[0]; if (doc) { // apply user permissions via docinfo (which is processed server-side) let docinfo = frappe.model.get_docinfo(doctype, doc.name); if (docinfo && docinfo.permissions) { Object.keys(docinfo.permissions).forEach((ptype) => { - perm[0][ptype] = docinfo.permissions[ptype]; + base_perm[ptype] = docinfo.permissions[ptype]; }); } // if owner - if (!$.isEmptyObject(perm[0].if_owner)) { - if (doc.owner === user) { - $.extend(perm[0], perm[0].if_owner); - } else { - // not owner, remove permissions - $.each(perm[0].if_owner, (ptype) => { - if (perm[0].if_owner[ptype]) { - perm[0][ptype] = 0; - } - }); + if (doc.owner !== user) { + for (const right of frappe.perm.rights) { + if (base_perm[right] && !base_perm.rights_without_if_owner.has(right)) { + base_perm[right] = 0; + } } } // apply permissions from shared if (docinfo && docinfo.shared) { - for (let i = 0; i < docinfo.shared.length; i++) { - let s = docinfo.shared[i]; - if (s.user === user) { - perm[0]["read"] = perm[0]["read"] || s.read; - perm[0]["write"] = perm[0]["write"] || s.write; - perm[0]["submit"] = perm[0]["submit"] || s.submit; - perm[0]["share"] = perm[0]["share"] || s.share; + for (const s of docinfo.shared) { + if (s.user !== user) continue; - if (s.read) { - // also give print, email permissions if read - // and these permissions exist at level [0] - perm[0].email = - frappe.boot.user.can_email.indexOf(doctype) !== -1 ? 1 : 0; - perm[0].print = - frappe.boot.user.can_print.indexOf(doctype) !== -1 ? 1 : 0; - } + for (const right of ["read", "write", "submit", "share"]) { + if (!base_perm[right]) base_perm[right] = s[right]; + } + + if (s.read) { + // also give print, email permissions if read + // and these permissions exist at level [0] + base_perm.email = + frappe.boot.user.can_email.indexOf(doctype) !== -1 ? 1 : 0; + base_perm.print = + frappe.boot.user.can_print.indexOf(doctype) !== -1 ? 1 : 0; } } } } - if (frappe.model.can_read(doctype) && !perm[0].read) { + if (!base_perm.read && frappe.model.can_read(doctype)) { // read via sharing - perm[0].read = 1; + base_perm.read = 1; } return perm; }, get_role_permissions: (meta) => { + /** Returns a `dict` of evaluated Role Permissions like: + { + "read": 1, + "write": 0, + "rights_without_if_owner": {"read", "write"} // for permlevel 0 + } + */ + let perm = [{ read: 0, permlevel: 0 }]; - // Returns a `dict` of evaluated Role Permissions + (meta.permissions || []).forEach((p) => { - // if user has this role - let permlevel = cint(p.permlevel); - if (!perm[permlevel]) { - perm[permlevel] = {}; - perm[permlevel]["permlevel"] = permlevel; + const permlevel = cint(p.permlevel); + const current_perm = (perm[permlevel] ??= { permlevel }); + + if (permlevel === 0) { + current_perm.rights_without_if_owner ??= new Set(); } + // if user has this role if (frappe.user_roles.includes(p.role)) { frappe.perm.rights.forEach((right) => { - let value = perm[permlevel][right] || p[right] || 0; - if (value) { - perm[permlevel][right] = value; + if (!p[right]) return; + + current_perm[right] = 1; + + if (permlevel === 0 && !p.if_owner) { + current_perm.rights_without_if_owner.add(right); } }); } @@ -169,7 +180,8 @@ $.extend(frappe.perm, { } } - if (perm[0].if_owner && perm[0].read) { + const base_perm = perm[0]; + if (base_perm.read && !base_perm.rights_without_if_owner.has("read")) { match_rules.push({ Owner: frappe.session.user }); } return match_rules; diff --git a/frappe/tests/ui_test_helpers.py b/frappe/tests/ui_test_helpers.py index e32b13a18a..05d3dedad6 100644 --- a/frappe/tests/ui_test_helpers.py +++ b/frappe/tests/ui_test_helpers.py @@ -579,3 +579,12 @@ def create_kanban(): ], } ).insert() + + +@whitelist_for_tests +def add_remove_role(action, user, role): + user_doc = frappe.get_doc("User", user) + if action == "remove": + user_doc.remove_roles(role) + else: + user_doc.add_roles(role) diff --git a/frappe/website/doctype/web_form/web_form.py b/frappe/website/doctype/web_form/web_form.py index 2bfa7e7133..d102ac2fd8 100644 --- a/frappe/website/doctype/web_form/web_form.py +++ b/frappe/website/doctype/web_form/web_form.py @@ -609,4 +609,6 @@ def get_link_options(web_form_name, doctype, allow_read_on_all_link_options=Fals return "\n".join([doc.value for doc in link_options]) else: - raise frappe.PermissionError(_("You don't have permission to access the {0} DocType.").format(doctype)) + raise frappe.PermissionError( + _("You don't have permission to access the {0} DocType.").format(doctype) + )