diff --git a/frappe/core/doctype/doctype/doctype.js b/frappe/core/doctype/doctype/doctype.js index 3e2a423b06..2b06d86553 100644 --- a/frappe/core/doctype/doctype/doctype.js +++ b/frappe/core/doctype/doctype/doctype.js @@ -13,6 +13,17 @@ frappe.ui.form.on('DocType', { refresh: function(frm) { + frm.set_query('role', 'permissions', function(doc) { + if (doc.custom && frappe.session.user != 'Administrator') { + return { + query: "frappe.core.doctype.role.role.role_query", + filters: { + name: ['not in', ['All']] + } + }; + } + }); + if(frappe.session.user !== "Administrator" || !frappe.boot.developer_mode) { if(frm.is_new()) { frm.set_value("custom", 1); diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index c0a82c594a..db9ae19b03 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -1112,6 +1112,20 @@ def validate_permissions(doctype, for_remove=False, alert=False): if d.get("import") and not isimportable: frappe.throw(_("{0}: Cannot set import as {1} is not importable").format(get_txt(d), doctype)) + def validate_permission_for_all_role(d): + if frappe.session.user == 'Administrator': return + + if doctype.custom: + if d.role == 'All': + frappe.throw(_('Row # {0}: Non administrator user can not set the role {1} to the custom doctype') + .format(d.idx, frappe.bold(_('All'))), title=_('Permissions Error')) + + roles = [row.name for row in frappe.get_all('Role', filters={'is_custom': 1})] + + if d.role in roles: + frappe.throw(_('Row # {0}: Non administrator user can not set the role {1} to the custom doctype') + .format(d.idx, frappe.bold(_(d.role))), title=_('Permissions Error')) + for d in permissions: if not d.permlevel: d.permlevel=0 @@ -1123,6 +1137,7 @@ def validate_permissions(doctype, for_remove=False, alert=False): check_if_importable(d) check_level_zero_is_set(d) remove_rights_for_single(d) + validate_permission_for_all_role(d) def make_module_and_roles(doc, perm_fieldname="permissions"): """Make `Module Def` and `Role` records if already not made. Called while installing.""" diff --git a/frappe/core/doctype/role/role.js b/frappe/core/doctype/role/role.js index 6968607008..47b2207c9a 100644 --- a/frappe/core/doctype/role/role.js +++ b/frappe/core/doctype/role/role.js @@ -3,6 +3,9 @@ frappe.ui.form.on('Role', { refresh: function(frm) { + frm.set_df_property('is_custom', 'read_only', + frappe.session.user == 'Administrator' ? false : true); + frm.add_custom_button("Role Permissions Manager", function() { frappe.route_options = {"role": frm.doc.name}; frappe.set_route("permission-manager"); diff --git a/frappe/core/doctype/role/role.json b/frappe/core/doctype/role/role.json index e47dc7194b..0135cbf9e8 100644 --- a/frappe/core/doctype/role/role.json +++ b/frappe/core/doctype/role/role.json @@ -25,7 +25,8 @@ "form_settings_section", "form_sidebar", "timeline", - "dashboard" + "dashboard", + "is_custom" ], "fields": [ { @@ -141,13 +142,20 @@ "fieldname": "notifications", "fieldtype": "Check", "label": "Notifications" + }, + { + "default": "0", + "fieldname": "is_custom", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Is Custom" } ], "icon": "fa fa-bookmark", "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2020-12-03 14:08:38.181035", + "modified": "2021-01-27 10:35:37.638350", "modified_by": "Administrator", "module": "Core", "name": "Role", diff --git a/frappe/core/doctype/role/role.py b/frappe/core/doctype/role/role.py index 7adfeba8d9..bacd2e5274 100644 --- a/frappe/core/doctype/role/role.py +++ b/frappe/core/doctype/role/role.py @@ -53,7 +53,6 @@ class Role(Document): if user_type != user.user_type: user.save() - def get_info_based_on_role(role, field='email'): ''' Get information of all users that have been assigned this role ''' users = frappe.get_list("Has Role", filters={"role": role, "parenttype": "User"}, @@ -73,3 +72,15 @@ def get_user_info(users, field='email'): def get_users(role): return [d.parent for d in frappe.get_all("Has Role", filters={"role": role, "parenttype": "User"}, fields=["parent"])] + + +# searches for active employees +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def role_query(doctype, txt, searchfield, start, page_len, filters): + filters.update({ + 'is_custom': 0, 'name': ('like', '%{0}%'.format(txt)) + }) + + return frappe.get_all('Role', limit_start=start, limit_page_length=page_len, + filters=filters, as_list=1) \ No newline at end of file diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js index 3548b4c913..081d0788f3 100644 --- a/frappe/core/doctype/user/user.js +++ b/frappe/core/doctype/user/user.js @@ -59,10 +59,11 @@ frappe.ui.form.on('User', { onload: function(frm) { frm.can_edit_roles = has_access_to_edit_user(); - if (frm.can_edit_roles && !frm.is_new()) { - if (!frm.roles_editor) { + if (frm.can_edit_roles && !frm.is_new() && frm.doc.user_type == 'System User') { + if(!frm.roles_editor) { const role_area = $('
') .appendTo(frm.fields_dict.roles_html.wrapper); + frm.roles_editor = new frappe.RoleEditor(role_area, frm, frm.doc.role_profile_name ? 1 : 0); var module_area = $('
') @@ -75,7 +76,7 @@ frappe.ui.form.on('User', { }, refresh: function(frm) { var doc = frm.doc; - if(!frm.is_new() && !frm.roles_editor && frm.can_edit_roles) { + if (frm.doc.user_type == 'System User' && !frm.is_new() && !frm.roles_editor && frm.can_edit_roles) { frm.reload_doc(); return; } diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json index 747ace5de6..66902732cd 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -191,7 +191,7 @@ "print_hide": 1 }, { - "depends_on": "enabled", + "depends_on": "eval:doc.user_type == 'System User' && doc.enabled == 1", "fieldname": "sb1", "fieldtype": "Section Break", "label": "Roles", @@ -391,6 +391,7 @@ }, { "collapsible": 1, + "depends_on": "eval:doc.user_type == 'System User'", "fieldname": "sb_allow_modules", "fieldtype": "Section Break", "label": "Allow Modules", @@ -453,18 +454,18 @@ "label": "Simultaneous Sessions" }, { + "bold": 1, "default": "System User", "description": "If the user has any role checked, then the user becomes a \"System User\". \"System User\" has access to the desktop", "fieldname": "user_type", - "fieldtype": "Select", + "fieldtype": "Link", "in_list_view": 1, "in_standard_filter": 1, "label": "User Type", "oldfieldname": "user_type", "oldfieldtype": "Select", - "options": "System User\nWebsite User", - "permlevel": 1, - "read_only": 1 + "options": "User Type", + "permlevel": 1 }, { "description": "Allow user to login only after this hour (0-24)", @@ -669,7 +670,7 @@ } ], "max_attachments": 5, - "modified": "2021-02-01 16:11:06.037543", + "modified": "2021-02-02 16:11:06.037543", "modified_by": "Administrator", "module": "Core", "name": "User", diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 64ac7f5bca..04d087e82a 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -10,7 +10,8 @@ import frappe.share import frappe.defaults import frappe.permissions from frappe.model.document import Document -from frappe.utils import cint, flt, has_gravatar, escape_html, format_datetime, now_datetime, get_formatted_email, today +from frappe.utils import (cint, flt, has_gravatar, escape_html, format_datetime, + now_datetime, get_formatted_email, today) from frappe import throw, msgprint, _ from frappe.utils.password import update_password as _update_password, check_password, get_password_reset_limit from frappe.desk.notifications import clear_notifications @@ -19,6 +20,7 @@ from frappe.utils.user import get_system_managers from frappe.website.utils import is_signup_enabled from frappe.rate_limiter import rate_limit from frappe.utils.background_jobs import enqueue +from frappe.core.doctype.user_type.user_type import user_linked_with_permission_on_doctype STANDARD_USERS = ("Guest", "Administrator") @@ -186,11 +188,36 @@ class User(Document): _update_password(user=self.name, pwd=new_password, logout_all_sessions=self.logout_all_sessions) def set_system_user(self): - '''Set as System User if any of the given roles has desk_access''' - if self.has_desk_access() or self.name == 'Administrator': - self.user_type = 'System User' + '''For the standard users like admin and guest, the user type is fixed.''' + user_type_mapper = { + 'Administrator': 'System User', + 'Guest': 'Website User' + } + + if self.user_type and not frappe.get_cached_value('User Type', self.user_type, 'is_standard'): + if user_type_mapper.get(self.name): + self.user_type = user_type_mapper.get(self.name) + else: + self.set_roles_and_modules_based_on_user_type() else: - self.user_type = 'Website User' + '''Set as System User if any of the given roles has desk_access''' + self.user_type = 'System User' if self.has_desk_access() else 'Website User' + + def set_roles_and_modules_based_on_user_type(self): + user_type_doc = frappe.get_cached_doc('User Type', self.user_type) + if user_type_doc.role: + self.roles = [] + + # Check whether User has linked with the 'Apply User Permission On' doctype or not + if user_linked_with_permission_on_doctype(user_type_doc, self.name): + self.append('roles', { + 'role': user_type_doc.role + }) + + frappe.msgprint(_('Role has been set as per the user type {0}') + .format(self.user_type), alert=True) + + user_type_doc.update_modules_in_user(self) def has_desk_access(self): '''Return true if any of the set roles has desk access''' @@ -877,7 +904,8 @@ def reset_password(user): def user_query(doctype, txt, searchfield, start, page_len, filters): from frappe.desk.reportview import get_match_cond, get_filters_cond conditions=[] - user_type_condition = "and user_type = 'System User'" + + user_type_condition = "and user_type != 'Website User'" if filters and filters.get('ignore_user_type'): user_type_condition = '' filters.pop('ignore_user_type') diff --git a/frappe/core/doctype/user_document_type/__init__.py b/frappe/core/doctype/user_document_type/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/user_document_type/user_document_type.json b/frappe/core/doctype/user_document_type/user_document_type.json new file mode 100644 index 0000000000..66ba7cd12e --- /dev/null +++ b/frappe/core/doctype/user_document_type/user_document_type.json @@ -0,0 +1,76 @@ +{ + "actions": [], + "creation": "2021-01-13 01:51:40.158521", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "document_type", + "column_break_2", + "is_custom", + "permissions_section", + "read", + "write", + "create" + ], + "fields": [ + { + "fieldname": "document_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Document Type", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "permissions_section", + "fieldtype": "Section Break", + "label": "Role Permissions" + }, + { + "default": "1", + "fieldname": "read", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Read" + }, + { + "default": "0", + "fieldname": "write", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Write" + }, + { + "default": "0", + "fieldname": "create", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Create" + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fetch_from": "document_type.custom", + "fieldname": "is_custom", + "fieldtype": "Check", + "label": "Is Custom", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-01-19 01:25:34.773430", + "modified_by": "Administrator", + "module": "Core", + "name": "User Document Type", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/core/doctype/user_document_type/user_document_type.py b/frappe/core/doctype/user_document_type/user_document_type.py new file mode 100644 index 0000000000..979bfcb250 --- /dev/null +++ b/frappe/core/doctype/user_document_type/user_document_type.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class UserDocumentType(Document): + pass diff --git a/frappe/core/doctype/user_select_document_type/__init__.py b/frappe/core/doctype/user_select_document_type/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/user_select_document_type/user_select_document_type.json b/frappe/core/doctype/user_select_document_type/user_select_document_type.json new file mode 100644 index 0000000000..86e19422c3 --- /dev/null +++ b/frappe/core/doctype/user_select_document_type/user_select_document_type.json @@ -0,0 +1,33 @@ +{ + "actions": [], + "creation": "2021-01-17 18:28:14.208576", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "document_type" + ], + "fields": [ + { + "fieldname": "document_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Document Type", + "options": "DocType", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-01-17 18:45:44.993190", + "modified_by": "Administrator", + "module": "Core", + "name": "User Select Document Type", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/core/doctype/user_select_document_type/user_select_document_type.py b/frappe/core/doctype/user_select_document_type/user_select_document_type.py new file mode 100644 index 0000000000..373eaf7aa3 --- /dev/null +++ b/frappe/core/doctype/user_select_document_type/user_select_document_type.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class UserSelectDocumentType(Document): + pass diff --git a/frappe/core/doctype/user_type/__init__.py b/frappe/core/doctype/user_type/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/user_type/test_user_type.py b/frappe/core/doctype/user_type/test_user_type.py new file mode 100644 index 0000000000..de61e0f476 --- /dev/null +++ b/frappe/core/doctype/user_type/test_user_type.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestUserType(unittest.TestCase): + pass diff --git a/frappe/core/doctype/user_type/user_type.js b/frappe/core/doctype/user_type/user_type.js new file mode 100644 index 0000000000..6ae95fb7df --- /dev/null +++ b/frappe/core/doctype/user_type/user_type.js @@ -0,0 +1,70 @@ +// Copyright (c) 2021, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('User Type', { + refresh: function(frm) { + frm.toggle_display('is_standard', frappe.boot.developer_mode ? true : false); + frm.set_df_property('is_standard', 'read_only', frappe.boot.developer_mode ? false : true); + + const fields = ['role', 'apply_user_permission_on', 'user_id_field', + 'user_doctypes', 'select_doctypes', 'user_type_modules']; + + frm.toggle_display(fields, frm.doc.is_standard ? false : true); + + frm.set_query('document_type', 'user_doctypes', function() { + return { + filters: { + istable: 0 + } + }; + }); + + frm.set_query('document_type', 'select_doctypes', function() { + return { + filters: { + istable: 0, + is_submittable: 0 + } + }; + }); + + frm.set_query('role', function() { + return { + filters: { + is_custom: 1, + disabled: 0, + desk_access: 1 + } + }; + }); + + frm.set_query('apply_user_permission_on', function() { + return { + query: "frappe.core.doctype.user_type.user_type.get_user_linked_doctypes" + }; + }); + }, + + onload: function(frm) { + frm.trigger('get_user_id_fields'); + }, + + apply_user_permission_on: function(frm) { + frm.set_value('user_id_field', ''); + frm.trigger('get_user_id_fields'); + }, + + get_user_id_fields: function(frm) { + if (frm.doc.apply_user_permission_on) { + frappe.call({ + method: 'frappe.core.doctype.user_type.user_type.get_user_id', + args: { + parent: frm.doc.apply_user_permission_on + }, + callback: function(r) { + set_field_options('user_id_field', [""].concat(r.message)); + } + }); + } + } +}); diff --git a/frappe/core/doctype/user_type/user_type.json b/frappe/core/doctype/user_type/user_type.json new file mode 100644 index 0000000000..6b4a13b2e6 --- /dev/null +++ b/frappe/core/doctype/user_type/user_type.json @@ -0,0 +1,128 @@ +{ + "actions": [], + "autoname": "Prompt", + "creation": "2021-01-13 01:48:02.378548", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "is_standard", + "section_break_2", + "role", + "column_break_4", + "apply_user_permission_on", + "user_id_field", + "section_break_6", + "user_doctypes", + "select_doctypes", + "allowed_modules_section", + "user_type_modules" + ], + "fields": [ + { + "default": "0", + "fieldname": "is_standard", + "fieldtype": "Check", + "label": "Is Standard" + }, + { + "depends_on": "eval: !doc.is_standard", + "fieldname": "section_break_2", + "fieldtype": "Section Break", + "label": "Document Types and Permissions" + }, + { + "fieldname": "user_doctypes", + "fieldtype": "Table", + "label": "Document Types", + "mandatory_depends_on": "eval: !doc.is_standard", + "options": "User Document Type" + }, + { + "fieldname": "role", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Role", + "mandatory_depends_on": "eval: !doc.is_standard", + "options": "Role" + }, + { + "fieldname": "select_doctypes", + "fieldtype": "Table", + "label": "Document Types (Select Permissions Only)", + "options": "User Select Document Type" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "description": "Can only list down the document types which has been linked to the User document type.", + "fieldname": "apply_user_permission_on", + "fieldtype": "Link", + "label": "Apply User Permission On", + "mandatory_depends_on": "eval: !doc.is_standard", + "options": "DocType" + }, + { + "depends_on": "eval: !doc.is_standard", + "fieldname": "section_break_6", + "fieldtype": "Section Break", + "hide_border": 1 + }, + { + "depends_on": "apply_user_permission_on", + "fieldname": "user_id_field", + "fieldtype": "Select", + "label": "User Id Field", + "mandatory_depends_on": "eval: !doc.is_standard" + }, + { + "depends_on": "eval: !doc.is_standard", + "fieldname": "allowed_modules_section", + "fieldtype": "Section Break", + "label": "Allowed Modules" + }, + { + "fieldname": "user_type_modules", + "fieldtype": "Table", + "no_copy": 1, + "options": "User Type Module", + "print_hide": 1, + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-01-24 04:47:08.243320", + "modified_by": "Administrator", + "module": "Core", + "name": "User Type", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Administrator", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/core/doctype/user_type/user_type.py b/frappe/core/doctype/user_type/user_type.py new file mode 100644 index 0000000000..62a20cf937 --- /dev/null +++ b/frappe/core/doctype/user_type/user_type.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from six import iteritems +from frappe.utils import get_link_to_form +from frappe.config import get_modules_from_app +from frappe.permissions import add_permission, add_user_permission +from frappe.model.document import Document + +class UserType(Document): + def validate(self): + self.set_modules() + + def on_update(self): + if self.is_standard: return + + self.validate_document_type_limit() + self.validate_role() + self.add_role_permissions_for_user_doctypes() + self.add_role_permissions_for_select_doctypes() + self.update_users() + get_non_standard_user_type_details() + self.remove_permission_for_deleted_doctypes() + + def on_trash(self): + if self.is_standard: + frappe.throw(_('Standard user type {0} can not be deleted.') + .format(frappe.bold(self.name))) + + def set_modules(self): + if not self.user_doctypes: return + + modules = frappe.get_all('DocType', fields=['distinct module as module'], + filters={'name': ('in', [d.document_type for d in self.user_doctypes])}) + + self.set('user_type_modules', []) + for row in modules: + self.append('user_type_modules', { + 'module': row.module + }) + + def validate_document_type_limit(self): + limit = frappe.conf.get('user_type_doctype_limit').get(frappe.scrub(self.name)) or 10 + + if self.user_doctypes and len(self.user_doctypes) > limit: + frappe.throw(_('The total number of user document types limit has been crossed.'), + title=_('User Document Types Limit Exceeded')) + + custom_doctypes = [row.document_type for row in self.user_doctypes if row.is_custom] + if custom_doctypes and len(custom_doctypes) > 3: + frappe.throw(_('You can only set the 3 custom doctypes in the Document Types table.'), + title=_('Custom Document Types Limit Exceeded')) + + def validate_role(self): + if not self.role: + frappe.throw(_("The field {0} is mandatory") + .format(frappe.bold(_('Role')))) + + if not frappe.db.get_value('Role', self.role, 'is_custom'): + frappe.throw(_("The role {0} should be a custom role.") + .format(frappe.bold(get_link_to_form('Role', self.role)))) + + def update_users(self): + for row in frappe.get_all('User', filters = {'user_type': self.name}): + user = frappe.get_cached_doc('User', row.name) + self.update_roles_in_user(user) + self.update_modules_in_user(user) + user.update_children() + + def update_roles_in_user(self, user): + user.set('roles', []) + user.append('roles', { + 'role': self.role + }) + + def update_modules_in_user(self, user): + block_modules = frappe.get_all('Module Def', fields = ['name as module'], + filters={'name': ['not in', [d.module for d in self.user_type_modules]]}) + + if block_modules: + user.set('block_modules', block_modules) + + def add_role_permissions_for_user_doctypes(self): + perms = ['read', 'write', 'create'] + for row in self.user_doctypes: + docperm = add_role_permissions(row.document_type, self.role) + + values = {perm:row.get(perm) for perm in perms} + for perm in ['print', 'email', 'share']: + values[perm] = 1 + + frappe.db.set_value('Custom DocPerm', docperm, values) + + def add_role_permissions_for_select_doctypes(self): + for row in self.select_doctypes: + docperm = add_role_permissions(row.document_type, self.role) + frappe.db.set_value('Custom DocPerm', docperm, + {'select': 1, 'read': 0, 'create': 0, 'write': 0}) + + def remove_permission_for_deleted_doctypes(self): + doctypes = [d.document_type for d in self.user_doctypes] + + for dt in self.select_doctypes: + doctypes.append(dt.document_type) + + for perm in frappe.get_all('Custom DocPerm', + filters = {'role': self.role, 'parent': ['not in', doctypes]}): + frappe.delete_doc('Custom DocPerm', perm.name) + +def add_role_permissions(doctype, role): + name = frappe.get_value('Custom DocPerm', dict(parent=doctype, + role=role, permlevel=0)) + + if not name: + name = add_permission(doctype, role, 0) + + return name + +def get_non_standard_user_type_details(): + user_types = frappe.get_all('User Type', + fields=['apply_user_permission_on', 'name', 'user_id_field'], + filters={'is_standard': 0}) + + if user_types: + user_type_details = {d.name: [d.apply_user_permission_on, d.user_id_field] for d in user_types} + + frappe.cache().set_value('non_standard_user_types', user_type_details) + + return user_type_details + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def get_user_linked_doctypes(doctype, txt, searchfield, start, page_len, filters): + modules = [d.get('module_name') for d in get_modules_from_app('frappe')] + + filters = [['DocField', 'options', '=', 'User'], ['DocType', 'is_submittable', '=', 0], + ['DocType', 'issingle', '=', 0], ['DocType', 'module', 'not in', modules], + ['DocType', 'read_only', '=', 0], ['DocType', 'name', 'like', '%{0}%'.format(txt)]] + + doctypes = frappe.get_all('DocType', fields = ['`tabDocType`.`name`'], filters=filters, + order_by = '`tabDocType`.`idx` desc', limit_start=start, limit_page_length=page_len, as_list=1, debug=1) + + custom_dt_filters = [['Custom Field', 'dt', 'like', '%{0}%'.format(txt)], + ['Custom Field', 'options', '=', 'User'], ['Custom Field', 'fieldtype', '=', 'Link']] + + custom_doctypes = frappe.get_all('Custom Field', fields = ['dt as name'], + filters= custom_dt_filters, as_list=1) + + return doctypes + custom_doctypes + +@frappe.whitelist() +def get_user_id(parent): + data = frappe.get_all('DocField', fields = ['label', 'fieldname as value'], + filters= {'options': 'User', 'fieldtype': 'Link', 'parent': parent}) or [] + + data.extend(frappe.get_all('Custom Field', fields = ['label', 'fieldname as value'], + filters= {'options': 'User', 'fieldtype': 'Link', 'dt': parent})) + + return data + +def user_linked_with_permission_on_doctype(doc, user): + if not doc.apply_user_permission_on: return True + + if not doc.user_id_field: + frappe.throw(_('User Id Field is mandatory in the user type {0}') + .format(frappe.bold(doc.name))) + + if frappe.db.get_value(doc.apply_user_permission_on, + {doc.user_id_field: user}, 'name'): + return True + else: + label = frappe.get_meta(doc.apply_user_permission_on).get_field(doc.user_id_field).label + + frappe.msgprint(_("To set the role {0} in the user {1}, kindly set the {2} field as {3} in one of the {4} record.") + .format(frappe.bold(doc.role), frappe.bold(user), frappe.bold(label), + frappe.bold(user), frappe.bold(doc.apply_user_permission_on))) + + return False + +def apply_permissions_for_non_standard_user_type(doc, method=None): + '''Create user permission for the non standard user type''' + user_types = frappe.cache().get_value('non_standard_user_types') + + if not user_types: + user_types = get_non_standard_user_type_details() + + for user_type, data in iteritems(user_types): + if doc.doctype != data[0]: continue + if frappe.get_cached_value('User', doc.get(data[1]), 'user_type') != user_type: + return + + if (doc.get(data[1]) and (doc.get(data[1]) != doc._doc_before_save.get(data[1]) + or not frappe.db.get_value('User Permission', + {'user': doc.get(data[1]), 'allow': data[0], 'for_value': doc.name}, 'name'))): + + perm_data = frappe.db.get_value('User Permission', + {'allow': doc.doctype, 'for_value': doc.name}, ['name', 'user']) + + if not perm_data: + user_doc = frappe.get_cached_doc('User', doc.get(data[1])) + user_doc.set_roles_and_modules_based_on_user_type() + user_doc.update_children() + add_user_permission(doc.doctype, doc.name, doc.get(data[1])) + else: + frappe.db.set_value('User Permission', perm_data[0], 'user', doc.get(data[1])) \ No newline at end of file diff --git a/frappe/core/doctype/user_type/user_type_dashboard.py b/frappe/core/doctype/user_type/user_type_dashboard.py new file mode 100644 index 0000000000..7e14198bca --- /dev/null +++ b/frappe/core/doctype/user_type/user_type_dashboard.py @@ -0,0 +1,13 @@ +from __future__ import unicode_literals +from frappe import _ + +def get_data(): + return { + 'fieldname': 'user_type', + 'transactions': [ + { + 'label': _('Reference'), + 'items': ['User'] + } + ] + } \ No newline at end of file diff --git a/frappe/core/doctype/user_type/user_type_list.js b/frappe/core/doctype/user_type/user_type_list.js new file mode 100644 index 0000000000..9a9ef417ac --- /dev/null +++ b/frappe/core/doctype/user_type/user_type_list.js @@ -0,0 +1,10 @@ +frappe.listview_settings['User Type'] = { + add_fields: ["is_standard"], + get_indicator: function (doc) { + if (doc.is_standard) { + return [__("Standard"), "green", "is_standard,=,1"]; + } else { + return [__("Custom"), "blue", "is_standard,=,0"]; + } + } +}; diff --git a/frappe/core/doctype/user_type_module/__init__.py b/frappe/core/doctype/user_type_module/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/user_type_module/user_type_module.json b/frappe/core/doctype/user_type_module/user_type_module.json new file mode 100644 index 0000000000..0f9cbefc25 --- /dev/null +++ b/frappe/core/doctype/user_type_module/user_type_module.json @@ -0,0 +1,33 @@ +{ + "actions": [], + "creation": "2021-01-24 03:05:24.634719", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "module" + ], + "fields": [ + { + "fieldname": "module", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Module", + "options": "Module Def", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-01-24 03:07:43.602927", + "modified_by": "Administrator", + "module": "Core", + "name": "User Type Module", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/core/doctype/user_type_module/user_type_module.py b/frappe/core/doctype/user_type_module/user_type_module.py new file mode 100644 index 0000000000..6cd2cbacdb --- /dev/null +++ b/frappe/core/doctype/user_type_module/user_type_module.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class UserTypeModule(Document): + pass diff --git a/frappe/core/page/permission_manager/permission_manager.py b/frappe/core/page/permission_manager/permission_manager.py index be8921e2ff..1c215eb6e1 100644 --- a/frappe/core/page/permission_manager/permission_manager.py +++ b/frappe/core/page/permission_manager/permission_manager.py @@ -30,8 +30,16 @@ def get_roles_and_doctypes(): "restrict_to_domain": ("in", active_domains) }, fields=["name"]) + restricted_roles = ['Administrator'] + if frappe.session.user != 'Administrator': + custom_user_type_roles = frappe.get_all('User Type', filters = {'is_standard': 0}, fields=['role']) + for row in custom_user_type_roles: + restricted_roles.append(row.role) + + restricted_roles.append('All') + roles = frappe.get_all("Role", filters={ - "name": ("not in", "Administrator"), + "name": ("not in", restricted_roles), "disabled": 0, }, or_filters={ "ifnull(restrict_to_domain, '')": "", @@ -54,9 +62,14 @@ def get_permissions(doctype=None, role=None): if doctype: out = [p for p in out if p.parent == doctype] else: - out = frappe.get_all('Custom DocPerm', fields='*', filters=dict(parent = doctype), order_by="permlevel") + filters=dict(parent = doctype) + if frappe.session.user != 'Administrator': + custom_roles = frappe.get_all('Role', filters={'is_custom': 1}) + filters['role'] = ['not in', [row.name for row in custom_roles]] + + out = frappe.get_all('Custom DocPerm', fields='*', filters=filters, order_by="permlevel") if not out: - out = frappe.get_all('DocPerm', fields='*', filters=dict(parent = doctype), order_by="permlevel") + out = frappe.get_all('DocPerm', fields='*', filters=filters, order_by="permlevel") linked_doctypes = {} for d in out: @@ -78,14 +91,14 @@ def add(parent, role, permlevel): @frappe.whitelist() def update(doctype, role, permlevel, ptype, value=None): """Update role permission params - + Args: doctype (str): Name of the DocType to update params for role (str): Role to be updated for, eg "Website Manager". permlevel (int): perm level the provided rule applies to ptype (str): permission type, example "read", "delete", etc. value (None, optional): value for ptype, None indicates False - + Returns: str: Refresh flag is permission is updated successfully """ diff --git a/frappe/hooks.py b/frappe/hooks.py index c06930afd8..74c538c5df 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -147,6 +147,7 @@ doc_events = { "frappe.core.doctype.file.file.attach_files_to_document", "frappe.event_streaming.doctype.event_update_log.event_update_log.notify_consumers", "frappe.automation.doctype.assignment_rule.assignment_rule.update_due_date", + "frappe.core.doctype.user_type.user_type.apply_permissions_for_non_standard_user_type" ], "after_rename": "frappe.desk.notifications.clear_doctype_notifications", "on_cancel": [ diff --git a/frappe/patches.txt b/frappe/patches.txt index 6e94bf0adc..a056df0915 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -333,3 +333,4 @@ frappe.patches.v13_0.delete_package_publish_tool frappe.patches.v13_0.rename_list_view_setting_to_list_view_settings frappe.patches.v13_0.remove_twilio_settings frappe.patches.v12_0.rename_uploaded_files_with_proper_name +frappe.patches.v13_0.make_user_type diff --git a/frappe/patches/v13_0/make_user_type.py b/frappe/patches/v13_0/make_user_type.py new file mode 100644 index 0000000000..dac21c7eec --- /dev/null +++ b/frappe/patches/v13_0/make_user_type.py @@ -0,0 +1,10 @@ +import frappe +from frappe.utils.install import create_user_type + +def execute(): + frappe.reload_doc('core', 'doctype', 'role') + frappe.reload_doc('core', 'doctype', 'user_document_type') + frappe.reload_doc('core', 'doctype', 'user_select_document_type') + frappe.reload_doc('core', 'doctype', 'user_type') + + create_user_type() diff --git a/frappe/permissions.py b/frappe/permissions.py index abb1f6653a..a718ed7169 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -471,7 +471,7 @@ def setup_custom_perms(parent): copy_perms(parent) return True -def add_permission(doctype, role, permlevel=0): +def add_permission(doctype, role, permlevel=0, ptype=None): '''Add a new permission rule to the given doctype for the given Role and Permission Level''' from frappe.core.doctype.doctype.doctype import validate_permissions_for_doctype @@ -481,6 +481,9 @@ def add_permission(doctype, role, permlevel=0): permlevel=permlevel, if_owner=0)): return + if not ptype: + ptype = 'read' + custom_docperm = frappe.get_doc({ "doctype":"Custom DocPerm", "__islocal": 1, @@ -488,13 +491,14 @@ def add_permission(doctype, role, permlevel=0): "parenttype": "DocType", "parentfield": "permissions", "role": role, - 'read': 1, "permlevel": permlevel, + ptype: 1, }) custom_docperm.save() validate_permissions_for_doctype(doctype) + return custom_docperm.name def copy_perms(parent): '''Copy all DocPerm in to Custom DocPerm for the given document''' diff --git a/frappe/utils/install.py b/frappe/utils/install.py index 4ecae440cb..93f46a2a16 100644 --- a/frappe/utils/install.py +++ b/frappe/utils/install.py @@ -18,6 +18,7 @@ def after_install(): # reset installed apps for re-install frappe.db.set_global("installed_apps", '["frappe"]') + create_user_type() install_basic_docs() from frappe.core.doctype.file.file import make_home_folder @@ -49,6 +50,15 @@ def after_install(): frappe.db.commit() +def create_user_type(): + for user_type in ['System User', 'Website User']: + if not frappe.db.exists('User Type', user_type): + frappe.get_doc({ + 'doctype': 'User Type', + 'name': user_type, + 'is_standard': 1 + }).insert(ignore_permissions=True) + def install_basic_docs(): # core users / roles install_docs = [