I went digging into frappe/frappe/desk/desktop.py
to understand why my custom workspaces werent consistently showing up. Adding Roles to the workspace doesn’t help (this is the buggy part).
The logic is sound but misses a few things. Acceptable because its an “Experimental” feature.
class Workspace:
def __init__(self, page, minimal=False):
# ...
# 1
self.allowed_modules = self.get_cached("user_allowed_modules", self.get_allowed_modules)
self.doc = frappe.get_cached_doc("Workspace", self.page_name)
if (
self.doc
and self.doc.module
and self.doc.module not in self.allowed_modules # 3
and not self.workspace_manager
):
raise frappe.PermissionError # 4
# ...
def get_allowed_modules(self):
if not self.user.allow_modules:
self.user.build_permissions() # 5
return self.user.allow_modules
Explanation
I’ve included numbered reference points
self.allowed_modules
(1) usesself.user.build_permissions()
(5) infrappe/frappe/utils/user.py
to get all the docs your user has permission to.- When your workspace has no doctypes, they will NOT be included in allowed_modules
- This fails the check at no. 3
- Which raises the PermissionError exception used to filter out workspace pages without necessary permissions
Workaround
- Make sure your custom module is assigned a Module. You need to open the custom workspace and do it there.
- Ensure the user has the assigned module as checked under User → Allowed Modules
- Create a stand-in doctype to use to assign matching roles. You can call it whatever you want, no fields necessary.
3.1. You can go further by checking User Cannot Search and User Cannot Create so this dummy doctype doesnt confuse your users.
3.2. Assign user roles you want to access this workspace
3.3. You may want to clear Roles assigned to the workspace if there’s any conflict.
Suggested bug fix
Get all workspace modules whose roles match the user’s roles and add them to allowed_modules
class Workspace:
def __init__(self, page, minimal=False):
self.page_name = page.get("name")
self.page_title = page.get("title")
self.public_page = page.get("public")
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)
# Get workspace modules and add them to allowed_modules
workspace_modules = self.get_cached("user_allowed_workspace_modules", self.get_allowed_workspace_modules)
self.allowed_modules.extend([m for m in workspace_modules if m not in self.allowed_modules])
self.doc = frappe.get_cached_doc("Workspace", self.page_name)
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)
if not minimal:
self.allowed_pages = get_allowed_pages(cache=True)
self.allowed_reports = get_allowed_reports(cache=True)
if self.doc.content:
self.onboarding_list = [
x["data"]["onboarding_name"] for x in loads(self.doc.content) if x["type"] == "onboarding"
]
self.onboardings = []
self.table_counts = get_table_with_counts()
self.restricted_doctypes = (
frappe.cache.get_value("domain_restricted_doctypes") or build_domain_restriced_doctype_cache()
)
self.restricted_pages = (
frappe.cache.get_value("domain_restricted_pages") or build_domain_restriced_page_cache()
)
def get_allowed_workspace_modules(self):
"""Get list of workspace modules that the user has access to.
Checks both Workspace Manager role and roles assigned to workspaces."""
user_roles = frappe.get_roles()
has_workspace_manager = "Workspace Manager" in user_roles
# Get blocked modules
blocked_modules = frappe.get_cached_doc("User", frappe.session.user).get_blocked_modules()
blocked_modules.append("Dummy Module")
# Include pages without domain restriction
allowed_domains = [None, *frappe.get_active_domains()]
if has_workspace_manager:
# Workspace Manager can see all modules
modules = frappe.get_all(
"Workspace",
fields=["distinct module"],
filters={
"restrict_to_domain": ["in", allowed_domains],
"module": ["not in", blocked_modules]
},
ignore_permissions=True
)
else:
# Get modules from workspaces where user has role access
roles_str = "', '".join(user_roles)
modules = frappe.db.sql("""
SELECT DISTINCT w.module
FROM `tabWorkspace` w
LEFT JOIN `tabHas Role` hr ON hr.parent = w.name
WHERE
(w.restrict_to_domain IS NULL OR w.restrict_to_domain IN %(allowed_domains)s)
AND (w.module NOT IN %(blocked_modules)s)
AND (
hr.role IN %(user_roles)s
OR NOT EXISTS (
SELECT 1 FROM `tabHas Role`
WHERE parent = w.name
)
)
""", {
'allowed_domains': allowed_domains,
'blocked_modules': blocked_modules,
'user_roles': user_roles
}, as_dict=1)
return [ws.module for ws in modules if ws.module]
- Needs a some refinement around handling of permission errors