$npx -y skills add Impertio-Studio/Frappe_Claude_Skill_Package --skill frappe-core-permissionsUse when implementing the Frappe/ERPNext permission system. Covers roles, user permissions, perm levels, data masking, and permission hooks for v14/v15/v16. Prevents common access control mistakes and security issues. Keywords: permissions, roles, user permissions, perm levels, d
| 1 | # Frappe Permissions |
| 2 | |
| 3 | > Deterministic patterns for the five-layer Frappe permission system. |
| 4 | |
| 5 | --- |
| 6 | |
| 7 | ## Permission Layers |
| 8 | |
| 9 | | Layer | Controls | Configured Via | Version | |
| 10 | |-------|----------|----------------|---------| |
| 11 | | **Role Permissions** | What users CAN do | DocType permissions table | All | |
| 12 | | **User Permissions** | WHICH records users see | User Permission DocType | All | |
| 13 | | **Perm Levels** | WHICH fields users see/edit | Field `permlevel` property | All | |
| 14 | | **Permission Hooks** | Custom deny logic | `hooks.py` | All | |
| 15 | | **Data Masking** | Masked field values | Field `mask` property | [v16+] | |
| 16 | |
| 17 | --- |
| 18 | |
| 19 | ## Decision Tree |
| 20 | |
| 21 | ``` |
| 22 | Need to control access? |
| 23 | ├── Who can Create/Read/Write/Delete a DocType? → Role Permissions |
| 24 | ├── Which specific records can a user see? → User Permissions |
| 25 | ├── Which fields should be hidden? → Perm Levels (permlevel 1+) |
| 26 | ├── Which fields show masked values? → Data Masking [v16+] |
| 27 | ├── Custom runtime deny logic? → has_permission hook |
| 28 | ├── Filter list queries dynamically? → permission_query_conditions hook |
| 29 | └── Share one document with one user? → frappe.share |
| 30 | |
| 31 | Checking permissions in code? |
| 32 | ├── Before action → frappe.has_permission() or doc.has_permission() |
| 33 | ├── Raise on denial → doc.check_permission() or throw=True |
| 34 | ├── System bypass → doc.flags.ignore_permissions = True (ALWAYS document why) |
| 35 | └── List query → ALWAYS use frappe.get_list() for user-facing data |
| 36 | ``` |
| 37 | |
| 38 | --- |
| 39 | |
| 40 | ## Permission Types |
| 41 | |
| 42 | | Type | API Check | Applies To | |
| 43 | |------|-----------|------------| |
| 44 | | `read` | `frappe.has_permission(dt, "read")` | All DocTypes | |
| 45 | | `write` | `frappe.has_permission(dt, "write")` | All DocTypes | |
| 46 | | `create` | `frappe.has_permission(dt, "create")` | All DocTypes | |
| 47 | | `delete` | `frappe.has_permission(dt, "delete")` | All DocTypes | |
| 48 | | `submit` | `frappe.has_permission(dt, "submit")` | Submittable only | |
| 49 | | `cancel` | `frappe.has_permission(dt, "cancel")` | Submittable only | |
| 50 | | `amend` | `frappe.has_permission(dt, "amend")` | Submittable only | |
| 51 | | `select` | `frappe.has_permission(dt, "select")` | Link fields [v14+] | |
| 52 | | `report` | N/A | Report Builder access | |
| 53 | | `export` | N/A | Excel/CSV export | |
| 54 | | `import` | N/A | Data Import Tool | |
| 55 | | `share` | N/A | Share with other users | |
| 56 | | `print` | N/A | Print/PDF generation | |
| 57 | | `email` | N/A | Send email | |
| 58 | | `mask` | Role permission for unmasked view | Data Masking [v16+] | |
| 59 | |
| 60 | --- |
| 61 | |
| 62 | ## Automatic Roles |
| 63 | |
| 64 | | Role | Assigned To | Notes | |
| 65 | |------|-------------|-------| |
| 66 | | `Guest` | Everyone (including anonymous) | Public pages | |
| 67 | | `All` | All registered users | Basic authenticated access | |
| 68 | | `Administrator` | Only the Administrator user | ALWAYS has all permissions | |
| 69 | | `Desk User` | System Users only | [v15+] | |
| 70 | |
| 71 | --- |
| 72 | |
| 73 | ## Essential API |
| 74 | |
| 75 | ### Check Permission |
| 76 | |
| 77 | ```python |
| 78 | # DocType-level |
| 79 | frappe.has_permission("Sales Order", "write") |
| 80 | |
| 81 | # Document-level (by name or object) |
| 82 | frappe.has_permission("Sales Order", "write", "SO-00001") |
| 83 | frappe.has_permission("Sales Order", "write", doc=doc) |
| 84 | |
| 85 | # For specific user |
| 86 | frappe.has_permission("Sales Order", "read", user="john@example.com") |
| 87 | |
| 88 | # Throw on denial |
| 89 | frappe.has_permission("Sales Order", "delete", throw=True) |
| 90 | |
| 91 | # Debug mode — prints evaluation steps |
| 92 | frappe.has_permission("Sales Order", "read", debug=True) |
| 93 | print(frappe.local.permission_debug_log) |
| 94 | ``` |
| 95 | |
| 96 | ### Document Instance Methods |
| 97 | |
| 98 | ```python |
| 99 | doc = frappe.get_doc("Sales Order", "SO-00001") |
| 100 | |
| 101 | # Returns bool |
| 102 | if doc.has_permission("write"): |
| 103 | doc.status = "Approved" |
| 104 | doc.save() |
| 105 | |
| 106 | # Raises frappe.PermissionError if denied |
| 107 | doc.check_permission("write") |
| 108 | ``` |
| 109 | |
| 110 | ### Get Effective Permissions |
| 111 | |
| 112 | ```python |
| 113 | from frappe.permissions import get_doc_permissions |
| 114 | |
| 115 | perms = get_doc_permissions(doc) |
| 116 | # {'read': 1, 'write': 1, 'create': 0, 'delete': 0, ...} |
| 117 | |
| 118 | perms = get_doc_permissions(doc, user="john@example.com") |
| 119 | ``` |
| 120 | |
| 121 | --- |
| 122 | |
| 123 | ## User Permissions (Record-Level) |
| 124 | |
| 125 | Restrict users to specific Link field values (e.g., specific Company, Territory). |
| 126 | |
| 127 | ```python |
| 128 | from frappe.permissions import add_user_permission, remove_user_permission |
| 129 | |
| 130 | # Restrict user to one company |
| 131 | add_user_permission( |
| 132 | doctype="Company", |
| 133 | name="My Company", |
| 134 | user="john@example.com", |
| 135 | is_default=1, # auto-fill in new documents |
| 136 | applicable_for="Sales Order" # only for this DocType (optional) |
| 137 | ) |
| 138 | |
| 139 | # Remove restriction |
| 140 | remove_user_permission("Company", "My Company", "john@example.com") |
| 141 | |
| 142 | # Query current restricti |