关于 frappe-errors-permissions
面向 Frappe 开发者的权限错误排查技能,按错误表现梳理角色、文档级权限、钩子和访客访问问题,并提供安全修复准则。
主要功能
- 01按错误信息定位权限原因
- 02排查权限钩子与查询条件
- 03诊断用户权限和字段级权限
- 04说明共享及访客访问规则
- 05提供安全编码与调试流程
适用场景
- 01用户无法打开单个文档时定位原因
- 02列表记录意外消失时检查查询条件
- 03修复字段不可见或访客端点拒绝访问
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ impertio-studio/ frappe_claude_skill_package --skill frappe-errors-permissions安装后在 Claude Code 中输入/frappe-errors-permissions确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/anti-patterns.mdexamples.mdpatterns.md详细介绍
权限错误处理
权限系统概览请参阅 frappe-core-permissions。钩子语法请参阅 frappe-syntax-hooks。
快速诊断:错误消息 -> 原因 -> 修复方法
| 错误消息 | 原因 | 修复方法 |
|---|---|---|
frappe.exceptions.PermissionError | 用户缺少角色或文档级访问权限 | 在 Role Permissions Manager 中添加角色,或授予 User Permission |
| 打开文档时显示“Not permitted” | has_permission 钩子返回 False,或角色缺少 read 权限 | 检查 frappe.permissions.get_doc_permissions(doc, user) 的输出 |
| 列表视图显示 0 条记录 | permission_query_conditions 返回的 SQL 过于严格 | 调试 SQL 条件;检查 Link 字段的 User Permissions |
| “Not allowed to access ... for Guest” | 端点缺少 allow_guest=True,或 DocType 未授予 Guest read 权限 | 在 @frappe.whitelist() 中添加 allow_guest=True |
| 角色有 read 权限,但字段不可见 | 字段的 perm_level > 0,且角色缺少相应级别的权限 | 为特定 perm_level 添加角色权限行 |
| “User Permission restriction” 阻止访问 | Link 字段上的 User Permission 会自动筛选文档 | 取消选中该角色权限行上的“Apply User Permissions”,或添加匹配的 User Permission |
| 分享后仍无法访问 | 分享可以添加访问权限,但无法弥补角色权限缺失 | 用户必须拥有基本角色权限;分享只能添加文档级授权 |
ignore_permissions 无效 | 在 get_doc 已检查权限后才设置标志 | 调用 save() 或 insert() 之前设置 flags.ignore_permissions = True |
| System Manager 无法访问 | 自定义 has_permission 钩子拒绝访问,却没有检查角色 | 在钩子中始终检查 System Manager / Administrator |
决策树:错误出在哪里?
Permission error occurred
├── Document-level (single doc access)?
│ ├── has_permission hook returning False?
│ │ └── Debug: frappe.permissions.get_doc_permissions(doc, user)
│ ├── User Permission restricting Link field?
│ │ └── Check: frappe.get_all("User Permission", filters={"user": user})
│ ├── perm_level blocking field?
│ │ └── Check: role has permission row for that perm_level
│ └── Sharing not applying?
│ └── Check: user has base role + sharing record exists
├── List-level (0 records in list view)?
│ ├── permission_query_conditions returning bad SQL?
│ │ └── Debug: run condition manually in MariaDB console
│ ├── User Permission auto-filtering?
│ │ └── Check "Apply User Permissions" checkbox on role row
│ └── get_all vs get_list confusion?
│ └── ALWAYS use get_list for user-facing queries
├── API endpoint (403 response)?
│ ├── Missing @frappe.whitelist()?
│ │ └── Add decorator to Python method
│ ├── Missing allow_guest=True?
│ │ └── Add allow_guest parameter for public endpoints
│ └── frappe.only_for() blocking?
│ └── Check user has required role
└── System Manager bypass failing?
└── Custom hook does not check for System Manager role
权限钩子错误
has_permission 钩子:绝不抛出异常
# hooks.py
has_permission = {
"Sales Order": "myapp.permissions.sales_order_has_permission",
}
# WRONG — Breaks ALL document access
def sales_order_has_permission(doc, user, permission_type):
if doc.status == "Locked":
frappe.throw("Locked") # NEVER do this
# CORRECT — Return False to deny, None to defer
def sales_order_has_permission(doc, user, permission_type):
"""
ALWAYS wrap in try/except. NEVER throw. NEVER return True.
Returns: False (deny) or None (defer to standard system).
"""
try:
user = user or frappe.session.user
if user == "Administrator":
return None
# ALWAYS check System Manager early
if "System Manager" in frappe.get_roles(user):
return None
# Deny write on locked docs (but allow read)
if permission_type in ("write", "delete", "cancel"):
if doc.get("status") == "Locked":
return False
return None # Defer to standard permission system
except Exception:
frappe.log_error(frappe.get_traceback(),
f"has_permission error: {getattr(doc, 'name', 'unknown')}")
return None # Safe fallback — defer
has_permission 钩子的关键规则:
- 始终返回
None以交由默认逻辑处理,返回False以拒绝访问。绝不返回True——钩子只能限制权限,不能授予权限。 - 始终在整个函数外层使用
try/except。未处理的异常会导致对该 DocType 的所有访问都失败。 - 始终在最前面检查
Administrator和System Manager。 - 绝不要在此钩子中调用
frappe.throw()。
permission_query_conditions:绝不抛出异常
# hooks.py
permission_query_conditions = {
"Sales Order": "myapp.permissions.sales_order_query",
}
# WRONG — Breaks list view for all users
def sales_order_query(user):
if not user:
frappe.throw("User required") # NEVER do this
return f"owner = '{user}'" # SQL injection!
# CORRECT — Return SQL string or empty string
def sales_order_query(user):
"""
ALWAYS return a string. Empty string = no restriction.
ALWAYS use frappe.db.escape(). ALWAYS wrap in try/except.
"""
try:
user = user or frappe.session.user
if user == "Administrator":
return ""
if "System Manager" in frappe.get_roles(user):
return ""
return f"`tabSales Order`.owner = {frappe.db.escape(user)}"
except Exception:
frappe.log_error(frappe.get_traceback(), "Query conditions error")
# SAFE FALLBACK: most restrictive
return f"`tabSales Order`.owner = {frappe.db.escape(frappe.session.user)}"
permission_query_conditions 的关键规则:
- 绝不抛出错误——返回
"1=0"以拒绝所有访问,或返回限制性 SQL 字符串。 - 始终对每个用户提供的值使用
frappe.db.escape()。 - 此钩子只影响
frappe.get_list()/frappe.db.get_list(),不会影响frappe.get_all()/frappe.db.get_all()。
User Permission 错误
限制过严:记录消失
Error: User can't see any Sales Orders despite having Sales User role.
Cause: A User Permission for "Company" exists, and "Apply User Permissions"
is checked on the Sales Order role row. Sales Order has a Company
Link field, so ALL Sales Orders are filtered by that Company value.
调试步骤:
# Step 1: Check what User Permissions exist
frappe.get_all("User Permission",
filters={"user": "[email protected]"},
fields=["allow", "for_value", "applicable_for"])
# Step 2: Check if Apply User Permissions is checked
frappe.get_all("DocPerm",
filters={"parent": "Sales Order", "role": "Sales User"},
fields=["role", "permlevel", "apply_user_permissions"]) # [v14]
# Step 3: Check effective permissions on a specific doc
from frappe.permissions import get_doc_permissions
perms = get_doc_permissions(frappe.get_doc("Sales Order", "SO-001"), "[email protected]")
修复方法:
- 移除过于宽泛、会筛选意外 DocType 的 User Permissions。
- 使用
applicable_for字段 [v14+] 限制 User Permission 适用的 DocType。 - 如果不需要全面筛选,请取消选中角色权限行上的“Apply User Permissions”。
限制过宽:用户可以查看所有内容
Error: User Permission set for Territory = "North" but user sees all territories.
Cause: "Apply User Permissions" is NOT checked on the role permission row,
or the DocType has no Link field for Territory.
**修复方法:**确保角色权限行已选中“Apply User Permissions”,且 DocType 包含指向受限 DocType 的 Link 字段。
perm_level 错误
Error: Field "cost_center" is invisible despite user having read permission.
Cause: Field has permlevel=1 but role only has permission for permlevel=0.
# Check which perm_levels a role has access to
frappe.get_all("DocPerm",
filters={"parent": "Sales Invoice", "role": "Accounts User"},
fields=["permlevel", "read", "write"])
**修复方法:**在 DocType 的 Permission 表中为该角色添加新行,并设置所需的 permlevel。
分享权限错误
Error: Document shared with user but user still gets PermissionError.
Cause: User has NO base role permission on the DocType. Sharing only
supplements — it never replaces role-based permissions.
# Share a document (user MUST already have a role with at least read)
frappe.share.add("Sales Order", "SO-001", "[email protected]",
read=1, write=1, share=1)
# Check if sharing grants access
frappe.share.get_sharing_permissions("Sales Order", "SO-001", "[email protected]")
规则:
- 分享之前,始终确保用户至少拥有一个对该 DocType 具有 read 权限的角色。
- 分享会在角色权限的基础上添加文档级授权。
- [v15+]
frappe.share.add接受notify=1,以发送电子邮件通知。
Guest 访问错误
Error: "Not permitted" for unauthenticated users.
Cause: DocType has no Guest read permission, or API missing allow_guest.
网页 / 门户的修复方法:
# Add Guest read permission in DocType Permission table
# Role: Guest, Level: 0, Read: checked
API 端点的修复方法:
@frappe.whitelist(allow_guest=True)
def public_endpoint():
# ALWAYS validate input — guest endpoints are exposed to the internet
pass
除非 DocType 专为公开提交设计(例如,Web Form 后端),否则绝不授予 Guest write/create/delete 权限。
调试工作流:frappe.permissions
import frappe
from frappe.permissions import get_doc_permissions
# Get all effective permissions for a user on a document
doc = frappe.get_doc("Sales Order", "SO-001")
perms = get_doc_permissions(doc, user="[email protected]")
# Returns dict: {"read": 1, "write": 0, "create": 0, ...}
# Check specific permission with full context
frappe.has_permission("Sales Order", ptype="write",
doc="SO-001", user="[email protected]", throw=False)
# List all roles for a user
frappe.get_roles("[email protected]")
# Check User Permissions
frappe.get_all("User Permission",
filters={"user": "[email protected]"},
fields=["allow", "for_value", "applicable_for", "is_default"])
关键规则
始终遵循
- 将权限钩子封装在 try/except 中 — 未处理的错误会导致所有访问中断
- 在 has_permission 中返回 None(而不是 True) — 钩子只能拒绝权限
- 在查询条件中使用 frappe.db.escape() — 防止 SQL 注入
- 在自定义钩子中优先检查 System Manager / Administrator
- 使用 frappe.has_permission(throw=True) 检查端点权限
- 面向用户的查询使用 get_list(而不是 get_all) — get_all 会绕过权限检查
- 使用
frappe.log_error()记录权限拒绝,以便进行安全审计
绝不遵循
- 在 has_permission 或 permission_query_conditions 中抛出错误 — 会导致访问完全中断
- 在 has_permission 中返回 True — 不会产生任何效果,钩子只能限制权限
- 对 SQL 使用字符串格式化 — 使用
frappe.db.escape()防止注入 - 授予 Guest 写入/删除权限 — 存在安全风险
- 使用 ignore_permissions 却不说明原因 — 会造成审计缺口
- 认为共享可以取代角色权限 — 共享只能作为补充
参考文件
| 文件 | 内容 |
|---|---|
references/patterns.md | 完整的钩子模式、查询条件和 API 端点 |
references/examples.md | 包含 hooks.py 配置的完整可运行示例 |
references/anti-patterns.md | 15 个常见错误及错误/正确对照示例 |
另请参阅
frappe-core-permissions— 权限系统架构frappe-errors-api— API 错误处理(401/403/404)frappe-errors-hooks— 钩子错误处理模式frappe-syntax-hooks— 钩子注册语法
评论
支持 Markdown