mcpField
F
frappe-core-permissions

frappe-core-permissions

github.com/impertio-studio安全198124 次安装30 周前更新
代码仓库

关于 frappe-core-permissions

面向 Frappe 开发者的权限系统参考技能,梳理角色、记录、字段、钩子和数据遮蔽等控制层,并提供 API 与查询安全模式。

主要功能

  1. 01五层权限模型说明
  2. 02权限检查与共享 API
  3. 03记录和字段级访问控制
  4. 04权限钩子与查询过滤
  5. 05数据遮蔽规则
  6. 06安全查询实践

适用场景

  1. 01设计 DocType 访问规则
  2. 02限制用户可见的记录和字段
  3. 03为列表查询添加权限过滤

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/impertio-studio/frappe_claude_skill_package --skill frappe-core-permissions

安装后在 Claude Code 中输入/frappe-core-permissions确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
anti-patterns.md
examples.md
permission-api-reference.md
permission-hooks-reference.md
permission-types-reference.md

详细介绍

Frappe 权限

Frappe 五层权限系统的确定性模式。


权限层

层控制内容配置方式版本
角色权限用户可以执行的操作DocType 权限表All
用户权限用户可以查看的记录User Permission DocTypeAll
权限级别用户可以查看/编辑的字段字段 permlevel 属性All
权限钩子自定义拒绝逻辑hooks.pyAll
数据脱敏脱敏后的字段值字段 mask 属性[v16+]

决策树

Need to control access?
├── Who can Create/Read/Write/Delete a DocType? → Role Permissions
├── Which specific records can a user see? → User Permissions
├── Which fields should be hidden? → Perm Levels (permlevel 1+)
├── Which fields show masked values? → Data Masking [v16+]
├── Custom runtime deny logic? → has_permission hook
├── Filter list queries dynamically? → permission_query_conditions hook
└── Share one document with one user? → frappe.share

Checking permissions in code?
├── Before action → frappe.has_permission() or doc.has_permission()
├── Raise on denial → doc.check_permission() or throw=True
├── System bypass → doc.flags.ignore_permissions = True (ALWAYS document why)
└── List query → ALWAYS use frappe.get_list() for user-facing data

权限类型

类型API 检查适用范围
readfrappe.has_permission(dt, "read")所有 DocType
writefrappe.has_permission(dt, "write")所有 DocType
createfrappe.has_permission(dt, "create")所有 DocType
deletefrappe.has_permission(dt, "delete")所有 DocType
submitfrappe.has_permission(dt, "submit")仅限可提交的 DocType
cancelfrappe.has_permission(dt, "cancel")仅限可提交的 DocType
amendfrappe.has_permission(dt, "amend")仅限可提交的 DocType
selectfrappe.has_permission(dt, "select")Link 字段 [v14+]
reportN/AReport Builder 访问权限
exportN/AExcel/CSV 导出
importN/AData Import Tool
shareN/A与其他用户共享
printN/A打印/PDF 生成
emailN/A发送电子邮件
mask角色对未脱敏视图的权限数据脱敏 [v16+]

自动分配的角色

角色分配对象备注
Guest所有人(包括匿名用户)公共页面
All所有已注册用户基础身份验证访问权限
Administrator仅 Administrator 用户始终拥有所有权限
Desk User仅限系统用户[v15+]

核心 API

检查权限
# DocType-level
frappe.has_permission("Sales Order", "write")

# Document-level (by name or object)
frappe.has_permission("Sales Order", "write", "SO-00001")
frappe.has_permission("Sales Order", "write", doc=doc)

# For specific user
frappe.has_permission("Sales Order", "read", user="[email protected]")

# Throw on denial
frappe.has_permission("Sales Order", "delete", throw=True)

# Debug mode — prints evaluation steps
frappe.has_permission("Sales Order", "read", debug=True)
print(frappe.local.permission_debug_log)
文档实例方法
doc = frappe.get_doc("Sales Order", "SO-00001")

# Returns bool
if doc.has_permission("write"):
    doc.status = "Approved"
    doc.save()

# Raises frappe.PermissionError if denied
doc.check_permission("write")
获取有效权限
from frappe.permissions import get_doc_permissions

perms = get_doc_permissions(doc)
# {'read': 1, 'write': 1, 'create': 0, 'delete': 0, ...}

perms = get_doc_permissions(doc, user="[email protected]")

用户权限(记录级别)

将用户限制为特定 Link 字段值(例如,特定的 Company、Territory)。

from frappe.permissions import add_user_permission, remove_user_permission

# Restrict user to one company
add_user_permission(
    doctype="Company",
    name="My Company",
    user="[email protected]",
    is_default=1,            # auto-fill in new documents
    applicable_for="Sales Order"  # only for this DocType (optional)
)

# Remove restriction
remove_user_permission("Company", "My Company", "[email protected]")

# Query current restrictions
from frappe.permissions import get_user_permissions
perms = get_user_permissions("[email protected]")
# {"Company": [{"doc": "My Company", "is_default": 1}], ...}

共享(文档级别)

向特定用户授予对单个文档的访问权限。

from frappe.share import add as add_share, remove as remove_share

add_share("Sales Order", "SO-00001", "[email protected]",
          read=1, write=1, share=0, notify=1)

remove_share("Sales Order", "SO-00001", "[email protected]")

# Share with everyone
add_share("Sales Order", "SO-00001", everyone=1, read=1)

字段级权限(权限级别)

按 permlevel(0-9)对字段分组。必须先授予级别 0,才能授予更高级别。

