erpnext-errors-permissions
npx machina-cli add skill OpenAEC-Foundation/ERPNext_Anthropic_Claude_Development_Skill_Package/erpnext-errors-permissions --openclawERPNext 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 overviewerpnext-errors-hooks- Hook error handlingerpnext-errors-api- API error handlingerpnext-syntax-hooks- Hook syntax
Source
git clone https://github.com/OpenAEC-Foundation/ERPNext_Anthropic_Claude_Development_Skill_Package/blob/main/skills/source/errors/erpnext-errors-permissions/SKILL.mdView on GitHub Overview
This skill codifies error handling patterns for the ERPNext/Frappe permission system, focusing on PermissionError, has_permission failures, role issues, and document access problems. It targets V14–V16 compatibility and emphasizes safe fallbacks and proper HTTP responses for access denial scenarios.
How This Skill Works
It leverages permission hooks (has_permission, permission_query_conditions), API endpoints, and controller checks to enforce access control. Hooks must never throw; they should return False or an empty string and be wrapped in try/except with safe fallbacks. Use frappe.has_permission with throw=True for automatic error handling, and catch frappe.PermissionError for custom responses; endpoints should use frappe.only_for and doc.has_permission before operations, returning HTTP 403 when access is denied.
When to Use It
- Before reading or modifying a document to enforce access control
- Securing a role-restricted API endpoint with frappe.only_for
- Validating permissions in client scripts via frappe.call error handling
- Handling permission_query_conditions and has_permission hooks without throwing
- Catching and responding to PermissionError in custom controllers
Quick Start
- Step 1: Review permission hooks and ensure they return False/empty string instead of throwing; add logging for failures
- Step 2: Add has_permission() checks with throw=True before sensitive operations
- Step 3: Wrap checks in try/except for frappe.PermissionError and implement a user-friendly fallback response
Best Practices
- Never throw in has_permission or permission_query_conditions hooks; return False or empty string and log errors
- Use frappe.has_permission() before operations with throw=True for automatic error handling
- Wrap permission checks in try/except and provide safe fallbacks when checks fail
- In controllers, use doc.has_permission() or doc.check_permission() to gate actions
- Use frappe.only_for() for role-restricted endpoints and return HTTP 403 on access denied
Example Use Cases
- Protect a customer document read by checking has_permission before access
- Restrict an admin API endpoint using frappe.only_for and proper permission checks
- Handle PermissionError in a client script via the error callback and provide guidance to the user
- Controller path using doc.has_permission() before write operations
- permission_query_conditions returns a safe fallback SQL condition when permission check fails