关于 frappe-core-permissions
面向 Frappe 开发者的权限系统参考技能,梳理角色、记录、字段、钩子和数据遮蔽等控制层,并提供 API 与查询安全模式。
主要功能
- 01五层权限模型说明
- 02权限检查与共享 API
- 03记录和字段级访问控制
- 04权限钩子与查询过滤
- 05数据遮蔽规则
- 06安全查询实践
适用场景
- 01设计 DocType 访问规则
- 02限制用户可见的记录和字段
- 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.mdexamples.mdpermission-api-reference.mdpermission-hooks-reference.mdpermission-types-reference.md详细介绍
Frappe 权限
Frappe 五层权限系统的确定性模式。
权限层
| 层 | 控制内容 | 配置方式 | 版本 |
|---|---|---|---|
| 角色权限 | 用户可以执行的操作 | DocType 权限表 | All |
| 用户权限 | 用户可以查看的记录 | User Permission DocType | All |
| 权限级别 | 用户可以查看/编辑的字段 | 字段 permlevel 属性 | All |
| 权限钩子 | 自定义拒绝逻辑 | hooks.py | All |
| 数据脱敏 | 脱敏后的字段值 | 字段 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 检查 | 适用范围 |
|---|---|---|
read | frappe.has_permission(dt, "read") | 所有 DocType |
write | frappe.has_permission(dt, "write") | 所有 DocType |
create | frappe.has_permission(dt, "create") | 所有 DocType |
delete | frappe.has_permission(dt, "delete") | 所有 DocType |
submit | frappe.has_permission(dt, "submit") | 仅限可提交的 DocType |
cancel | frappe.has_permission(dt, "cancel") | 仅限可提交的 DocType |
amend | frappe.has_permission(dt, "amend") | 仅限可提交的 DocType |
select | frappe.has_permission(dt, "select") | Link 字段 [v14+] |
report | N/A | Report Builder 访问权限 |
export | N/A | Excel/CSV 导出 |
import | N/A | Data Import Tool |
share | N/A | 与其他用户共享 |
print | N/A | 打印/PDF 生成 |
email | N/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)
关键规则
- 始终使用
frappe.has_permission()——绝不直接检查角色来进行访问控制 - 始终对面向用户的查询使用
frappe.get_list()——绝不使用get_all() - 始终在查询钩子中转义 SQL——
frappe.db.escape(user) - 始终在查询钩子中为表名添加前缀——
`tabDocType`.fieldname - 始终在
has_permission钩子中默认返回None——绝不返回True - 始终在权限变更后清除缓存——
frappe.clear_cache() - 始终使用注释说明
ignore_permissions的用法 - 绝不在
has_permission钩子中抛出错误——返回False以拒绝访问 - 绝不在未授予权限级别 0 的情况下授予权限级别 1 及以上的权限
- 绝不假定数据掩码适用于自定义 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 |
版本差异
| 功能 | v14 | v15 | v16 |
|---|---|---|---|
select 权限 | 是 | 是 | 是 |
Desk User 角色 | 否 | 是 | 是 |
数据掩码(mask 字段) | 否 | 否 | 是 |
mask 权限类型 | 否 | 否 | 是 |
| 自定义权限类型 | 否 | 否 | 实验性 |
权限优先级
- Administrator——始终拥有所有权限(无法限制)
- 角色权限——基于已分配的角色
- 用户权限——限制为特定的文档值
- has_permission 钩子——只能拒绝(任何
False都表示拒绝) - 共享——授予对共享文档的访问权限
- 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