fix: Role based access to workspace

This commit is contained in:
shariquerik 2021-07-19 11:04:12 +05:30
parent e8e7211d62
commit 4a05697cfe
6 changed files with 108 additions and 87 deletions

View file

@ -105,8 +105,8 @@ def load_conf_settings(bootinfo):
if key in conf: bootinfo[key] = conf.get(key)
def load_desktop_data(bootinfo):
from frappe.desk.doctype.workspace.workspace import get_pages
bootinfo.allowed_workspaces = get_pages().get('pages')
from frappe.desk.desktop import get_wspace_sidebar_items
bootinfo.allowed_workspaces = get_wspace_sidebar_items().get('pages')
bootinfo.module_page_map = get_controller("Workspace").get_module_page_map()
bootinfo.dashboards = frappe.get_all("Dashboard")

View file

@ -3,10 +3,10 @@
# Author - Shivam Mishra <shivam@frappe.io>
import frappe
import json
from json import loads, dumps
from frappe import _, DoesNotExistError, ValidationError, _dict
from frappe.boot import get_allowed_pages, get_allowed_reports
from frappe.core.doctype.custom_role.custom_role import get_custom_allowed_roles
from functools import wraps
from frappe.cache_manager import (
build_domain_restriced_doctype_cache,
@ -35,13 +35,14 @@ class Workspace:
self.extended_links = []
self.extended_charts = []
self.extended_shortcuts = []
self.workspace_manager = "Workspace Manager" in frappe.get_roles()
self.user = frappe.get_user()
self.allowed_modules = self.get_cached('user_allowed_modules', self.get_allowed_modules)
self.doc = self.get_page_for_user()
self.doc = frappe.get_cached_doc("Workspace", self.page_name)
if self.doc.module and self.doc.module not in self.allowed_modules:
if self.doc and self.doc.module and self.doc.module not in self.allowed_modules and not self.workspace_manager:
raise frappe.PermissionError
self.can_read = self.get_cached('user_perm_can_read', self.get_can_read_items)
@ -58,8 +59,8 @@ class Workspace:
self.restricted_pages = frappe.cache().get_value("domain_restricted_pages") or build_domain_restriced_page_cache()
def is_page_allowed(self):
cards = self.doc.get_link_groups() + get_custom_reports_and_doctypes(self.doc.module) + self.extended_links
shortcuts = self.doc.shortcuts + self.extended_shortcuts
cards = self.doc.get_link_groups() + get_custom_reports_and_doctypes(self.doc.module)
shortcuts = self.doc.shortcuts
for section in cards:
links = loads(section.get('links')) if isinstance(section.get('links'), str) else section.get('links')
@ -77,8 +78,28 @@ class Workspace:
if self.is_item_allowed(item.link_to, item.type) and _in_active_domains(item):
return True
if not shortcuts and not self.doc.links:
return True
return False
def is_permitted(self):
"""Returns true if Has Role is not set or the user is allowed."""
from frappe.utils import has_common
allowed = [d.role for d in frappe.get_all("Has Role", fields=["role"], filters={"parent": self.doc.name})]
custom_roles = get_custom_allowed_roles('page', self.doc.name)
allowed.extend(custom_roles)
if not allowed:
return True
roles = frappe.get_roles()
if has_common(roles, allowed):
return True
def get_cached(self, cache_key, fallback_fn):
_cache = frappe.cache()
@ -104,24 +125,6 @@ class Workspace:
return self.user.allow_modules
def get_page_for_user(self):
if self.public_page:
filters = {
'label': self.page_name,
'public': 1
}
public_pages = frappe.get_all("Workspace", filters=filters, limit=1)
if public_pages:
return frappe.get_cached_doc("Workspace", public_pages[0])
filters = {
'label': self.page_title + "-" + frappe.session.user,
'for_user': frappe.session.user
}
user_pages = frappe.get_all("Workspace", filters=filters, limit=1)
if user_pages:
return frappe.get_cached_doc("Workspace", user_pages[0])
def get_onboarding_doc(self):
# Check if onboarding is enabled
if not frappe.get_system_settings("enable_onboarding"):
@ -372,39 +375,45 @@ def get_desktop_page(page):
return {}
@frappe.whitelist()
def get_desk_sidebar_items():
def get_wspace_sidebar_items():
"""Get list of sidebar items for desk"""
has_access = "Workspace Manager" in frappe.get_roles()
# don't get domain restricted pages
blocked_modules = frappe.get_doc('User', frappe.session.user).get_blocked_modules()
blocked_modules.append('Dummy Module')
filters = {
'restrict_to_domain': ['in', frappe.get_active_domains()],
'extends_another_page': 0,
'for_user': '',
'module': ['not in', blocked_modules]
}
if not frappe.local.conf.developer_mode:
filters['developer_mode_only'] = '0'
if has_access:
filters = []
# pages sorted based on pinned to top and then by name
order_by = "pin_to_top desc, pin_to_bottom asc, name asc"
all_pages = frappe.get_all("Workspace", fields=["name", "title", "public", "module", "icon"],
filters=filters, order_by=order_by, ignore_permissions=True)
# pages sorted based on sequence id
order_by = "sequence_id asc"
fields = ["name", "title", "for_user", "parent_page", "content", "public", "module", "icon"]
all_pages = frappe.get_all("Workspace", fields=fields, filters=filters, order_by=order_by, ignore_permissions=True)
pages = []
private_pages = []
# Filter Page based on Permission
for page in all_pages:
try:
wspace = Workspace(page)
if wspace.is_page_allowed():
pages.append(page)
if wspace.is_permitted() and wspace.is_page_allowed() or has_access:
if page.public:
pages.append(page)
elif page.for_user == frappe.session.user:
private_pages.append(page)
page['label'] = _(page.get('name'))
except frappe.PermissionError:
pass
if private_pages:
pages.extend(private_pages)
return pages
return {'pages': pages, 'has_access': has_access}
def get_table_with_counts():
counts = frappe.cache().get_value("information_schema:counts")
@ -575,7 +584,7 @@ def clean_up(original_page, blocks):
for wid in ['shortcut', 'card', 'chart']:
# get list of widget's name from blocks
page_widgets[wid] = [x['data'][wid + '_name'] for x in json.loads(blocks) if x['type'] == wid]
page_widgets[wid] = [x['data'][wid + '_name'] for x in loads(blocks) if x['type'] == wid]
# shortcut & chart cleanup
for wid in ['shortcut', 'chart']:

View file

@ -38,7 +38,8 @@
"shortcuts",
"section_break_18",
"cards_label",
"links"
"links",
"roles"
],
"fields": [
{
@ -252,10 +253,16 @@
"fieldname": "sequence_id",
"fieldtype": "Int",
"label": "Sequence Id"
},
{
"fieldname": "roles",
"fieldtype": "Table",
"label": "Roles",
"options": "Has Role"
}
],
"links": [],
"modified": "2021-07-05 17:41:36.272294",
"modified": "2021-07-15 14:20:37.623926",
"modified_by": "Administrator",
"module": "Desk",
"name": "Workspace",
@ -269,7 +276,7 @@
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"role": "Workspace Manager",
"share": 1,
"write": 1
},

