erpnext-permissions

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

ERPNext Permissions Skill

ERPNext 权限技能指南

Deterministic patterns for implementing robust permission systems in Frappe/ERPNext applications.

在Frappe/ERPNext应用中实现可靠权限系统的标准化模式。

Overview

概述

Frappe's permission system has five layers:
LayerControlsConfigured ViaVersion
Role PermissionsWhat users CAN doDocType permissions tableAll
User PermissionsWHICH documents users seeUser Permission recordsAll
Perm LevelsWHICH fields users seeField permlevel propertyAll
Permission HooksCustom logichooks.pyAll
Data MaskingMASKED field valuesField mask propertyv16+

Frappe的权限系统包含五个层级:
层级控制范围配置方式版本
角色权限用户可执行的操作文档类型(DocType)权限表所有版本
用户权限用户可查看的具体文档用户权限记录所有版本
权限级别(Perm Levels)用户可查看的具体字段字段的permlevel属性所有版本
权限钩子自定义逻辑hooks.py所有版本
数据脱敏字段值的脱敏显示字段的mask属性v16+

Quick Reference

快速参考

Permission Types

权限类型

TypeCheckFor
read
frappe.has_permission(dt, "read")
View document
write
frappe.has_permission(dt, "write")
Edit document
create
frappe.has_permission(dt, "create")
Create new
delete
frappe.has_permission(dt, "delete")
Delete
submit
frappe.has_permission(dt, "submit")
Submit (submittable only)
cancel
frappe.has_permission(dt, "cancel")
Cancel
select
frappe.has_permission(dt, "select")
Select in Link (v14+)
mask
Role permission for unmasked viewView unmasked data (v16+)
类型检查方式适用场景
read
frappe.has_permission(dt, "read")
查看文档
write
frappe.has_permission(dt, "write")
编辑文档
create
frappe.has_permission(dt, "create")
创建新文档
delete
frappe.has_permission(dt, "delete")
删除文档
submit
frappe.has_permission(dt, "submit")
提交文档(仅适用于可提交的文档类型)
cancel
frappe.has_permission(dt, "cancel")
取消文档
select
frappe.has_permission(dt, "select")
在链接字段中选择(v14+)
mask
角色的无脱敏查看权限查看未脱敏数据(v16+)

Automatic Roles

自动分配角色

RoleAssigned To
Guest
Everyone (including anonymous)
All
All registered users
Administrator
Only Administrator user
Desk User
System Users (v15+)

角色分配对象
Guest
所有用户(包括匿名用户)
All
所有注册用户
Administrator
仅管理员用户
Desk User
系统用户(v15+)

Essential API

核心API

Check Permission

检查权限

python
undefined
python
undefined

DocType 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")
undefined
doc.check_permission("write")
undefined

Get Permissions

获取权限

python
from frappe.permissions import get_doc_permissions
python
from frappe.permissions import get_doc_permissions

Get 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, ...}

undefined
undefined

User Permissions

用户权限管理

python
from frappe.permissions import add_user_permission, remove_user_permission
python
from frappe.permissions import add_user_permission, remove_user_permission

Restrict 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")
undefined
from frappe.permissions import get_user_permissions perms = get_user_permissions("john@example.com")
undefined

Sharing

文档共享

python
from frappe.share import add as add_share
python
from frappe.share import add as add_share

Share 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
mask
permission see masked values (e.g.,
****
,
+91-811XXXXXXX
).
数据脱敏功能在保持字段可见的同时保护敏感字段值。没有
mask
权限的用户会看到脱敏后的值(例如
****
+91-811XXXXXXX
)。

