seitime-frappe/frappe/permissions.py
Ankush Menat d0a24e66c5
fix: incorrect caching of is_owner permissions (#21048)
- Create a doctype
- Pick any role and only allow owner to create, read, write.
- after_insert hook re-save the document
- This will fail because there's a function call to `frappe.only_has_select_perm` which doesn't pass the is_owner, this is likely acceptable if document is not available.

Fix: cache should be separate for is_owner

Triggered from but not caused by https://github.com/frappe/frappe/pull/20810
2023-05-19 09:34:48 +05:30

728 lines
21 KiB
Python

# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import copy
import frappe
import frappe.share
from frappe import _, msgprint
from frappe.query_builder import DocType
from frappe.utils import cint, cstr
rights = (
"select",
"read",
"write",
"create",
"delete",
"submit",
"cancel",
"amend",
"print",
"email",
"report",
"import",
"export",
"share",
)
def print_has_permission_check_logs(func):
def inner(*args, **kwargs):
frappe.flags["has_permission_check_logs"] = []
result = func(*args, **kwargs)
self_perm_check = True if not kwargs.get("user") else kwargs.get("user") == frappe.session.user
raise_exception = kwargs.get("raise_exception", True)
# print only if access denied
# and if user is checking his own permission
if not result and self_perm_check and raise_exception:
msgprint(("<br>").join(frappe.flags.get("has_permission_check_logs", [])))
frappe.flags.pop("has_permission_check_logs", None)
return result
return inner
@print_has_permission_check_logs
def has_permission(
doctype,
ptype="read",
doc=None,
verbose=False,
user=None,
raise_exception=True,
*,
parent_doctype=None,
):
"""Returns True if user has permission `ptype` for given `doctype`.
If `doc` is passed, it also checks user, share and owner permissions.
:param doctype: DocType to check permission for
:param ptype: Permission Type to check
:param doc: Check User Permissions for specified document.
:param verbose: DEPRECATED, will be removed in a future release.
:param user: User to check permission for. Defaults to current user.
:param raise_exception:
DOES NOT raise an exception.
If not False, will display a message using frappe.msgprint
which explains why the permission check failed.
:param parent_doctype:
Required when checking permission for a child DocType (unless doc is specified)
"""
if not user:
user = frappe.session.user
if user == "Administrator":
return True
if ptype == "share" and frappe.get_system_settings("disable_document_sharing"):
return False
if not doc and hasattr(doctype, "doctype"):
# first argument can be doc or doctype
doc = doctype
doctype = doc.doctype
if frappe.is_table(doctype):
return has_child_permission(doctype, ptype, doc, user, raise_exception, parent_doctype)
meta = frappe.get_meta(doctype)
if doc:
if isinstance(doc, str):
doc = frappe.get_doc(meta.name, doc)
perm = get_doc_permissions(doc, user=user, ptype=ptype).get(ptype)
if not perm:
push_perm_check_log(
_("User {0} does not have access to this document").format(frappe.bold(user))
)
else:
if ptype == "submit" and not cint(meta.is_submittable):
push_perm_check_log(_("Document Type is not submittable"))
return False
if ptype == "import" and not cint(meta.allow_import):
push_perm_check_log(_("Document Type is not importable"))
return False
role_permissions = get_role_permissions(meta, user=user)
perm = role_permissions.get(ptype)
if not perm:
push_perm_check_log(
_("User {0} does not have doctype access via role permission for document {1}").format(
frappe.bold(user), frappe.bold(doctype)
)
)
def false_if_not_shared():
if ptype in ("read", "write", "share", "submit", "email", "print"):
shared = frappe.share.get_shared(
doctype, user, ["read" if ptype in ("email", "print") else ptype]
)
if doc:
doc_name = get_doc_name(doc)
if doc_name in shared:
if ptype in ("read", "write", "share", "submit") or meta.permissions[0].get(ptype):
return True
elif shared:
# if atleast one shared doc of that type, then return True
# this is used in db_query to check if permission on DocType
return True
return False
if not perm:
perm = false_if_not_shared()
return bool(perm)
def get_doc_permissions(doc, user=None, ptype=None):
"""Returns a dict of evaluated permissions for given `doc` like `{"read":1, "write":1}`"""
if not user:
user = frappe.session.user
if frappe.is_table(doc.doctype):
return {"read": 1, "write": 1}
meta = frappe.get_meta(doc.doctype)
def is_user_owner():
return (doc.get("owner") or "").lower() == user.lower()
if has_controller_permissions(doc, ptype, user=user) is False:
push_perm_check_log("Not allowed via controller permission check")
return {ptype: 0}
permissions = copy.deepcopy(get_role_permissions(meta, user=user, is_owner=is_user_owner()))
if not cint(meta.is_submittable):
permissions["submit"] = 0
if not cint(meta.allow_import):
permissions["import"] = 0
# Override with `if_owner` perms irrespective of user
if permissions.get("has_if_owner_enabled"):
# apply owner permissions on top of existing permissions
# some access might be only for the owner
# eg. everyone might have read access but only owner can delete
permissions.update(permissions.get("if_owner", {}))
if not has_user_permission(doc, user):
if is_user_owner():
# replace with owner permissions
permissions = permissions.get("if_owner", {})
# if_owner does not come with create rights...
permissions["create"] = 0
else:
permissions = {}
return permissions
def get_role_permissions(doctype_meta, user=None, is_owner=None):
"""
Returns dict of evaluated role permissions like
{
"read": 1,
"write": 0,
// if "if_owner" is enabled
"if_owner":
{
"read": 1,
"write": 0
}
}
"""
if isinstance(doctype_meta, str):
doctype_meta = frappe.get_meta(doctype_meta) # assuming doctype name was passed
if not user:
user = frappe.session.user
cache_key = (doctype_meta.name, user, bool(is_owner))
if user == "Administrator":
return allow_everything()
if not frappe.local.role_permissions.get(cache_key):
perms = frappe._dict(if_owner={})
roles = frappe.get_roles(user)
def is_perm_applicable(perm):
return perm.role in roles and cint(perm.permlevel) == 0
def has_permission_without_if_owner_enabled(ptype):
return any(p.get(ptype, 0) and not p.get("if_owner", 0) for p in applicable_permissions)
applicable_permissions = list(
filter(is_perm_applicable, getattr(doctype_meta, "permissions", []))
)
has_if_owner_enabled = any(p.get("if_owner", 0) for p in applicable_permissions)
perms["has_if_owner_enabled"] = has_if_owner_enabled
for ptype in rights:
pvalue = any(p.get(ptype, 0) for p in applicable_permissions)
# check if any perm object allows perm type
perms[ptype] = cint(pvalue)
if (
pvalue
and has_if_owner_enabled
and not has_permission_without_if_owner_enabled(ptype)
and ptype != "create"
):
perms["if_owner"][ptype] = cint(pvalue and is_owner)
# has no access if not owner
# only provide select or read access so that user is able to at-least access list
# (and the documents will be filtered based on owner sin further checks)
perms[ptype] = 1 if ptype in ("select", "read") else 0
frappe.local.role_permissions[cache_key] = perms
return frappe.local.role_permissions[cache_key]
def get_user_permissions(user):
from frappe.core.doctype.user_permission.user_permission import get_user_permissions
return get_user_permissions(user)
def has_user_permission(doc, user=None):
"""Returns True if User is allowed to view considering User Permissions"""
from frappe.core.doctype.user_permission.user_permission import get_user_permissions
user_permissions = get_user_permissions(user)
if not user_permissions:
# no user permission rules specified for this doctype
return True
# user can create own role permissions, so nothing applies
if get_role_permissions("User Permission", user=user).get("write"):
return True
apply_strict_user_permissions = frappe.get_system_settings("apply_strict_user_permissions")
doctype = doc.get("doctype")
docname = doc.get("name")
# STEP 1: ---------------------
# check user permissions on self
if doctype in user_permissions:
allowed_docs = get_allowed_docs_for_doctype(user_permissions.get(doctype, []), doctype)
# if allowed_docs is empty it states that there is no applicable permission under the current doctype
# only check if allowed_docs is not empty
if allowed_docs and docname not in allowed_docs:
# no user permissions for this doc specified
push_perm_check_log(_("Not allowed for {0}: {1}").format(_(doctype), docname))
return False
# STEP 2: ---------------------------------
# check user permissions in all link fields
def check_user_permission_on_link_fields(d):
# check user permissions for all the link fields of the given
# document object d
#
# called for both parent and child records
meta = frappe.get_meta(d.get("doctype"))
# check all link fields for user permissions
for field in meta.get_link_fields():
if field.ignore_user_permissions:
continue
# empty value, do you still want to apply user permissions?
if not d.get(field.fieldname) and not apply_strict_user_permissions:
# nah, not strict
continue
if field.options not in user_permissions:
continue
# get the list of all allowed values for this link
allowed_docs = get_allowed_docs_for_doctype(user_permissions.get(field.options, []), doctype)
if allowed_docs and d.get(field.fieldname) not in allowed_docs:
# restricted for this link field, and no matching values found
# make the right message and exit
if d.get("parentfield"):
# "You are not allowed to access this Employee record because it is linked
# to Company 'Restricted Company' in row 3, field Reference Type"
msg = _(
"You are not allowed to access this {0} record because it is linked to {1} '{2}' in row {3}, field {4}"
).format(
_(meta.doctype),
_(field.options),
d.get(field.fieldname) or _("empty"),
d.idx,
_(field.label) if field.label else field.fieldname,
)
else:
# "You are not allowed to access Company 'Restricted Company' in field Reference Type"
msg = _(
"You are not allowed to access this {0} record because it is linked to {1} '{2}' in field {3}"
).format(
_(meta.doctype),
_(field.options),
d.get(field.fieldname) or _("empty"),
_(field.label) if field.label else field.fieldname,
)
push_perm_check_log(msg)
return False
return True
if not check_user_permission_on_link_fields(doc):
return False
for d in doc.get_all_children():
if not check_user_permission_on_link_fields(d):
return False
return True
def has_controller_permissions(doc, ptype, user=None):
"""Returns controller permissions if defined. None if not defined"""
if not user:
user = frappe.session.user
methods = frappe.get_hooks("has_permission").get(doc.doctype, [])
if not methods:
return None
for method in reversed(methods):
controller_permission = frappe.call(frappe.get_attr(method), doc=doc, ptype=ptype, user=user)
if controller_permission is not None:
return controller_permission
# controller permissions could not decide on True or False
return None
def get_doctypes_with_read():
return list({cstr(p.parent) for p in get_valid_perms() if p.parent})
def get_valid_perms(doctype=None, user=None):
"""Get valid permissions for the current user from DocPerm and Custom DocPerm"""
roles = get_roles(user)
perms = get_perms_for(roles)
custom_perms = get_perms_for(roles, "Custom DocPerm")
doctypes_with_custom_perms = get_doctypes_with_custom_docperms()
for p in perms:
if not p.parent in doctypes_with_custom_perms:
custom_perms.append(p)
if doctype:
return [p for p in custom_perms if p.parent == doctype]
else:
return custom_perms
def get_all_perms(role):
"""Returns valid permissions for a given role"""
perms = frappe.get_all("DocPerm", fields="*", filters=dict(role=role))
custom_perms = frappe.get_all("Custom DocPerm", fields="*", filters=dict(role=role))
doctypes_with_custom_perms = frappe.get_all("Custom DocPerm", pluck="parent", distinct=True)
for p in perms:
if p.parent not in doctypes_with_custom_perms:
custom_perms.append(p)
return custom_perms
def get_roles(user=None, with_standard=True):
"""get roles of current user"""
if not user:
user = frappe.session.user
if user == "Guest" or not user:
return ["Guest"]
def get():
if user == "Administrator":
return frappe.get_all("Role", pluck="name") # return all available roles
else:
table = DocType("Has Role")
roles = (
frappe.qb.from_(table)
.where(
(table.parenttype == "User") & (table.parent == user) & (table.role.notin(["All", "Guest"]))
)
.select(table.role)
.run(pluck=True)
)
return roles + ["All", "Guest"]
roles = frappe.cache().hget("roles", user, get)
# filter standard if required
if not with_standard:
roles = [r for r in roles if r not in ["All", "Guest", "Administrator"]]
return roles
def get_doctype_roles(doctype, access_type="read"):
"""Returns a list of roles that are allowed to access passed doctype."""
meta = frappe.get_meta(doctype)
return [d.role for d in meta.get("permissions") if d.get(access_type)]
def get_perms_for(roles, perm_doctype="DocPerm"):
"""Get perms for given roles"""
filters = {"permlevel": 0, "docstatus": 0, "role": ["in", roles]}
return frappe.get_all(perm_doctype, fields=["*"], filters=filters)
def get_doctypes_with_custom_docperms():
"""Returns all the doctypes with Custom Docperms"""
doctypes = frappe.get_all("Custom DocPerm", fields=["parent"], distinct=1)
return [d.parent for d in doctypes]
def add_user_permission(
doctype,
name,
user,
ignore_permissions=False,
applicable_for=None,
is_default=0,
hide_descendants=0,
):
"""Add user permission"""
from frappe.core.doctype.user_permission.user_permission import user_permission_exists
if not user_permission_exists(user, doctype, name, applicable_for):
if not frappe.db.exists(doctype, name):
frappe.throw(_("{0} {1} not found").format(_(doctype), name), frappe.DoesNotExistError)
frappe.get_doc(
dict(
doctype="User Permission",
user=user,
allow=doctype,
for_value=name,
is_default=is_default,
applicable_for=applicable_for,
apply_to_all_doctypes=0 if applicable_for else 1,
hide_descendants=hide_descendants,
)
).insert(ignore_permissions=ignore_permissions)
def remove_user_permission(doctype, name, user):
user_permission_name = frappe.db.get_value(
"User Permission", dict(user=user, allow=doctype, for_value=name)
)
frappe.delete_doc("User Permission", user_permission_name)
def clear_user_permissions_for_doctype(doctype, user=None):
filters = {"allow": doctype}
if user:
filters["user"] = user
user_permissions_for_doctype = frappe.get_all("User Permission", filters=filters)
for d in user_permissions_for_doctype:
frappe.delete_doc("User Permission", d.name)
def can_import(doctype, raise_exception=False):
if not ("System Manager" in frappe.get_roles() or has_permission(doctype, "import")):
if raise_exception:
raise frappe.PermissionError(f"You are not allowed to import: {doctype}")
else:
return False
return True
def can_export(doctype, raise_exception=False):
if "System Manager" in frappe.get_roles():
return True
else:
role_permissions = frappe.permissions.get_role_permissions(doctype)
has_access = role_permissions.get("export") or role_permissions.get("if_owner").get("export")
if not has_access and raise_exception:
raise frappe.PermissionError(_("You are not allowed to export {} doctype").format(doctype))
return has_access
def update_permission_property(doctype, role, permlevel, ptype, value=None, validate=True):
"""Update a property in Custom Perm"""
from frappe.core.doctype.doctype.doctype import validate_permissions_for_doctype
out = setup_custom_perms(doctype)
name = frappe.get_value("Custom DocPerm", dict(parent=doctype, role=role, permlevel=permlevel))
table = DocType("Custom DocPerm")
frappe.qb.update(table).set(ptype, value).where(table.name == name).run()
if validate:
validate_permissions_for_doctype(doctype)
return out
def setup_custom_perms(parent):
"""if custom permssions are not setup for the current doctype, set them up"""
if not frappe.db.exists("Custom DocPerm", dict(parent=parent)):
copy_perms(parent)
return True
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
setup_custom_perms(doctype)
if frappe.db.get_value(
"Custom DocPerm", dict(parent=doctype, role=role, permlevel=permlevel, if_owner=0)
):
return
if not ptype:
ptype = "read"
custom_docperm = frappe.get_doc(
{
"doctype": "Custom DocPerm",
"__islocal": 1,
"parent": doctype,
"parenttype": "DocType",
"parentfield": "permissions",
"role": role,
"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"""
for d in frappe.get_all("DocPerm", fields="*", filters=dict(parent=parent)):
custom_perm = frappe.new_doc("Custom DocPerm")
custom_perm.update(d)
custom_perm.insert(ignore_permissions=True)
def reset_perms(doctype):
"""Reset permissions for given doctype."""
from frappe.desk.notifications import delete_notification_count_for
delete_notification_count_for(doctype)
frappe.db.delete("Custom DocPerm", {"parent": doctype})
def get_linked_doctypes(dt: str) -> list:
meta = frappe.get_meta(dt)
linked_doctypes = [dt] + [
d.options
for d in meta.get(
"fields",
{"fieldtype": "Link", "ignore_user_permissions": ("!=", 1), "options": ("!=", "[Select]")},
)
]
return list(set(linked_doctypes))
def get_doc_name(doc):
if not doc:
return None
return doc if isinstance(doc, str) else str(doc.name)
def allow_everything():
"""
returns a dict with access to everything
eg. {"read": 1, "write": 1, ...}
"""
perm = {ptype: 1 for ptype in rights}
return perm
def get_allowed_docs_for_doctype(user_permissions, doctype):
"""Returns all the docs from the passed user_permissions that are
allowed under provided doctype"""
return filter_allowed_docs_for_doctype(user_permissions, doctype, with_default_doc=False)
def filter_allowed_docs_for_doctype(user_permissions, doctype, with_default_doc=True):
"""Returns all the docs from the passed user_permissions that are
allowed under provided doctype along with default doc value if with_default_doc is set"""
allowed_doc = []
default_doc = None
for doc in user_permissions:
if not doc.get("applicable_for") or doc.get("applicable_for") == doctype:
allowed_doc.append(doc.get("doc"))
if doc.get("is_default") or len(user_permissions) == 1 and with_default_doc:
default_doc = doc.get("doc")
return (allowed_doc, default_doc) if with_default_doc else allowed_doc
def push_perm_check_log(log):
if frappe.flags.get("has_permission_check_logs") is None:
return
frappe.flags.get("has_permission_check_logs").append(_(log))
def has_child_permission(
child_doctype,
ptype="read",
child_doc=None,
user=None,
raise_exception=True,
parent_doctype=None,
):
if isinstance(child_doc, str):
child_doc = frappe.db.get_value(
child_doctype,
child_doc,
("parent", "parenttype", "parentfield"),
as_dict=True,
)
if child_doc:
parent_doctype = child_doc.parenttype
if not parent_doctype:
push_perm_check_log(
_("Please specify a valid parent DocType for {0}").format(frappe.bold(child_doctype))
)
return False
parent_meta = frappe.get_meta(parent_doctype)
if parent_meta.istable or not (
valid_parentfields := [
df.fieldname for df in parent_meta.get_table_fields() if df.options == child_doctype
]
):
push_perm_check_log(
_("{0} is not a valid parent DocType for {1}").format(
frappe.bold(parent_doctype), frappe.bold(child_doctype)
)
)
return False
if child_doc:
parentfield = child_doc.parentfield
if not parentfield:
push_perm_check_log(
_("Parentfield not specified in {0}: {1}").format(
frappe.bold(child_doctype), frappe.bold(child_doc.name)
)
)
return False
if parentfield not in valid_parentfields:
push_perm_check_log(
_("{0} is not a valid parentfield for {1}").format(
frappe.bold(parentfield), frappe.bold(child_doctype)
)
)
return False
permlevel = parent_meta.get_field(parentfield).permlevel
if permlevel > 0 and permlevel not in parent_meta.get_permlevel_access(ptype, user=user):
push_perm_check_log(
_("Insufficient Permission Level for {0}").format(frappe.bold(parent_doctype))
)
return False
return has_permission(
parent_doctype,
ptype=ptype,
doc=child_doc and getattr(child_doc, "parent_doc", child_doc.parent),
user=user,
raise_exception=raise_exception,
)