erpnext-errors-permissions

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

ERPNext Permissions - Error Handling

ERPNext权限 - 错误处理

This skill covers error handling patterns for the Frappe permission system. For permission syntax, see
erpnext-permissions
. For hooks, see
erpnext-syntax-hooks
.
Version: v14/v15/v16 compatible

本技能涵盖Frappe权限系统的错误处理模式。如需了解权限语法,请查看
erpnext-permissions
;如需了解钩子相关内容,请查看
erpnext-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
undefined
python
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 None
undefined
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 None
undefined

permission_query_conditions - NEVER Throw!

permission_query_conditions - 切勿抛出错误!

python
undefined
python
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
undefined
python
undefined

Uses 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 )
undefined
frappe.throw( _("You don't have permission to access this resource"), exc=frappe.PermissionError )
undefined

Permission 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 widgets

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 widgets

Client-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

✅ 必须遵守

  1. Return None in has_permission hooks - Never return True
  2. Use frappe.db.escape() in query conditions - Prevent SQL injection
  3. Wrap hooks in try/except - Errors break access entirely
  4. Log permission errors - Security audit trail
  5. Use throw=True in permission checks - Automatic error handling
  6. Provide helpful error messages - Tell users what they can do
  1. 在has_permission钩子中返回None - 切勿返回True
  2. 在查询条件中使用frappe.db.escape() - 防止SQL注入
  3. 用try/except包裹钩子 - 错误会完全破坏访问权限
  4. 记录权限错误 - 用于安全审计追踪
  5. 在权限检查中使用throw=True - 自动处理错误
  6. 提供有帮助的错误信息 - 告知用户可行的操作

❌ NEVER

❌ 切勿违反

  1. Don't throw in permission hooks - Return False instead
  2. Don't use string concatenation in SQL - SQL injection risk
  3. Don't return True in has_permission - Hooks can only deny
  4. Don't ignore permission errors - Security risk
  5. Don't expose sensitive info in errors - Security risk

  1. 不要在权限钩子中抛出错误 - 改用返回False
  2. 不要在SQL中使用字符串拼接 - 存在SQL注入风险
  3. 不要在has_permission中返回True - 钩子仅能拒绝权限,无法授予
  4. 不要忽略权限错误 - 存在安全风险
  5. 不要在错误中暴露敏感信息 - 存在安全风险

Quick Reference: Permission Error Handling

快速参考:权限错误处理

ContextError MethodFallback
has_permission hookReturn FalseReturn None
permission_query_conditionsReturn restrictive SQLOwn records filter
Whitelisted methodfrappe.throw(exc=PermissionError)N/A
Controllerdoc.check_permission()Let propagate
Client Scripterror callbackShow user message

场景处理方法回退方案
has_permission钩子返回False返回None
permission_query_conditions返回限制性SQL条件仅显示自己的记录
白名单方法frappe.throw(exc=PermissionError)
控制器doc.check_permission()让异常向上传播
客户端脚本错误回调处理显示用户提示信息

Reference Files

参考文件

FileContents
references/patterns.md
Complete error handling patterns
references/examples.md
Full working examples
references/anti-patterns.md
Common mistakes to avoid

文件内容
references/patterns.md
完整的错误处理模式
references/examples.md
完整的可运行示例
references/anti-patterns.md
需避免的常见错误

See Also

另请参阅

  • erpnext-permissions
    - Permission system overview
  • erpnext-errors-hooks
    - Hook error handling
  • erpnext-errors-api
    - API error handling
  • erpnext-syntax-hooks
    - Hook syntax
  • erpnext-permissions
    - 权限系统概述
  • erpnext-errors-hooks
    - 钩子错误处理
  • erpnext-errors-api
    - API错误处理
  • erpnext-syntax-hooks
    - 钩子语法