Use 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
mask
permission to roles that should see unmasked data:
json
{
  "permissions": [
    {"role": "Employee", "permlevel": 0, "read": 1},
    {"role": "HR Manager", "permlevel": 0, "read": 1, "mask": 1}
  ]
}
为需要查看未脱敏数据的角色添加
mask
权限:
json
{
  "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
    frappe.db.sql()
    calls
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
undefined

hooks.py

hooks.py

has_permission = { "Sales Order": "myapp.permissions.check_order_permission" }

```python
has_permission = { "Sales Order": "myapp.permissions.check_order_permission" }

```python

myapp/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 default
undefined
def 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  # 默认情况下必须返回None
undefined

permission_query_conditions Hook

permission_query_conditions钩子

Filter list queries. Only affects
get_list()
, NOT
get_all()
.
python
undefined
过滤列表查询结果,仅影响
get_list()
不影响
get_all()
python
undefined

hooks.py

hooks.py

permission_query_conditions = { "Customer": "myapp.permissions.customer_query" }

```python
permission_query_conditions = { "Customer": "myapp.permissions.customer_query" }

```python

myapp/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 的区别

MethodUser PermissionsQuery Hook
frappe.get_list()
✅ Applied✅ Applied
frappe.get_all()
❌ Ignored❌ Ignored
python
undefined
方法是否应用用户权限是否应用查询钩子
frappe.get_list()
✅ 应用✅ 应用
frappe.get_all()
❌ 忽略❌ 忽略
python
undefined

User-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 role

python
@frappe.whitelist()
def sensitive_action():
    frappe.only_for(["Manager", "Administrator"])
    # 只有拥有指定角色的用户才能执行以下逻辑

Critical Rules

核心规则

  1. ALWAYS use permission API - Not role checks
  2. ALWAYS escape SQL -
    frappe.db.escape(user)
  3. ALWAYS use get_list - For user-facing queries
  4. ALWAYS return None - In has_permission hooks (not True)
  5. ALWAYS document - When using ignore_permissions
  6. ALWAYS clear cache - After permission changes:
    frappe.clear_cache()
  7. ALWAYS mask manually - In custom SQL queries (v16+)

  1. 始终使用权限API - 不要直接检查角色
  2. 始终转义SQL - 使用
    frappe.db.escape(user)
  3. 始终使用get_list - 面向用户的查询必须使用该方法
  4. 始终返回None - 在has_permission钩子中(不要返回True)
  5. 始终添加注释 - 当使用ignore_permissions时
  6. 始终清除缓存 - 修改权限后执行:
    frappe.clear_cache()
  7. 始终手动脱敏 - 在自定义SQL查询中(v16+)

Anti-Patterns

反模式

❌ Don't✅ Do
if "Role" in frappe.get_roles()
frappe.has_permission(dt, ptype)
frappe.get_all()
for user queries
frappe.get_list()
return True
in has_permission
return None
f"owner = '{user}'"
f"owner = {frappe.db.escape(user)}"
frappe.throw()
in hooks
return False
Assume masking in custom SQLImplement masking manually

❌ 不要做✅ 应该做
if "Role" in frappe.get_roles()
frappe.has_permission(dt, ptype)
面向用户的查询使用
frappe.get_all()
面向用户的查询使用
frappe.get_list()
在has_permission钩子中返回True在has_permission钩子中返回None
f"owner = '{user}'"
f"owner = {frappe.db.escape(user)}"
在钩子中使用
frappe.throw()
在钩子中返回False
假设自定义SQL查询会自动脱敏手动实现脱敏逻辑

Version Differences

版本差异

Featurev14v15v16
select
permission
Desk User
role
Custom Permission Types✅ (experimental)
Data Masking
mask
permission type

功能v14v15v16
select
权限
✅ 支持✅ 支持✅ 支持
Desk User
角色
❌ 无✅ 支持✅ 支持
自定义权限类型❌ 无❌ 无✅ 支持(实验性)
数据脱敏❌ 无❌ 无✅ 支持
mask
权限类型
❌ 无❌ 无✅ 支持

Debugging

调试

python
undefined
python
undefined

Enable 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
references/
folder for:
  • permission-types-reference.md
    - All permission types
  • permission-api-reference.md
    - Complete API reference
  • permission-hooks-reference.md
    - Hook patterns
  • examples.md
    - Working examples
  • anti-patterns.md
    - Common mistakes

请查看
references/
文件夹中的以下文件:
  • permission-types-reference.md
    - 所有权限类型说明
  • permission-api-reference.md
    - 完整API参考
  • permission-hooks-reference.md
    - 钩子实现模式
  • examples.md
    - 可运行的示例代码
  • anti-patterns.md
    - 常见错误示例

Related Skills

相关技能

  • erpnext-database
    - Database operations that respect permissions
  • erpnext-syntax-controllers
    - Controller permission checks
  • erpnext-syntax-hooks
    - Hook configuration

Last updated: 2026-01-18 | Frappe v14/v15/v16
  • erpnext-database
    - 遵循权限控制的数据库操作
  • erpnext-syntax-controllers
    - 控制器中的权限检查
  • erpnext-syntax-hooks
    - 钩子配置指南

最后更新:2026-01-18 | Frappe v14/v15/v16