{
  "fields": [
    {"fieldname": "employee_name", "permlevel": 0},
    {"fieldname": "salary",        "permlevel": 1}
  ],
  "permissions": [
    {"role": "Employee",   "permlevel": 0, "read": 1},
    {"role": "HR Manager", "permlevel": 0, "read": 1, "write": 1},
    {"role": "HR Manager", "permlevel": 1, "read": 1, "write": 1}
  ]
}

规则:级别之间不代表层级关系。级别 2 并不比级别 1“更高”。它们是彼此独立的字段组。


数据脱敏 [v16+]

对于没有 mask 权限的用户,带有 mask=1 的字段会显示脱敏值(例如,****、+91-811XXXXXXX)。

{
  "fieldname": "phone_number", "fieldtype": "Data", "mask": 1
}

为必须查看未脱敏值的角色授予 mask 权限:

{"role": "HR Manager", "permlevel": 0, "read": 1, "mask": 1}

重要:数据脱敏不适用于 frappe.db.sql() 或使用原始 SQL 的 Query Reports。必须在自定义 SQL 查询中手动脱敏。


权限钩子

has_permission:自定义拒绝逻辑

只能拒绝访问。绝不能返回 True 来授予访问权限。始终返回 None,以继续执行标准检查。

# hooks.py
has_permission = {
    "Sales Order": "myapp.permissions.check_order_permission"
}
# myapp/permissions.py
def check_order_permission(doc, ptype, user):
    if ptype == "write" and doc.docstatus == 2:
        if "Sales Manager" not in frappe.get_roles(user):
            return False
    return None  # ALWAYS return None by default
permission_query_conditions:筛选列表查询

返回 SQL WHERE 子句片段。仅影响 get_list(),不影响 get_all()。

# hooks.py
permission_query_conditions = {
    "Customer": "myapp.permissions.customer_query"
}
def customer_query(user):
    if not user:
        user = frappe.session.user
    if "Sales Manager" in frappe.get_roles(user):
        return ""
    return f"`tabCustomer`.owner = {frappe.db.escape(user)}"

始终使用 frappe.db.escape()——绝不能将原始用户输入与字符串拼接。


get_list 与 get_all

方法用户权限查询钩子用途
frappe.get_list()应用应用面向用户的查询
frappe.get_all()忽略忽略系统/后台查询

向用户返回数据时,始终使用 get_list()。get_all() 会绕过所有权限筛选。


常见模式

仅所有者可编辑
{"role": "Sales User", "read": 1, "write": 1, "create": 1, "if_owner": 1}
受角色限制的端点
@frappe.whitelist()
def sensitive_action():
    frappe.only_for(["Manager", "Administrator"])
    # Only reaches here if user has one of these roles
绕过权限(说明原因!)
# On document — ALWAYS add a comment explaining the reason
doc.flags.ignore_permissions = True
doc.save()

# On method call
doc.save(ignore_permissions=True)
doc.insert(ignore_permissions=True)

关键规则

  1. 始终使用 frappe.has_permission()——绝不直接检查角色来进行访问控制
  2. 始终对面向用户的查询使用 frappe.get_list()——绝不使用 get_all()
  3. 始终在查询钩子中转义 SQL——frappe.db.escape(user)
  4. 始终在查询钩子中为表名添加前缀——`tabDocType`.fieldname
  5. 始终在 has_permission 钩子中默认返回 None——绝不返回 True
  6. 始终在权限变更后清除缓存——frappe.clear_cache()
  7. 始终使用注释说明 ignore_permissions 的用法
  8. 绝不在 has_permission 钩子中抛出错误——返回 False 以拒绝访问
  9. 绝不在未授予权限级别 0 的情况下授予权限级别 1 及以上的权限
  10. 绝不假定数据掩码适用于自定义 SQL 查询 [v16+]

反模式

不要这样做应该这样做
使用 if "Role" in frappe.get_roles() 进行访问控制使用 frappe.has_permission(dt, ptype)
使用 frappe.get_all() 查询用户数据使用 frappe.get_list()
在 has_permission 钩子中使用 return True使用 return None
在 SQL 中使用 f"owner = '{user}'"使用 f"owner = {frappe.db.escape(user)}"
在权限钩子中使用 frappe.throw()使用 return False
使用 frappe.db.set_value() 进行面向用户的更新在进行权限检查后使用 doc.save()
在错误消息中包含敏感数据使用通用的 frappe.PermissionError

版本差异

功能v14v15v16
select 权限是是是
Desk User 角色否是是
数据掩码(mask 字段)否否是
mask 权限类型否否是
自定义权限类型否否实验性

权限优先级

  1. Administrator——始终拥有所有权限(无法限制)
  2. 角色权限——基于已分配的角色
  3. 用户权限——限制为特定的文档值
  4. has_permission 钩子——只能拒绝(任何 False 都表示拒绝)
  5. 共享——授予对共享文档的访问权限
  6. if_owner——进一步限制为仅可访问自己拥有的文档

参考文件

文件内容
permission-types-reference.md所有权限类型及其选项
permission-api-reference.md包含所有签名的完整 API
permission-hooks-reference.md钩子模式和示例
examples.md可运行的实现示例
anti-patterns.md常见错误及修复方法

相关技能

  • frappe-core-database — 遵循权限规则的数据库操作
  • frappe-core-api — 带权限检查的 API 端点
  • frappe-syntax-controllers — 控制器权限验证
  • frappe-syntax-hooks — 钩子配置模式

已根据 Frappe 文档验证,日期:2026-03-20 | Frappe v14/v15/v16

评论

支持 Markdown