frappe-errors-permissions
$
npx mdskill add Impertio-Studio/Frappe_Claude_Skill_Package/frappe-errors-permissionsDiagnose and fix permission errors in Frappe/ERPNext instantly.
- Resolves access denials, role conflicts, and hook failures.
- Integrates with Frappe v14-v16 permission hooks and debug workflows.
- Analyzes error messages to identify missing roles or permissions.
- Suggests specific fixes like granting User Permission or adjusting perm_level.
SKILL.md
.github/skills/frappe-errors-permissionsView on GitHub ↗
---
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