mcpField
F
frappe-errors-permissions

frappe-errors-permissions

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

关于 frappe-errors-permissions

面向 Frappe 开发者的权限错误排查技能,按错误表现梳理角色、文档级权限、钩子和访客访问问题,并提供安全修复准则。

主要功能

  1. 01按错误信息定位权限原因
  2. 02排查权限钩子与查询条件
  3. 03诊断用户权限和字段级权限
  4. 04说明共享及访客访问规则
  5. 05提供安全编码与调试流程

适用场景

  1. 01用户无法打开单个文档时定位原因
  2. 02列表记录意外消失时检查查询条件
  3. 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.md
examples.md
patterns.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"])

关键规则

始终遵循
  1. 将权限钩子封装在 try/except 中 — 未处理的错误会导致所有访问中断
  2. 在 has_permission 中返回 None(而不是 True) — 钩子只能拒绝权限
  3. 在查询条件中使用 frappe.db.escape() — 防止 SQL 注入
  4. 在自定义钩子中优先检查 System Manager / Administrator
  5. 使用 frappe.has_permission(throw=True) 检查端点权限
  6. 面向用户的查询使用 get_list(而不是 get_all) — get_all 会绕过权限检查
  7. 使用 frappe.log_error() 记录权限拒绝,以便进行安全审计
绝不遵循
  1. 在 has_permission 或 permission_query_conditions 中抛出错误 — 会导致访问完全中断
  2. 在 has_permission 中返回 True — 不会产生任何效果,钩子只能限制权限
  3. 对 SQL 使用字符串格式化 — 使用 frappe.db.escape() 防止注入
  4. 授予 Guest 写入/删除权限 — 存在安全风险
  5. 使用 ignore_permissions 却不说明原因 — 会造成审计缺口
  6. 认为共享可以取代角色权限 — 共享只能作为补充

参考文件

文件内容
references/patterns.md完整的钩子模式、查询条件和 API 端点
references/examples.md包含 hooks.py 配置的完整可运行示例
references/anti-patterns.md15 个常见错误及错误/正确对照示例

另请参阅

  • frappe-core-permissions — 权限系统架构
  • frappe-errors-api — API 错误处理(401/403/404)
  • frappe-errors-hooks — 钩子错误处理模式
  • frappe-syntax-hooks — 钩子注册语法

评论

支持 Markdown