erpnext-errors-permissions

ERPNext Permissions - Error Handling

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "erpnext-errors-permissions" with this command: npx skills add openaec-foundation/erpnext_anthropic_claude_development_skill_package/openaec-foundation-erpnext-anthropic-claude-development-skill-package-erpnext-errors-permissions

ERPNext Permissions - Error Handling

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

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 │ │ │ └─────────────────────────────────────────────────────────────────────┘

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 │ │ │ └─────────────────────────────────────────────────────────────────────────┘

Permission Hook Error Handling

has_permission - NEVER Throw!

❌ WRONG - Breaks document access!

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

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

permission_query_conditions - NEVER Throw!

❌ WRONG - Breaks list views!

def query_conditions(user): if not user: frappe.throw("User required") return f"owner = '{user}'" # Also SQL injection!

✅ 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)}"

Permission Check Error Handling

Pattern 1: Check Before Action (Recommended)

@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

@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

@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

Uses frappe.PermissionError - returns HTTP 403

frappe.throw( _("You don't have permission to access this resource"), exc=frappe.PermissionError )

Permission Error with Context

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)

@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

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

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

❌ 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

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

Reference Files

File Contents

references/patterns.md

Complete error handling patterns

references/examples.md

Full working examples

references/anti-patterns.md

Common mistakes to avoid

See Also

  • erpnext-permissions

  • Permission system overview

  • erpnext-errors-hooks

  • Hook error handling

  • erpnext-errors-api

  • API error handling

  • erpnext-syntax-hooks

  • Hook syntax

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

erpnext-code-interpreter

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

erpnext-syntax-jinja

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

erpnext-impl-controllers

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

erpnext-database

No summary provided by upstream source.

Repository SourceNeeds Review