erpnext-permissions
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseERPNext Permissions Skill
ERPNext 权限技能指南
Deterministic patterns for implementing robust permission systems in Frappe/ERPNext applications.
在Frappe/ERPNext应用中实现可靠权限系统的标准化模式。
Overview
概述
Frappe's permission system has five layers:
| Layer | Controls | Configured Via | Version |
|---|---|---|---|
| Role Permissions | What users CAN do | DocType permissions table | All |
| User Permissions | WHICH documents users see | User Permission records | All |
| Perm Levels | WHICH fields users see | Field permlevel property | All |
| Permission Hooks | Custom logic | hooks.py | All |
| Data Masking | MASKED field values | Field mask property | v16+ |
Frappe的权限系统包含五个层级:
| 层级 | 控制范围 | 配置方式 | 版本 |
|---|---|---|---|
| 角色权限 | 用户可执行的操作 | 文档类型(DocType)权限表 | 所有版本 |
| 用户权限 | 用户可查看的具体文档 | 用户权限记录 | 所有版本 |
| 权限级别(Perm Levels) | 用户可查看的具体字段 | 字段的permlevel属性 | 所有版本 |
| 权限钩子 | 自定义逻辑 | hooks.py | 所有版本 |
| 数据脱敏 | 字段值的脱敏显示 | 字段的mask属性 | v16+ |
Quick Reference
快速参考
Permission Types
权限类型
| Type | Check | For |
|---|---|---|
| | View document |
| | Edit document |
| | Create new |
| | Delete |
| | Submit (submittable only) |
| | Cancel |
| | Select in Link (v14+) |
| Role permission for unmasked view | View unmasked data (v16+) |
| 类型 | 检查方式 | 适用场景 |
|---|---|---|
| | 查看文档 |
| | 编辑文档 |
| | 创建新文档 |
| | 删除文档 |
| | 提交文档(仅适用于可提交的文档类型) |
| | 取消文档 |
| | 在链接字段中选择(v14+) |
| 角色的无脱敏查看权限 | 查看未脱敏数据(v16+) |
Automatic Roles
自动分配角色
| Role | Assigned To |
|---|---|
| Everyone (including anonymous) |
| All registered users |
| Only Administrator user |
| System Users (v15+) |
| 角色 | 分配对象 |
|---|---|
| 所有用户(包括匿名用户) |
| 所有注册用户 |
| 仅管理员用户 |
| 系统用户(v15+) |
Essential API
核心API
Check Permission
检查权限
python
undefinedpython
undefinedDocType level
文档类型级别
frappe.has_permission("Sales Order", "write")
frappe.has_permission("Sales Order", "write")
Document level
文档实例级别
frappe.has_permission("Sales Order", "write", "SO-00001")
frappe.has_permission("Sales Order", "write", doc=doc)
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="john@example.com")
frappe.has_permission("Sales Order", "read", user="john@example.com")
Throw on denial
无权限时抛出异常
frappe.has_permission("Sales Order", "delete", throw=True)
frappe.has_permission("Sales Order", "delete", throw=True)
On document instance
在文档实例上检查
doc = frappe.get_doc("Sales Order", "SO-00001")
if doc.has_permission("write"):
doc.status = "Approved"
doc.save()
doc = frappe.get_doc("Sales Order", "SO-00001")
if doc.has_permission("write"):
doc.status = "Approved"
doc.save()
Raise error if no permission
无权限时直接抛出错误
doc.check_permission("write")
undefineddoc.check_permission("write")
undefinedGet Permissions
获取权限
python
from frappe.permissions import get_doc_permissionspython
from frappe.permissions import get_doc_permissionsGet all permissions for document
获取文档的所有权限
perms = get_doc_permissions(doc)
perms = get_doc_permissions(doc)
{'read': 1, 'write': 1, 'create': 0, 'delete': 0, ...}
{'read': 1, 'write': 1, 'create': 0, 'delete': 0, ...}
undefinedundefinedUser Permissions
用户权限管理
python
from frappe.permissions import add_user_permission, remove_user_permissionpython
from frappe.permissions import add_user_permission, remove_user_permissionRestrict user to specific company
限制用户仅访问特定公司
add_user_permission(
doctype="Company",
name="My Company",
user="john@example.com",
is_default=1
)
add_user_permission(
doctype="Company",
name="My Company",
user="john@example.com",
is_default=1
)
Remove restriction
移除访问限制
remove_user_permission("Company", "My Company", "john@example.com")
remove_user_permission("Company", "My Company", "john@example.com")
Get user's permissions
获取用户的权限列表
from frappe.permissions import get_user_permissions
perms = get_user_permissions("john@example.com")
undefinedfrom frappe.permissions import get_user_permissions
perms = get_user_permissions("john@example.com")
undefinedSharing
文档共享
python
from frappe.share import add as add_sharepython
from frappe.share import add as add_shareShare document with user
将文档共享给指定用户
add_share(
doctype="Sales Order",
name="SO-00001",
user="jane@example.com",
read=1,
write=1
)
---add_share(
doctype="Sales Order",
name="SO-00001",
user="jane@example.com",
read=1,
write=1
)
---Data Masking (v16+)
数据脱敏(v16+)
Data Masking protects sensitive field values while keeping fields visible. Users without permission see masked values (e.g., , ).
mask****+91-811XXXXXXX数据脱敏功能在保持字段可见的同时保护敏感字段值。没有权限的用户会看到脱敏后的值(例如、)。
mask****+91-811XXXXXXXUse Cases
适用场景
- HR: Show employee details but mask salary amounts
- Support: Show phone numbers partially masked
- Finance: Show bank account fields without full numbers
- 人力资源:显示员工基本信息但隐藏薪资金额
- 客服支持:显示部分脱敏的电话号码
- 财务部门:显示银行账户字段但隐藏完整账号
Enable Data Masking
启用数据脱敏
Via DocType (Developer Mode) or Customize Form:
json
{
"fieldname": "phone_number",
"fieldtype": "Data",
"options": "Phone",
"mask": 1
}Supported Field Types:
- Data, Date, Datetime
- Currency, Float, Int, Percent
- Phone, Password
- Link, Dynamic Link
- Select, Read Only, Duration
通过文档类型(开发者模式)或自定义表单配置:
json
{
"fieldname": "phone_number",
"fieldtype": "Data",
"options": "Phone",
"mask": 1
}支持的字段类型:
- 文本(Data)、日期(Date)、日期时间(Datetime)
- 货币(Currency)、浮点数(Float)、整数(Int)、百分比(Percent)
- 电话(Phone)、密码(Password)
- 链接(Link)、动态链接(Dynamic Link)
- 选择框(Select)、只读(Read Only)、时长(Duration)
Configure Permission
配置权限
Add permission to roles that should see unmasked data:
maskjson
{
"permissions": [
{"role": "Employee", "permlevel": 0, "read": 1},
{"role": "HR Manager", "permlevel": 0, "read": 1, "mask": 1}
]
}为需要查看未脱敏数据的角色添加权限:
maskjson
{
"permissions": [
{"role": "Employee", "permlevel": 0, "read": 1},
{"role": "HR Manager", "permlevel": 0, "read": 1, "mask": 1}
]
}How It Works
工作流程
┌─────────────────────────────────────────────────────────────────────┐
│ DATA MASKING FLOW │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. Field has mask=1 in DocField configuration │
│ │
│ 2. System checks: meta.has_permlevel_access_to( │
│ fieldname=df.fieldname, │
│ df=df, │
│ permission_type="mask" │
│ ) │
│ │
│ 3. If user LACKS mask permission: │
│ └─► Value automatically masked in: │
│ • Form views │
│ • List views │
│ • Report views │
│ • API responses (/api/resource/, /api/method/) │
│ │
│ 4. If user HAS mask permission: │
│ └─► Full value displayed │
│ │
└─────────────────────────────────────────────────────────────────────┘┌─────────────────────────────────────────────────────────────────────┐
│ 数据脱敏流程 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 字段在DocField配置中设置mask=1 │
│ │
│ 2. 系统检查:meta.has_permlevel_access_to( │
│ fieldname=df.fieldname, │
│ df=df, │
│ permission_type="mask" │
│ ) │
│ │
│ 3. 如果用户没有mask权限: │
│ └─► 值会自动在以下场景脱敏显示: │
│ • 表单视图 │
│ • 列表视图 │
│ • 报表视图 │
│ • API响应(/api/resource/, /api/method/) │
│ │
│ 4. 如果用户拥有mask权限: │
│ └─► 显示完整值 │
│ │
└─────────────────────────────────────────────────────────────────────┘⚠️ Critical: Custom SQL Queries
⚠️ 重要提示:自定义SQL查询
Data Masking does NOT apply to:
- Custom SQL queries
- Query Reports using raw SQL
- Direct calls
frappe.db.sql()
You must implement masking manually:
python
def get_customer_report(filters):
data = frappe.db.sql("""
SELECT name, phone, email FROM tabCustomer
""", as_dict=True)
# Manual masking for users without permission
if not frappe.has_permission("Customer", "mask"):
for row in data:
if row.phone:
row.phone = mask_phone(row.phone)
return data
def mask_phone(phone):
"""Mask phone number: +91-81123XXXXX"""
if len(phone) > 5:
return phone[:6] + "X" * (len(phone) - 6)
return "****"数据脱敏不会自动应用于:
- 自定义SQL查询
- 使用原生SQL的报表查询
- 直接调用的场景
frappe.db.sql()
你必须手动实现脱敏逻辑:
python
def get_customer_report(filters):
data = frappe.db.sql("""
SELECT name, phone, email FROM tabCustomer
""", as_dict=True)
# 对无权限用户手动脱敏
if not frappe.has_permission("Customer", "mask"):
for row in data:
if row.phone:
row.phone = mask_phone(row.phone)
return data
def mask_phone(phone):
"""脱敏电话号码:+91-81123XXXXX"""
if len(phone) > 5:
return phone[:6] + "X" * (len(phone) - 6)
return "****"Permission Hooks
权限钩子
has_permission Hook
has_permission钩子
Add custom permission logic. Can only deny, not grant.
python
undefined添加自定义权限逻辑,仅能拒绝权限,无法授予权限。
python
undefinedhooks.py
hooks.py
has_permission = {
"Sales Order": "myapp.permissions.check_order_permission"
}
```pythonhas_permission = {
"Sales Order": "myapp.permissions.check_order_permission"
}
```pythonmyapp/permissions.py
myapp/permissions.py
def check_order_permission(doc, ptype, user):
"""
Returns:
None: Continue standard checks
False: Deny permission
"""
# Deny editing cancelled orders for non-managers
if ptype == "write" and doc.docstatus == 2:
if "Sales Manager" not in frappe.get_roles(user):
return False
return None # ALWAYS return None by defaultundefineddef check_order_permission(doc, ptype, user):
"""
返回值:
None: 继续执行标准权限检查
False: 拒绝权限
"""
# 拒绝非经理用户编辑已取消的订单
if ptype == "write" and doc.docstatus == 2:
if "Sales Manager" not in frappe.get_roles(user):
return False
return None # 默认情况下必须返回Noneundefinedpermission_query_conditions Hook
permission_query_conditions钩子
Filter list queries. Only affects , NOT .
get_list()get_all()python
undefined过滤列表查询结果,仅影响,不影响。
get_list()get_all()python
undefinedhooks.py
hooks.py
permission_query_conditions = {
"Customer": "myapp.permissions.customer_query"
}
```pythonpermission_query_conditions = {
"Customer": "myapp.permissions.customer_query"
}
```pythonmyapp/permissions.py
myapp/permissions.py
def customer_query(user):
"""Return SQL WHERE clause fragment."""
if not user:
user = frappe.session.user
# Managers see all
if "Sales Manager" in frappe.get_roles(user):
return ""
# Others see only their customers
return f"`tabCustomer`.owner = {frappe.db.escape(user)}"
**CRITICAL**: Always use `frappe.db.escape()` - never string concatenation!
---def customer_query(user):
"""返回SQL WHERE子句片段。"""
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 vs get_all
get_list 与 get_all 的区别
| Method | User Permissions | Query Hook |
|---|---|---|
| ✅ Applied | ✅ Applied |
| ❌ Ignored | ❌ Ignored |
python
undefined| 方法 | 是否应用用户权限 | 是否应用查询钩子 |
|---|---|---|
| ✅ 应用 | ✅ 应用 |
| ❌ 忽略 | ❌ 忽略 |
python
undefinedUser-facing query - respects permissions
面向用户的查询 - 遵循权限控制
docs = frappe.get_list("Sales Order", filters={"status": "Open"})
docs = frappe.get_list("Sales Order", filters={"status": "Open"})
System query - bypasses permissions
系统级查询 - 绕过权限控制
docs = frappe.get_all("Sales Order", filters={"status": "Open"})
---docs = frappe.get_all("Sales Order", filters={"status": "Open"})
---Field-Level Permissions (Perm Levels)
字段级权限(权限级别)
Configure Field
配置字段
json
{
"fieldname": "salary",
"fieldtype": "Currency",
"permlevel": 1
}json
{
"fieldname": "salary",
"fieldtype": "Currency",
"permlevel": 1
}Configure Role Access
配置角色访问权限
json
{
"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}
]
}Rule: Level 0 MUST be granted before higher levels.
json
{
"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}
]
}规则:必须先授予0级权限,才能授予更高层级的权限。
Decision Tree
决策树
Need to control access?
├── To entire DocType → Role Permissions
├── To specific documents → User Permissions
├── To specific fields (hide completely) → Perm Levels
├── To specific fields (show masked) → Data Masking (v16+)
├── With custom logic → has_permission hook
└── For list queries → permission_query_conditions hook
Checking permissions in code?
├── Before action → frappe.has_permission() or doc.has_permission()
├── Raise error → doc.check_permission() or throw=True
└── Bypass needed → doc.flags.ignore_permissions = True (document why!)需要控制访问权限?
├── 针对整个文档类型 → 角色权限
├── 针对特定文档 → 用户权限
├── 针对特定字段(完全隐藏) → 权限级别
├── 针对特定字段(显示脱敏值) → 数据脱敏(v16+)
├── 需要自定义逻辑 → has_permission钩子
└── 针对列表查询 → permission_query_conditions钩子
在代码中检查权限?
├── 执行操作前检查 → frappe.has_permission() 或 doc.has_permission()
├── 需要抛出错误 → doc.check_permission() 或 throw=True
└── 需要绕过权限 → doc.flags.ignore_permissions = True(务必注明原因!)Common Patterns
常见实现模式
Owner-Only Edit
仅所有者可编辑
json
{
"role": "Sales User",
"read": 1, "write": 1, "create": 1,
"if_owner": 1
}json
{
"role": "Sales User",
"read": 1, "write": 1, "create": 1,
"if_owner": 1
}Check Before Action
执行操作前检查权限
python
@frappe.whitelist()
def approve_order(order_name):
doc = frappe.get_doc("Sales Order", order_name)
if not doc.has_permission("write"):
frappe.throw(_("No permission"), frappe.PermissionError)
doc.status = "Approved"
doc.save()python
@frappe.whitelist()
def approve_order(order_name):
doc = frappe.get_doc("Sales Order", order_name)
if not doc.has_permission("write"):
frappe.throw(_("无操作权限"), frappe.PermissionError)
doc.status = "Approved"
doc.save()Role-Restricted Endpoint
角色限制的接口
python
@frappe.whitelist()
def sensitive_action():
frappe.only_for(["Manager", "Administrator"])
# Only reaches here if user has rolepython
@frappe.whitelist()
def sensitive_action():
frappe.only_for(["Manager", "Administrator"])
# 只有拥有指定角色的用户才能执行以下逻辑Critical Rules
核心规则
- ALWAYS use permission API - Not role checks
- ALWAYS escape SQL -
frappe.db.escape(user) - ALWAYS use get_list - For user-facing queries
- ALWAYS return None - In has_permission hooks (not True)
- ALWAYS document - When using ignore_permissions
- ALWAYS clear cache - After permission changes:
frappe.clear_cache() - ALWAYS mask manually - In custom SQL queries (v16+)
- 始终使用权限API - 不要直接检查角色
- 始终转义SQL - 使用
frappe.db.escape(user) - 始终使用get_list - 面向用户的查询必须使用该方法
- 始终返回None - 在has_permission钩子中(不要返回True)
- 始终添加注释 - 当使用ignore_permissions时
- 始终清除缓存 - 修改权限后执行:
frappe.clear_cache() - 始终手动脱敏 - 在自定义SQL查询中(v16+)
Anti-Patterns
反模式
| ❌ Don't | ✅ Do |
|---|---|
| |
| |
| |
| |
| |
| Assume masking in custom SQL | Implement masking manually |
| ❌ 不要做 | ✅ 应该做 |
|---|---|
| |
面向用户的查询使用 | 面向用户的查询使用 |
| 在has_permission钩子中返回True | 在has_permission钩子中返回None |
| |
在钩子中使用 | 在钩子中返回False |
| 假设自定义SQL查询会自动脱敏 | 手动实现脱敏逻辑 |
Version Differences
版本差异
| Feature | v14 | v15 | v16 |
|---|---|---|---|
| ✅ | ✅ | ✅ |
| ❌ | ✅ | ✅ |
| Custom Permission Types | ❌ | ❌ | ✅ (experimental) |
| Data Masking | ❌ | ❌ | ✅ |
| ❌ | ❌ | ✅ |
| 功能 | v14 | v15 | v16 |
|---|---|---|---|
| ✅ 支持 | ✅ 支持 | ✅ 支持 |
| ❌ 无 | ✅ 支持 | ✅ 支持 |
| 自定义权限类型 | ❌ 无 | ❌ 无 | ✅ 支持(实验性) |
| 数据脱敏 | ❌ 无 | ❌ 无 | ✅ 支持 |
| ❌ 无 | ❌ 无 | ✅ 支持 |
Debugging
调试
python
undefinedpython
undefinedEnable debug output
启用调试输出
frappe.has_permission("Sales Order", "read", doc, debug=True)
frappe.has_permission("Sales Order", "read", doc, debug=True)
View logs
查看日志
print(frappe.local.permission_debug_log)
print(frappe.local.permission_debug_log)
Check user's effective permissions
检查用户的有效权限
from frappe.permissions import get_doc_permissions
perms = get_doc_permissions(doc, user="john@example.com")
---from frappe.permissions import get_doc_permissions
perms = get_doc_permissions(doc, user="john@example.com")
---Reference Files
参考文件
See folder for:
references/- - All permission types
permission-types-reference.md - - Complete API reference
permission-api-reference.md - - Hook patterns
permission-hooks-reference.md - - Working examples
examples.md - - Common mistakes
anti-patterns.md
请查看文件夹中的以下文件:
references/- - 所有权限类型说明
permission-types-reference.md - - 完整API参考
permission-api-reference.md - - 钩子实现模式
permission-hooks-reference.md - - 可运行的示例代码
examples.md - - 常见错误示例
anti-patterns.md
Related Skills
相关技能
- - Database operations that respect permissions
erpnext-database - - Controller permission checks
erpnext-syntax-controllers - - Hook configuration
erpnext-syntax-hooks
Last updated: 2026-01-18 | Frappe v14/v15/v16
- - 遵循权限控制的数据库操作
erpnext-database - - 控制器中的权限检查
erpnext-syntax-controllers - - 钩子配置指南
erpnext-syntax-hooks
最后更新:2026-01-18 | Frappe v14/v15/v16