Use when debugging or handling permission errors in Frappe/ERPNext. Prevents broken document access from throwing in permission hooks. Covers PermissionError (403), has_permission hook failures, User Permission restricting too much or too little, perm_level blocking field access, System Manager bypass not working, Guest access denied, sharing permissions not applying, permission_query_conditions breaking get_list, owner-based permissions confusion, Apply User Permission checkbox behavior, and...
Scanned 9/6/2026
Install to Claude Code
npx -y skills add OpenAEC-Foundation/ERPNext_Anthropic_Claude_Development_Skill_Package --skill frappe-errors-permissions --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Frappe Errors Permissions?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/openaec-foundation-frappe-errors-permissions)More formats (shields.io, HTML) on the badges page.
---
name: frappe-errors-permissions
description: >
Use when debugging or handling permission errors in Frappe/ERPNext.
Prevents broken document access from throwing in permission hooks.
Covers PermissionError (403), has_permission hook failures, User Permission
restricting too much or too little, perm_level blocking field access,
System Manager bypass not working, Guest access denied, sharing permissions
not applying, permission_query_conditions breaking get_list, owner-based
permissions confusion, Apply User Permission checkbox behavior, and the
permission debug workflow using frappe.permissions.get_doc_permissions.
Keywords: PermissionError, has_permission, permission_query_conditions,, permission denied, cannot access, user blocked, sharing not working, role not enough.
User Permission, perm_level, sharing, guest access, owner permission.
license: MIT
compatibility: "Claude Code, Claude.ai Projects, Claude API. Frappe v14-v16."
metadata:
author: OpenAEC-Foundation
version: "2.0"
---
# Permission Error Handling
For permission system overview see `frappe-core-permissions`. For hook syntax see `frappe-syntax-hooks`.
---
## Quick Diagnostic: Error Message -> Cause -> Fix
| Error Message | Cause | Fix |
|---------------|-------|-----|
| `frappe.exceptions.PermissionError` | User lacks role or doc-level access | Add role in Role Permissions Manager or grant User Permission |
| "Not permitted" on document open | `has_permission` hook returns False or role missing read | Check `frappe.permissions.get_doc_permissions(doc, user)` output |
| List view shows 0 records | `permission_query_conditions` returns overly restrictive SQL | Debug the SQL condition; check User Permissions for the Link field |
| "Not allowed to access ... for Guest" | Endpoint missing `allow_guest=True` or DocType lacks Guest read | Add `allow_guest=True` to `@frappe.whitelist()` |
| Field invisible despite role having read | `perm_level` > 0 on field and role lacks that level | Add role permission row for the specific `perm_level` |
| "User Permission restriction" blocking | User Permission on a Link field auto-filters documents | Uncheck "Apply User Permissions" on that role row or add matching User Permission |
| Sharing not granting access | Sharing adds access but never overrides role absence | User MUST have base role permission; sharing only adds doc-level grants |
| `ignore_permissions` has no effect | Flag set after `get_doc` already checked permissions | Set `flags.ignore_permissions = True` BEFORE calling `save()` or `insert()` |
| System Manager cannot access | Custom `has_permission` hook denies without checking role | ALWAYS check for System Manager / Administrator in hook |
---
## Decision Tree: Where Is the Error?
```
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
```
---
## Permission Hook Errors
### has_permission Hook: NEVER Throw
```python
# hooks.py
has_permission = {
"Sales Order": "myapp.permissions.sales_order_has_permission",
}
```
```python
# 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
```
**Critical rules for has_permission hooks:**
- ALWAYS return `None` to defer, `False` to deny. NEVER return `True` — hooks can only restrict, not grant.
- ALWAYS wrap the entire function in `try/except`. An unhandled exception breaks ALL access to that DocType.
- ALWAYS check for `Administrator` and `System Manager` at the top.
- NEVER call `frappe.throw()` inside this hook.
### permission_query_conditions: NEVER Throw
```python
# hooks.py
permission_query_conditions = {
"Sales Order": "myapp.permissions.sales_order_query",
}
```
```python
# 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)}"
```
**Critical rules for permission_query_conditions:**
- NEVER throw errors — return `"1=0"` to deny all or a restrictive SQL string.
- ALWAYS use `frappe.db.escape()` for every user-supplied value.
- This hook ONLY affects `frappe.get_list()` / `frappe.db.get_list()`. It does NOT affect `frappe.get_all()` / `frappe.db.get_all()`.
---
## User Permission Errors
### Too Restrictive: Records Disappear
```
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.
```
**Debug steps:**
```python
# Step 1: Check what User Permissions exist
frappe.get_all("User Permission",
filters={"user": "john@example.com"},
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"), "john@example.com")
```
**Fix patterns:**
- Remove overly broad User Permissions that filter unintended DocTypes.
- Use the `applicable_for` field [v14+] to limit which DocType a User Permission applies to.
- Uncheck "Apply User Permissions" on the role permission row if blanket filtering is unwanted.
### Too Permissive: User Sees Everything
```
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.
```
**Fix:** Ensure the role permission row has "Apply User Permissions" checked AND the DocType has a Link field to the restricted DocType.
---
## perm_level Errors
```
Error: Field "cost_center" is invisible despite user having read permission.
Cause: Field has permlevel=1 but role only has permission for permlevel=0.
```
```python
# Check which perm_levels a role has access to
frappe.get_all("DocPerm",
filters={"parent": "Sales Invoice", "role": "Accounts User"},
fields=["permlevel", "read", "write"])
```
**Fix:** Add a new row in the DocType's Permission table for the role at the required `permlevel`.
---
## Sharing Permission Errors
```
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.
```
```python
# Share a document (user MUST already have a role with at least read)
frappe.share.add("Sales Order", "SO-001", "john@example.com",
read=1, write=1, share=1)
# Check if sharing grants access
frappe.share.get_sharing_permissions("Sales Order", "SO-001", "john@example.com")
```
**Rules:**
- ALWAYS ensure the user has at least one role with read permission on the DocType before sharing.
- Sharing adds document-level grants on top of role permissions.
- [v15+] `frappe.share.add` accepts `notify=1` to send email notification.
---
## Guest Access Errors
```
Error: "Not permitted" for unauthenticated users.
Cause: DocType has no Guest read permission, or API missing allow_guest.
```
**Fix for web pages / portal:**
```python
# Add Guest read permission in DocType Permission table
# Role: Guest, Level: 0, Read: checked
```
**Fix for API endpoints:**
```python
@frappe.whitelist(allow_guest=True)
def public_endpoint():
# ALWAYS validate input — guest endpoints are exposed to the internet
pass
```
**NEVER grant Guest write/create/delete permissions** unless the DocType is specifically designed for public submission (e.g., Web Form backend).
---
## Debug Workflow: frappe.permissions
```python
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="john@example.com")
# 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="john@example.com", throw=False)
# List all roles for a user
frappe.get_roles("john@example.com")
# Check User Permissions
frappe.get_all("User Permission",
filters={"user": "john@example.com"},
fields=["allow", "for_value", "applicable_for", "is_default"])
```
---
## Critical Rules
### ALWAYS
1. **Wrap permission hooks in try/except** — unhandled errors break all access
2. **Return None (not True) in has_permission** — hooks can only deny
3. **Use frappe.db.escape() in query conditions** — prevent SQL injection
4. **Check System Manager / Administrator first** in custom hooks
5. **Use frappe.has_permission(throw=True)** for endpoint permission checks
6. **Use get_list (not get_all)** for user-facing queries — get_all bypasses permissions
7. **Log permission denials** for security audit with `frappe.log_error()`
### NEVER
1. **Throw in has_permission or permission_query_conditions** — breaks access entirely
2. **Return True in has_permission** — has no effect, hooks can only restrict
3. **Use string formatting for SQL** — use `frappe.db.escape()` to prevent injection
4. **Grant Guest write/delete permissions** — security risk
5. **Use ignore_permissions without documenting why** — creates audit gaps
6. **Assume sharing replaces role permissions** — sharing only supplements
---
## Reference Files
| File | Contents |
|------|----------|
| `references/patterns.md` | Complete hook patterns, query conditions, API endpoints |
| `references/examples.md` | Full working examples with hooks.py configuration |
| `references/anti-patterns.md` | 15 common mistakes with wrong/correct comparisons |
---
## See Also
- `frappe-core-permissions` — Permission system architecture
- `frappe-errors-api` — API error handling (401/403/404)
- `frappe-errors-hooks` — Hook error handling patterns
- `frappe-syntax-hooks` — Hook registration syntax
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!