View file

@ -3,7 +3,6 @@
# For license information, please see license.txt
import frappe
import json
from frappe import _
from frappe.modules.export_file import export_to_files
from frappe.model.document import Document
@ -26,8 +25,8 @@ class Workspace(Document):
frappe.throw(_("You can only have one default page that extends a particular standard page."))
def on_update(self):
# if disable_saving_as_standard():
# return
if disable_saving_as_standard():
return
if frappe.conf.developer_mode and self.module and self.public:
export_to_files(record_list=[['Workspace', self.name]], record_module=self.module)
@ -159,20 +158,7 @@ def get_report_type(report):
@frappe.whitelist()
def get_pages():
has_access = "System Manager" in frappe.get_roles()
fields = ['name', 'title', 'icon', 'public', 'parent_page', 'content']
pages = get_page_list(fields, {'public': 1})
private_pages = get_page_list(fields, {'for_user': frappe.session.user})
if private_pages:
pages.extend(private_pages)
return {'pages': pages, 'has_access': has_access}
@frappe.whitelist()
def save_page(title, parent, public, sb_items, deleted_pages, new_widgets, blocks, save):
def save_page(title, parent, public, sb_public_items, sb_private_items, deleted_pages, new_widgets, blocks, save):
save = frappe.parse_json(save)
public = frappe.parse_json(public)
if save:
@ -206,20 +192,20 @@ def save_page(title, parent, public, sb_items, deleted_pages, new_widgets, block
doc.content = blocks
doc.save(ignore_permissions=True)
if json.loads(new_widgets):
if loads(new_widgets):
save_new_widget(doc, title, blocks, new_widgets)
if json.loads(sb_items):
sort_pages(json.loads(sb_items))
if loads(sb_public_items) or loads(sb_private_items):
sort_pages(loads(sb_public_items), loads(sb_private_items))
if json.loads(deleted_pages):
return delete_pages(json.loads(deleted_pages))
if loads(deleted_pages):
return delete_pages(loads(deleted_pages))
return {"name": title, "public": public}
def delete_pages(deleted_pages):
for page in deleted_pages:
if page.get("public") and "System Manager" not in frappe.get_roles():
if page.get("public") and "Workspace Manager" not in frappe.get_roles():
return {"name": page.get("title"), "public": 1}
if frappe.db.exists("Workspace", page.get("name")):
@ -227,17 +213,15 @@ def delete_pages(deleted_pages):
return {"name": "Home", "public": 1}
def sort_pages(sb_items):
public_pages = [page for page in sb_items if page.get('public')=='1']
private_pages = [page for page in sb_items if page.get('public')=='0']
def sort_pages(sb_public_items, sb_private_items):
wspace_public_pages = get_page_list(['name', 'title'], {'public': 1})
wspace_private_pages = get_page_list(['name', 'title'], {'for_user': frappe.session.user})
sort_page(wspace_private_pages, private_pages)
if sb_private_items:
sort_page(wspace_private_pages, sb_private_items)
if "System Manager" in frappe.get_roles():
sort_page(wspace_public_pages, public_pages)
if sb_public_items and "Workspace Manager" in frappe.get_roles():
sort_page(wspace_public_pages, sb_public_items)
def sort_page(wspace_pages, pages):
for seq, d in enumerate(pages):

View file

@ -24,7 +24,8 @@ def create_content(doc):
del doc.charts[doc.charts.index(l)]
if doc.shortcuts:
invalid_links = []
content.append({"type":"spacer","data":{"col":12,"pt":0,"pr":0,"pb":0,"pl":0}})
if doc.charts:
content.append({"type":"spacer","data":{"col":12,"pt":0,"pr":0,"pb":0,"pl":0}})
content.append({"type":"header","data":{"text":doc.shortcuts_label or _("Your Shortcuts"),"level":4,"col":12,"pt":0,"pr":0,"pb":0,"pl":0}})
for s in doc.shortcuts:
if s.get_invalid_links()[0]:

View file

@ -22,7 +22,8 @@ frappe.views.Workspace = class Workspace {
this.page = wrapper.page;
this.isReadOnly = true;
this.new_page = null;
this.sorted_sidebar_items = [];
this.sorted_public_items = [];
this.sorted_private_items = [];
this.deleted_sidebar_items = [];
this.current_page = {};
this.sidebar_items = {
@ -30,8 +31,8 @@ frappe.views.Workspace = class Workspace {
'private': {}
};
this.sidebar_categories = [
"Public",
frappe.user.first_name()
'Public',
frappe.user.first_name() || 'Private'
];
this.tools = {
header: {
@ -113,7 +114,7 @@ frappe.views.Workspace = class Workspace {
}
get_pages() {
return frappe.xcall("frappe.desk.doctype.workspace.workspace.get_pages");
return frappe.xcall("frappe.desk.desktop.get_wspace_sidebar_items");
}
sidebar_item_container(item) {
@ -124,7 +125,7 @@ frappe.views.Workspace = class Workspace {
href="/app/${item.public ? frappe.router.slug(item.title) : 'private/'+frappe.router.slug(item.title) }"
class="item-anchor ${item.is_editable ? "" : "block-click" }" title="${item.title}"
>
<span>${frappe.utils.icon(item.icon || "folder-normal", "md")}</span>
<span class="sidebar-item-icon" item-icon=${item.icon || "folder-normal"}>${frappe.utils.icon(item.icon || "folder-normal", "md")}</span>
<span class="sidebar-item-label">${item.title}<span>
</a>
<div class="sidebar-item-control"></div>
@ -146,7 +147,9 @@ frappe.views.Workspace = class Workspace {
this.build_sidebar_section(category, root_pages);
});
this.sidebar.find('.selected')[0].scrollIntoView();
// Scroll sidebar to selected page if it is not in viewport.
!frappe.dom.is_element_in_viewport(this.sidebar.find('.selected'))
&& this.sidebar.find('.selected')[0].scrollIntoView();
}
build_sidebar_section(title, root_pages) {
@ -448,26 +451,36 @@ frappe.views.Workspace = class Workspace {
animation: 150,
fallbackOnBody: true,
swapThreshold: 0.65,
onEnd: function () {
me.prepare_sorted_sidebar();
onEnd: function (evt) {
let is_public = $(evt.item).attr('item-public') == '1';
me.prepare_sorted_sidebar(is_public);
}
});
});
}
prepare_sorted_sidebar() {
this.sorted_sidebar_items = [];
for (let page of $('.standard-sidebar-section').find('.sidebar-item-container')) {
prepare_sorted_sidebar(is_public) {
if (is_public) {
this.sorted_public_items = this.sort_sidebar(this.sidebar.find('.standard-sidebar-section').first());
} else {
this.sorted_private_items = this.sort_sidebar(this.sidebar.find('.standard-sidebar-section').last());
}
}
sort_sidebar($sidebar_section) {
let sorted_items = [];
for (let page of $sidebar_section.find('.sidebar-item-container')) {
let parent_page = "";
if (page.closest('.nested-container').classList.contains('sidebar-child-item')) {
parent_page = page.parentElement.parentElement.attributes["item-name"].value;
}
this.sorted_sidebar_items.push({
sorted_items.push({
title: page.attributes['item-name'].value,
parent_page: parent_page,
public: page.attributes['item-public'].value
});
}
return sorted_items;
}
make_blocks_sortable() {
@ -542,11 +555,16 @@ frappe.views.Workspace = class Workspace {
this.show_sidebar_actions();
this.make_sidebar_sortable();
this.make_blocks_sortable();
this.prepare_sorted_sidebar();
this.prepare_sorted_sidebar(values.is_public);
});
}
});
d.show();
// to enable focusing on input field when modal is open.
d.$wrapper.on('shown.bs.modal', function() {
$(document).off('focusin.modal');
});
}
validate_page(values) {
@ -652,7 +670,8 @@ frappe.views.Workspace = class Workspace {
title: me.title,
parent: me.parent || '',
public: me.public || 0,
sb_items: me.sorted_sidebar_items,
sb_public_items: me.sorted_public_items,
sb_private_items: me.sorted_private_items,
deleted_pages: me.deleted_sidebar_items,
new_widgets: new_widgets,
blocks: JSON.stringify(outputData.blocks),
@ -666,7 +685,8 @@ frappe.views.Workspace = class Workspace {
me.title = '';
me.parent = '';
me.public = false;
me.sorted_sidebar_items = [];
me.sorted_public_items = [];
me.sorted_private_items = [];
me.deleted_sidebar_items = [];
me.reload();
}