$npx -y skills add Impertio-Studio/Frappe_Claude_Skill_Package --skill frappe-errors-permissionsUse 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,
| 1 | # Permission Error Handling |
| 2 | |
| 3 | For permission system overview see `frappe-core-permissions`. For hook syntax see `frappe-syntax-hooks`. |
| 4 | |
| 5 | --- |
| 6 | |
| 7 | ## Quick Diagnostic: Error Message -> Cause -> Fix |
| 8 | |
| 9 | | Error Message | Cause | Fix | |
| 10 | |---------------|-------|-----| |
| 11 | | `frappe.exceptions.PermissionError` | User lacks role or doc-level access | Add role in Role Permissions Manager or grant User Permission | |
| 12 | | "Not permitted" on document open | `has_permission` hook returns False or role missing read | Check `frappe.permissions.get_doc_permissions(doc, user)` output | |
| 13 | | List view shows 0 records | `permission_query_conditions` returns overly restrictive SQL | Debug the SQL condition; check User Permissions for the Link field | |
| 14 | | "Not allowed to access ... for Guest" | Endpoint missing `allow_guest=True` or DocType lacks Guest read | Add `allow_guest=True` to `@frappe.whitelist()` | |
| 15 | | 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` | |
| 16 | | "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 | |
| 17 | | Sharing not granting access | Sharing adds access but never overrides role absence | User MUST have base role permission; sharing only adds doc-level grants | |
| 18 | | `ignore_permissions` has no effect | Flag set after `get_doc` already checked permissions | Set `flags.ignore_permissions = True` BEFORE calling `save()` or `insert()` | |
| 19 | | System Manager cannot access | Custom `has_permission` hook denies without checking role | ALWAYS check for System Manager / Administrator in hook | |
| 20 | |
| 21 | --- |
| 22 | |
| 23 | ## Decision Tree: Where Is the Error? |
| 24 | |
| 25 | ``` |
| 26 | Permission error occurred |
| 27 | ├── Document-level (single doc access)? |
| 28 | │ ├── has_permission hook returning False? |
| 29 | │ │ └── Debug: frappe.permissions.get_doc_permissions(doc, user) |
| 30 | │ ├── User Permission restricting Link field? |
| 31 | │ │ └── Check: frappe.get_all("User Permission", filters={"user": user}) |
| 32 | │ ├── perm_level blocking field? |
| 33 | │ │ └── Check: role has permission row for that perm_level |
| 34 | │ └── Sharing not applying? |
| 35 | │ └── Check: user has base role + sharing record exists |
| 36 | ├── List-level (0 records in list view)? |
| 37 | │ ├── permission_query_conditions returning bad SQL? |
| 38 | │ │ └── Debug: run condition manually in MariaDB console |
| 39 | │ ├── User Permission auto-filtering? |
| 40 | │ │ └── Check "Apply User Permissions" checkbox on role row |
| 41 | │ └── get_all vs get_list confusion? |
| 42 | │ └── ALWAYS use get_list for user-facing queries |
| 43 | ├── API endpoint (403 response)? |
| 44 | │ ├── Missing @frappe.whitelist()? |
| 45 | │ │ └── Add decorator to Python method |
| 46 | │ ├── Missing allow_guest=True? |
| 47 | │ │ └── Add allow_guest parameter for public endpoints |
| 48 | │ └── frappe.only_for() blocking? |
| 49 | │ └── Check user has required role |
| 50 | └── System Manager bypass failing? |
| 51 | └── Custom hook does not check for System Manager role |
| 52 | ``` |
| 53 | |
| 54 | --- |
| 55 | |
| 56 | ## Permission Hook Errors |
| 57 | |
| 58 | ### has_permission Hook: NEVER Throw |
| 59 | |
| 60 | ```python |
| 61 | # hooks.py |
| 62 | has_permission = { |
| 63 | "Sales Order": "myapp.permissions.sales_order_has_permission", |
| 64 | } |
| 65 | ``` |
| 66 | |
| 67 | ```python |
| 68 | # WRONG — Breaks ALL document access |
| 69 | def sales_order_has_permission(doc, user, permission_type): |
| 70 | if doc.status == "Locked": |
| 71 | frappe.throw("Locked") # NEVER do this |
| 72 | |
| 73 | # CORRECT — Return False to deny, None to defer |
| 74 | def sales_order_has_permission(doc, user, permission_type): |
| 75 | """ |
| 76 | ALWAYS wrap in try/except. NEVER throw. NEVER return True. |
| 77 | Returns: False (deny) or None (defer to standard system). |
| 78 | """ |
| 79 | try: |
| 80 | user = user or frappe.session.user |
| 81 | if user == "Administrator": |
| 82 | return None |
| 83 | |
| 84 | # ALWAYS check System Manager early |
| 85 | if "System Manager" in frappe.get_roles(user): |
| 86 | return None |
| 87 | |
| 88 | # Deny write on locked docs (but allow read) |
| 89 | if permission_type in ("write", "delete", "cancel"): |
| 90 | if do |