erpnext-errors-permissions
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseERPNext Permissions - Error Handling
ERPNext权限 - 错误处理
This skill covers error handling patterns for the Frappe permission system. For permission syntax, see . For hooks, see .
erpnext-permissionserpnext-syntax-hooksVersion: v14/v15/v16 compatible
本技能涵盖Frappe权限系统的错误处理模式。如需了解权限语法,请查看;如需了解钩子相关内容,请查看。
erpnext-permissionserpnext-syntax-hooks版本:兼容v14/v15/v16
Permission Error Handling Overview
权限错误处理概述
┌─────────────────────────────────────────────────────────────────────┐
│ PERMISSION ERRORS REQUIRE SPECIAL HANDLING │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Permission Hooks (has_permission, permission_query_conditions): │
│ ⚠️ NEVER throw errors - return False or empty string │
│ ⚠️ Errors break document access and list views │
│ ⚠️ Always provide safe fallbacks │
│ │
│ Permission Checks in Code: │
│ ✅ Use frappe.has_permission() before operations │
│ ✅ Use throw=True for automatic error handling │
│ ✅ Catch frappe.PermissionError for custom handling │
│ │
│ API Endpoints: │
│ ✅ frappe.only_for() for role-restricted endpoints │
│ ✅ doc.has_permission() before document operations │
│ ✅ Return proper HTTP 403 for access denied │
│ │
└─────────────────────────────────────────────────────────────────────┘┌─────────────────────────────────────────────────────────────────────┐
│ 权限错误需要特殊处理 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 权限钩子(has_permission、permission_query_conditions): │
│ ⚠️ 切勿抛出错误 - 返回False或空字符串 │
│ ⚠️ 错误会破坏文档访问和列表视图 │
│ ⚠️ 始终提供安全的回退方案 │
│ │
│ 代码中的权限检查: │
│ ✅ 在操作前使用frappe.has_permission() │
│ ✅ 使用throw=True实现自动错误处理 │
│ ✅ 捕获frappe.PermissionError进行自定义处理 │
│ │
│ API端点: │
│ ✅ 对角色受限的端点使用frappe.only_for() │
│ ✅ 在文档操作前使用doc.has_permission() │
│ ✅ 访问被拒绝时返回正确的HTTP 403状态码 │
│ │
└─────────────────────────────────────────────────────────────────────┘Main Decision: Where Is the Permission Check?
核心决策:权限检查的位置?
┌─────────────────────────────────────────────────────────────────────────┐
│ WHERE ARE YOU HANDLING PERMISSIONS? │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ► has_permission hook (hooks.py) │
│ └─► NEVER throw - return False to deny, None to defer │
│ └─► Wrap in try/except, log errors, return None on failure │
│ │
│ ► permission_query_conditions hook (hooks.py) │
│ └─► NEVER throw - return SQL condition or empty string │
│ └─► Wrap in try/except, return restrictive fallback on failure │
│ │
│ ► Whitelisted method / API endpoint │
│ └─► Use frappe.has_permission() with throw=True │
│ └─► Or catch PermissionError for custom response │
│ └─► Use frappe.only_for() for role-restricted endpoints │
│ │
│ ► Controller method │
│ └─► Use doc.has_permission() or doc.check_permission() │
│ └─► Let PermissionError propagate for standard handling │
│ │
│ ► Client Script │
│ └─► Handle in frappe.call error callback │
│ └─► Check exc type for PermissionError │
│ │
└─────────────────────────────────────────────────────────────────────────┘┌─────────────────────────────────────────────────────────────────────────┐
│ 你在何处处理权限? │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ► has_permission钩子(hooks.py) │
│ └─► 切勿抛出错误 - 返回False拒绝权限,返回None交由系统处理 │
│ └─► 用try/except包裹,记录错误,失败时返回None │
│ │
│ ► permission_query_conditions钩子(hooks.py) │
│ └─► 切勿抛出错误 - 返回SQL条件或空字符串 │
│ └─► 用try/except包裹,失败时返回限制性回退条件 │
│ │
│ ► 白名单方法 / API端点 │
│ └─► 使用带throw=True参数的frappe.has_permission() │
│ └─► 或捕获PermissionError来自定义响应 │
│ └─► 对角色受限的端点使用frappe.only_for() │
│ │
│ ► 控制器方法 │
│ └─► 使用doc.has_permission()或doc.check_permission() │
│ └─► 让PermissionError向上传播以使用标准处理流程 │
│ │
│ ► 客户端脚本 │
│ └─► 在frappe.call的错误回调中处理 │
│ └─► 检查异常类型是否为PermissionError │
│ │
└─────────────────────────────────────────────────────────────────────────┘Permission Hook Error Handling
权限钩子错误处理
has_permission - NEVER Throw!
has_permission - 切勿抛出错误!
python
undefinedpython
undefined❌ WRONG - Breaks document access!
❌ WRONG - Breaks document access!
def has_permission(doc, ptype, user):
if doc.status == "Locked":
frappe.throw("Document is locked") # DON'T DO THIS!
def has_permission(doc, ptype, user):
if doc.status == "Locked":
frappe.throw("Document is locked") # DON'T DO THIS!
✅ CORRECT - Return False to deny, None to defer
✅ CORRECT - Return False to deny, None to defer
def has_permission(doc, ptype, user):
"""
Custom permission check.
Returns:
None: Defer to standard permission system
False: Deny permission
NEVER return True - hooks can only deny, not grant.
"""
try:
user = user or frappe.session.user
# Deny editing locked documents
if ptype == "write" and doc.get("status") == "Locked":
if "System Manager" not in frappe.get_roles(user):
return False
# Deny access to confidential documents
if doc.get("is_confidential"):
allowed_users = get_allowed_users(doc.name)
if user not in allowed_users:
return False
# ALWAYS return None to defer to standard checks
return None
except Exception:
frappe.log_error(
frappe.get_traceback(),
f"Permission check error: {doc.name if hasattr(doc, 'name') else 'unknown'}"
)
# Safe fallback - defer to standard system
return Noneundefineddef has_permission(doc, ptype, user):
"""
Custom permission check.
Returns:
None: Defer to standard permission system
False: Deny permission
NEVER return True - hooks can only deny, not grant.
"""
try:
user = user or frappe.session.user
# Deny editing locked documents
if ptype == "write" and doc.get("status") == "Locked":
if "System Manager" not in frappe.get_roles(user):
return False
# Deny access to confidential documents
if doc.get("is_confidential"):
allowed_users = get_allowed_users(doc.name)
if user not in allowed_users:
return False
# ALWAYS return None to defer to standard checks
return None
except Exception:
frappe.log_error(
frappe.get_traceback(),
f"Permission check error: {doc.name if hasattr(doc, 'name') else 'unknown'}"
)
# Safe fallback - defer to standard system
return Noneundefinedpermission_query_conditions - NEVER Throw!
permission_query_conditions - 切勿抛出错误!
python
undefinedpython
undefined❌ WRONG - Breaks list views!
❌ WRONG - Breaks list views!
def query_conditions(user):
if not user:
frappe.throw("User required")
return f"owner = '{user}'" # Also SQL injection!
def query_conditions(user):
if not user:
frappe.throw("User required")
return f"owner = '{user}'" # Also SQL injection!
✅ CORRECT - Return safe SQL condition
✅ CORRECT - Return safe SQL condition
def query_conditions(user):
"""
Return SQL WHERE clause fragment for list filtering.
Returns:
str: SQL condition (empty string for no restriction)
NEVER throw errors - return restrictive fallback.
"""
try:
if not user:
user = frappe.session.user
roles = frappe.get_roles(user)
# Admins see all
if "System Manager" in roles:
return ""
# Managers see team records
if "Sales Manager" in roles:
team_users = get_team_users(user)
if team_users:
escaped = ", ".join([frappe.db.escape(u) for u in team_users])
return f"`tabSales Order`.owner IN ({escaped})"
# Default: own records only
return f"`tabSales Order`.owner = {frappe.db.escape(user)}"
except Exception:
frappe.log_error(
frappe.get_traceback(),
f"Permission query error for {user}"
)
# SAFE FALLBACK: Most restrictive - own records only
return f"`tabSales Order`.owner = {frappe.db.escape(frappe.session.user)}"
---def query_conditions(user):
"""
Return SQL WHERE clause fragment for list filtering.
Returns:
str: SQL condition (empty string for no restriction)
NEVER throw errors - return restrictive fallback.
"""
try:
if not user:
user = frappe.session.user
roles = frappe.get_roles(user)
# Admins see all
if "System Manager" in roles:
return ""
# Managers see team records
if "Sales Manager" in roles:
team_users = get_team_users(user)
if team_users:
escaped = ", ".join([frappe.db.escape(u) for u in team_users])
return f"`tabSales Order`.owner IN ({escaped})"
# Default: own records only
return f"`tabSales Order`.owner = {frappe.db.escape(user)}"
except Exception:
frappe.log_error(
frappe.get_traceback(),
f"Permission query error for {user}"
)
# SAFE FALLBACK: Most restrictive - own records only
return f"`tabSales Order`.owner = {frappe.db.escape(frappe.session.user)}"
---Permission Check Error Handling
权限检查错误处理
Pattern 1: Check Before Action (Recommended)
模式1:操作前检查(推荐)
python
@frappe.whitelist()
def update_order_status(order_name, new_status):
"""Update order status with permission check."""
# Check document exists
if not frappe.db.exists("Sales Order", order_name):
frappe.throw(
_("Sales Order {0} not found").format(order_name),
exc=frappe.DoesNotExistError
)
# Check permission - throws automatically
frappe.has_permission("Sales Order", "write", order_name, throw=True)
# Now safe to proceed
frappe.db.set_value("Sales Order", order_name, "status", new_status)
return {"status": "success"}python
@frappe.whitelist()
def update_order_status(order_name, new_status):
"""Update order status with permission check."""
# Check document exists
if not frappe.db.exists("Sales Order", order_name):
frappe.throw(
_("Sales Order {0} not found").format(order_name),
exc=frappe.DoesNotExistError
)
# Check permission - throws automatically
frappe.has_permission("Sales Order", "write", order_name, throw=True)
# Now safe to proceed
frappe.db.set_value("Sales Order", order_name, "status", new_status)
return {"status": "success"}Pattern 2: Custom Permission Error Response
模式2:自定义权限错误响应
python
@frappe.whitelist()
def sensitive_operation(doc_name):
"""Operation with custom permission error handling."""
try:
doc = frappe.get_doc("Sensitive Doc", doc_name)
doc.check_permission("write")
except frappe.DoesNotExistError:
frappe.throw(
_("Document not found"),
exc=frappe.DoesNotExistError
)
except frappe.PermissionError:
# Log attempted access
frappe.log_error(
f"Unauthorized access attempt: {doc_name} by {frappe.session.user}",
"Security Alert"
)
# Custom error message
frappe.throw(
_("You don't have permission to perform this action. This incident has been logged."),
exc=frappe.PermissionError
)
# Proceed with operation
return process_document(doc)python
@frappe.whitelist()
def sensitive_operation(doc_name):
"""Operation with custom permission error handling."""
try:
doc = frappe.get_doc("Sensitive Doc", doc_name)
doc.check_permission("write")
except frappe.DoesNotExistError:
frappe.throw(
_("Document not found"),
exc=frappe.DoesNotExistError
)
except frappe.PermissionError:
# Log attempted access
frappe.log_error(
f"Unauthorized access attempt: {doc_name} by {frappe.session.user}",
"Security Alert"
)
# Custom error message
frappe.throw(
_("You don't have permission to perform this action. This incident has been logged."),
exc=frappe.PermissionError
)
# Proceed with operation
return process_document(doc)Pattern 3: Role-Restricted Endpoint
模式3:角色受限端点
python
@frappe.whitelist()
def admin_dashboard_data():
"""Endpoint restricted to specific roles."""
# This throws PermissionError if user lacks role
frappe.only_for(["System Manager", "Dashboard Admin"])
# Only reaches here if authorized
return compile_dashboard_data()
@frappe.whitelist()
def manager_report():
"""Endpoint with graceful role check."""
allowed_roles = ["Sales Manager", "General Manager", "System Manager"]
user_roles = frappe.get_roles()
if not any(role in user_roles for role in allowed_roles):
frappe.throw(
_("This report is only available to managers"),
exc=frappe.PermissionError
)
return generate_report()python
@frappe.whitelist()
def admin_dashboard_data():
"""Endpoint restricted to specific roles."""
# This throws PermissionError if user lacks role
frappe.only_for(["System Manager", "Dashboard Admin"])
# Only reaches here if authorized
return compile_dashboard_data()
@frappe.whitelist()
def manager_report():
"""Endpoint with graceful role check."""
allowed_roles = ["Sales Manager", "General Manager", "System Manager"]
user_roles = frappe.get_roles()
if not any(role in user_roles for role in allowed_roles):
frappe.throw(
_("This report is only available to managers"),
exc=frappe.PermissionError
)
return generate_report()Error Response Patterns
错误响应模式
Standard Permission Error
标准权限错误
python
undefinedpython
undefinedUses frappe.PermissionError - returns HTTP 403
Uses frappe.PermissionError - returns HTTP 403
frappe.throw(
_("You don't have permission to access this resource"),
exc=frappe.PermissionError
)
undefinedfrappe.throw(
_("You don't have permission to access this resource"),
exc=frappe.PermissionError
)
undefinedPermission Error with Context
带上下文的权限错误
python
def check_access(doc):
"""Check access with helpful error message."""
if not doc.has_permission("read"):
owner_name = frappe.db.get_value("User", doc.owner, "full_name")
frappe.throw(
_("This document belongs to {0}. You can only view your own documents.").format(owner_name),
exc=frappe.PermissionError
)python
def check_access(doc):
"""Check access with helpful error message."""
if not doc.has_permission("read"):
owner_name = frappe.db.get_value("User", doc.owner, "full_name")
frappe.throw(
_("This document belongs to {0}. You can only view your own documents.").format(owner_name),
exc=frappe.PermissionError
)Soft Permission Denial (No Error)
软权限拒绝(无错误)
python
@frappe.whitelist()
def get_dashboard_widgets():
"""Return widgets based on user permissions."""
widgets = []
# Add widgets based on permissions
if frappe.has_permission("Sales Order", "read"):
widgets.append(get_sales_widget())
if frappe.has_permission("Purchase Order", "read"):
widgets.append(get_purchase_widget())
if frappe.has_permission("Employee", "read"):
widgets.append(get_hr_widget())
# No error if no widgets - just return empty
return widgetspython
@frappe.whitelist()
def get_dashboard_widgets():
"""Return widgets based on user permissions."""
widgets = []
# Add widgets based on permissions
if frappe.has_permission("Sales Order", "read"):
widgets.append(get_sales_widget())
if frappe.has_permission("Purchase Order", "read"):
widgets.append(get_purchase_widget())
if frappe.has_permission("Employee", "read"):
widgets.append(get_hr_widget())
# No error if no widgets - just return empty
return widgetsClient-Side Permission Error Handling
客户端权限错误处理
JavaScript Error Handling
JavaScript错误处理
javascript
// Handle permission errors in frappe.call
frappe.call({
method: "myapp.api.sensitive_operation",
args: { doc_name: "DOC-001" },
callback: function(r) {
if (r.message) {
frappe.show_alert({
message: __("Operation completed"),
indicator: "green"
});
}
},
error: function(r) {
// Check if it's a permission error
if (r.exc_type === "PermissionError") {
frappe.msgprint({
title: __("Access Denied"),
message: __("You don't have permission to perform this action."),
indicator: "red"
});
} else {
// Generic error handling
frappe.msgprint({
title: __("Error"),
message: r.exc || __("An error occurred"),
indicator: "red"
});
}
}
});javascript
// Handle permission errors in frappe.call
frappe.call({
method: "myapp.api.sensitive_operation",
args: { doc_name: "DOC-001" },
callback: function(r) {
if (r.message) {
frappe.show_alert({
message: __("Operation completed"),
indicator: "green"
});
}
},
error: function(r) {
// Check if it's a permission error
if (r.exc_type === "PermissionError") {
frappe.msgprint({
title: __("Access Denied"),
message: __("You don't have permission to perform this action."),
indicator: "red"
});
} else {
// Generic error handling
frappe.msgprint({
title: __("Error"),
message: r.exc || __("An error occurred"),
indicator: "red"
});
}
}
});Permission Check Before Action
操作前检查权限
javascript
// Check permission before showing button
frappe.ui.form.on("Sales Order", {
refresh: function(frm) {
// Only show button if user has write permission
if (frm.doc.docstatus === 0 && frappe.perm.has_perm("Sales Order", 0, "write")) {
frm.add_custom_button(__("Special Action"), function() {
perform_special_action(frm);
});
}
}
});
// Or use frappe.call to check server-side
frappe.call({
method: "frappe.client.has_permission",
args: {
doctype: "Sales Order",
docname: frm.doc.name,
ptype: "write"
},
async: false,
callback: function(r) {
if (r.message) {
// Has permission - show button
}
}
});javascript
// Check permission before showing button
frappe.ui.form.on("Sales Order", {
refresh: function(frm) {
// Only show button if user has write permission
if (frm.doc.docstatus === 0 && frappe.perm.has_perm("Sales Order", 0, "write")) {
frm.add_custom_button(__("Special Action"), function() {
perform_special_action(frm);
});
}
}
});
// Or use frappe.call to check server-side
frappe.call({
method: "frappe.client.has_permission",
args: {
doctype: "Sales Order",
docname: frm.doc.name,
ptype: "write"
},
async: false,
callback: function(r) {
if (r.message) {
// Has permission - show button
}
}
});Critical Rules
关键规则
✅ ALWAYS
✅ 必须遵守
- Return None in has_permission hooks - Never return True
- Use frappe.db.escape() in query conditions - Prevent SQL injection
- Wrap hooks in try/except - Errors break access entirely
- Log permission errors - Security audit trail
- Use throw=True in permission checks - Automatic error handling
- Provide helpful error messages - Tell users what they can do
- 在has_permission钩子中返回None - 切勿返回True
- 在查询条件中使用frappe.db.escape() - 防止SQL注入
- 用try/except包裹钩子 - 错误会完全破坏访问权限
- 记录权限错误 - 用于安全审计追踪
- 在权限检查中使用throw=True - 自动处理错误
- 提供有帮助的错误信息 - 告知用户可行的操作
❌ NEVER
❌ 切勿违反
- Don't throw in permission hooks - Return False instead
- Don't use string concatenation in SQL - SQL injection risk
- Don't return True in has_permission - Hooks can only deny
- Don't ignore permission errors - Security risk
- Don't expose sensitive info in errors - Security risk
- 不要在权限钩子中抛出错误 - 改用返回False
- 不要在SQL中使用字符串拼接 - 存在SQL注入风险
- 不要在has_permission中返回True - 钩子仅能拒绝权限,无法授予
- 不要忽略权限错误 - 存在安全风险
- 不要在错误中暴露敏感信息 - 存在安全风险
Quick Reference: Permission Error Handling
快速参考:权限错误处理
| Context | Error Method | Fallback |
|---|---|---|
| has_permission hook | Return False | Return None |
| permission_query_conditions | Return restrictive SQL | Own records filter |
| Whitelisted method | frappe.throw(exc=PermissionError) | N/A |
| Controller | doc.check_permission() | Let propagate |
| Client Script | error callback | Show user message |
| 场景 | 处理方法 | 回退方案 |
|---|---|---|
| has_permission钩子 | 返回False | 返回None |
| permission_query_conditions | 返回限制性SQL条件 | 仅显示自己的记录 |
| 白名单方法 | frappe.throw(exc=PermissionError) | 无 |
| 控制器 | doc.check_permission() | 让异常向上传播 |
| 客户端脚本 | 错误回调处理 | 显示用户提示信息 |
Reference Files
参考文件
| File | Contents |
|---|---|
| Complete error handling patterns |
| Full working examples |
| Common mistakes to avoid |
| 文件 | 内容 |
|---|---|
| 完整的错误处理模式 |
| 完整的可运行示例 |
| 需避免的常见错误 |
See Also
另请参阅
- - Permission system overview
erpnext-permissions - - Hook error handling
erpnext-errors-hooks - - API error handling
erpnext-errors-api - - Hook syntax
erpnext-syntax-hooks
- - 权限系统概述
erpnext-permissions - - 钩子错误处理
erpnext-errors-hooks - - API错误处理
erpnext-errors-api - - 钩子语法
erpnext-syntax-hooks