erpnext-permissions
Scannednpx machina-cli add skill OpenAEC-Foundation/ERPNext_Anthropic_Claude_Development_Skill_Package/erpnext-permissions --openclawERPNext Permissions Skill
Deterministic patterns for implementing robust permission systems in Frappe/ERPNext applications.
Overview
Frappe's permission system has five layers:
| Layer | Controls | Configured Via | Version |
|---|---|---|---|
| Role Permissions | What users CAN do | DocType permissions table | All |
| User Permissions | WHICH documents users see | User Permission records | All |
| Perm Levels | WHICH fields users see | Field permlevel property | All |
| Permission Hooks | Custom logic | hooks.py | All |
| Data Masking | MASKED field values | Field mask property | v16+ |
Quick Reference
Permission Types
| Type | Check | For |
|---|---|---|
read | frappe.has_permission(dt, "read") | View document |
write | frappe.has_permission(dt, "write") | Edit document |
create | frappe.has_permission(dt, "create") | Create new |
delete | frappe.has_permission(dt, "delete") | Delete |
submit | frappe.has_permission(dt, "submit") | Submit (submittable only) |
cancel | frappe.has_permission(dt, "cancel") | Cancel |
select | frappe.has_permission(dt, "select") | Select in Link (v14+) |
mask | Role permission for unmasked view | View unmasked data (v16+) |
Automatic Roles
| Role | Assigned To |
|---|---|
Guest | Everyone (including anonymous) |
All | All registered users |
Administrator | Only Administrator user |
Desk User | System Users (v15+) |
Essential API
Check Permission
# DocType level
frappe.has_permission("Sales Order", "write")
# Document level
frappe.has_permission("Sales Order", "write", "SO-00001")
frappe.has_permission("Sales Order", "write", doc=doc)
# For specific user
frappe.has_permission("Sales Order", "read", user="john@example.com")
# Throw on denial
frappe.has_permission("Sales Order", "delete", throw=True)
# On document instance
doc = frappe.get_doc("Sales Order", "SO-00001")
if doc.has_permission("write"):
doc.status = "Approved"
doc.save()
# Raise error if no permission
doc.check_permission("write")
Get Permissions
from frappe.permissions import get_doc_permissions
# Get all permissions for document
perms = get_doc_permissions(doc)
# {'read': 1, 'write': 1, 'create': 0, 'delete': 0, ...}
User Permissions
from frappe.permissions import add_user_permission, remove_user_permission
# Restrict user to specific company
add_user_permission(
doctype="Company",
name="My Company",
user="john@example.com",
is_default=1
)
# Remove restriction
remove_user_permission("Company", "My Company", "john@example.com")
# Get user's permissions
from frappe.permissions import get_user_permissions
perms = get_user_permissions("john@example.com")
Sharing
from frappe.share import add as add_share
# Share document with user
add_share(
doctype="Sales Order",
name="SO-00001",
user="jane@example.com",
read=1,
write=1
)
Data Masking (v16+)
Data Masking protects sensitive field values while keeping fields visible. Users without mask permission see masked values (e.g., ****, +91-811XXXXXXX).
Use Cases
- HR: Show employee details but mask salary amounts
- Support: Show phone numbers partially masked
- Finance: Show bank account fields without full numbers
Enable Data Masking
Via DocType (Developer Mode) or Customize Form:
{
"fieldname": "phone_number",
"fieldtype": "Data",
"options": "Phone",
"mask": 1
}
Supported Field Types:
- Data, Date, Datetime
- Currency, Float, Int, Percent
- Phone, Password
- Link, Dynamic Link
- Select, Read Only, Duration
Configure Permission
Add mask permission to roles that should see unmasked data:
{
"permissions": [
{"role": "Employee", "permlevel": 0, "read": 1},
{"role": "HR Manager", "permlevel": 0, "read": 1, "mask": 1}
]
}
How It Works
┌─────────────────────────────────────────────────────────────────────┐
│ DATA MASKING FLOW │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. Field has mask=1 in DocField configuration │
│ │
│ 2. System checks: meta.has_permlevel_access_to( │
│ fieldname=df.fieldname, │
│ df=df, │
│ permission_type="mask" │
│ ) │
│ │
│ 3. If user LACKS mask permission: │
│ └─► Value automatically masked in: │
│ • Form views │
│ • List views │
│ • Report views │
│ • API responses (/api/resource/, /api/method/) │
│ │
│ 4. If user HAS mask permission: │
│ └─► Full value displayed │
│ │
└─────────────────────────────────────────────────────────────────────┘
⚠️ Critical: Custom SQL Queries
Data Masking does NOT apply to:
- Custom SQL queries
- Query Reports using raw SQL
- Direct
frappe.db.sql()calls
You must implement masking manually:
def get_customer_report(filters):
data = frappe.db.sql("""
SELECT name, phone, email FROM tabCustomer
""", as_dict=True)
# Manual masking for users without permission
if not frappe.has_permission("Customer", "mask"):
for row in data:
if row.phone:
row.phone = mask_phone(row.phone)
return data
def mask_phone(phone):
"""Mask phone number: +91-81123XXXXX"""
if len(phone) > 5:
return phone[:6] + "X" * (len(phone) - 6)
return "****"
Permission Hooks
has_permission Hook
Add custom permission logic. Can only deny, not grant.
# hooks.py
has_permission = {
"Sales Order": "myapp.permissions.check_order_permission"
}
# myapp/permissions.py
def check_order_permission(doc, ptype, user):
"""
Returns:
None: Continue standard checks
False: Deny permission
"""
# Deny editing cancelled orders for non-managers
if ptype == "write" and doc.docstatus == 2:
if "Sales Manager" not in frappe.get_roles(user):
return False
return None # ALWAYS return None by default
permission_query_conditions Hook
Filter list queries. Only affects get_list(), NOT get_all().
# hooks.py
permission_query_conditions = {
"Customer": "myapp.permissions.customer_query"
}
# myapp/permissions.py
def customer_query(user):
"""Return SQL WHERE clause fragment."""
if not user:
user = frappe.session.user
# Managers see all
if "Sales Manager" in frappe.get_roles(user):
return ""
# Others see only their customers
return f"`tabCustomer`.owner = {frappe.db.escape(user)}"
CRITICAL: Always use frappe.db.escape() - never string concatenation!
get_list vs get_all
| Method | User Permissions | Query Hook |
|---|---|---|
frappe.get_list() | ✅ Applied | ✅ Applied |
frappe.get_all() | ❌ Ignored | ❌ Ignored |
# User-facing query - respects permissions
docs = frappe.get_list("Sales Order", filters={"status": "Open"})
# System query - bypasses permissions
docs = frappe.get_all("Sales Order", filters={"status": "Open"})
Field-Level Permissions (Perm Levels)
Configure Field
{
"fieldname": "salary",
"fieldtype": "Currency",
"permlevel": 1
}
Configure Role Access
{
"permissions": [
{"role": "Employee", "permlevel": 0, "read": 1},
{"role": "HR Manager", "permlevel": 0, "read": 1, "write": 1},
{"role": "HR Manager", "permlevel": 1, "read": 1, "write": 1}
]
}
Rule: Level 0 MUST be granted before higher levels.
Decision Tree
Need to control access?
├── To entire DocType → Role Permissions
├── To specific documents → User Permissions
├── To specific fields (hide completely) → Perm Levels
├── To specific fields (show masked) → Data Masking (v16+)
├── With custom logic → has_permission hook
└── For list queries → permission_query_conditions hook
Checking permissions in code?
├── Before action → frappe.has_permission() or doc.has_permission()
├── Raise error → doc.check_permission() or throw=True
└── Bypass needed → doc.flags.ignore_permissions = True (document why!)
Common Patterns
Owner-Only Edit
{
"role": "Sales User",
"read": 1, "write": 1, "create": 1,
"if_owner": 1
}
Check Before Action
@frappe.whitelist()
def approve_order(order_name):
doc = frappe.get_doc("Sales Order", order_name)
if not doc.has_permission("write"):
frappe.throw(_("No permission"), frappe.PermissionError)
doc.status = "Approved"
doc.save()
Role-Restricted Endpoint
@frappe.whitelist()
def sensitive_action():
frappe.only_for(["Manager", "Administrator"])
# Only reaches here if user has role
Critical Rules
- ALWAYS use permission API - Not role checks
- ALWAYS escape SQL -
frappe.db.escape(user) - ALWAYS use get_list - For user-facing queries
- ALWAYS return None - In has_permission hooks (not True)
- ALWAYS document - When using ignore_permissions
- ALWAYS clear cache - After permission changes:
frappe.clear_cache() - ALWAYS mask manually - In custom SQL queries (v16+)
Anti-Patterns
| ❌ Don't | ✅ Do |
|---|---|
if "Role" in frappe.get_roles() | frappe.has_permission(dt, ptype) |
frappe.get_all() for user queries | frappe.get_list() |
return True in has_permission | return None |
f"owner = '{user}'" | f"owner = {frappe.db.escape(user)}" |
frappe.throw() in hooks | return False |
| Assume masking in custom SQL | Implement masking manually |
Version Differences
| Feature | v14 | v15 | v16 |
|---|---|---|---|
select permission | ✅ | ✅ | ✅ |
Desk User role | ❌ | ✅ | ✅ |
| Custom Permission Types | ❌ | ❌ | ✅ (experimental) |
| Data Masking | ❌ | ❌ | ✅ |
mask permission type | ❌ | ❌ | ✅ |
Debugging
# Enable debug output
frappe.has_permission("Sales Order", "read", doc, debug=True)
# View logs
print(frappe.local.permission_debug_log)
# Check user's effective permissions
from frappe.permissions import get_doc_permissions
perms = get_doc_permissions(doc, user="john@example.com")
Reference Files
See references/ folder for:
permission-types-reference.md- All permission typespermission-api-reference.md- Complete API referencepermission-hooks-reference.md- Hook patternsexamples.md- Working examplesanti-patterns.md- Common mistakes
Related Skills
erpnext-database- Database operations that respect permissionserpnext-syntax-controllers- Controller permission checkserpnext-syntax-hooks- Hook configuration
Last updated: 2026-01-18 | Frappe v14/v15/v16
Source
git clone https://github.com/OpenAEC-Foundation/ERPNext_Anthropic_Claude_Development_Skill_Package/blob/main/skills/source/core/erpnext-permissions/SKILL.mdView on GitHub Overview
This skill covers the five-layer ERPNext permission model: Role Permissions, User Permissions, Perm Levels, Permission Hooks, and Data Masking (v16+). It explains how to design robust, scalable access control for Frappe/ERPNext apps, enforce security, and protect sensitive data across users and documents.
How This Skill Works
Permissions are evaluated in layers: Role Permissions govern what users can do on a DocType, User Permissions constrain access to specific documents, Perm Levels control field visibility, and Permission Hooks provide custom access logic. Data Masking ensures sensitive fields appear masked to users without the mask right (available in v16+), enabling compliant data exposure without sacrificing usability.
When to Use It
- When you must restrict a user to specific documents (e.g., a user can see only their own Sales Orders).
- When you need granular control over who can read, write, create, or delete a DocType.
- When you must show masked data to most users while preserving full values for admins (data masking).
- When custom access logic is needed beyond standard permissions (using permission hooks).
- When field visibility must vary by user or role (perm levels and field-level access).
Quick Start
- Step 1: Define roles and set up automatic roles (Guest, All, Administrator, Desk User) as a baseline.
- Step 2: Create targeted User Permissions and configure perm levels to grant document access selectively.
- Step 3: Enable Data Masking on sensitive fields (via DocType or Customize Form) and implement permission hooks for custom checks.
Best Practices
- Map roles to business processes first to anchor your permission model.
- Start with limited DocType permissions and refine with targeted User Permissions.
- Use Perm Levels to control field visibility rather than broad read/write restrictions.
- Enable and test Data Masking for PII and other sensitive fields in non-admin views.
- Regularly review, audit, and document permission changes as roles and data structures evolve.
Example Use Cases
- HR: Show salary amounts to HR/admin only; mask for other staff.
- Support: Partially mask customer phone numbers to protect privacy.
- Finance: Mask bank account numbers while keeping necessary financial details visible.
- Multi-branch access: Restrict users to their own Company via User Permissions.
- Custom approval flow: Use permission hooks to enforce branch or role-based approvals.
Frequently Asked Questions
Related Skills
ssh
chaterm/terminal-skills
SSH 管理与安全
file-operations
chaterm/terminal-skills
Linux file and directory operations
user-permissions
chaterm/terminal-skills
Linux user and permission management
audit
chaterm/terminal-skills
--- name: audit description: 安全审计 version: 1.0.0 author: terminal-skills tags: [security, audit, auditd, logging, compliance, vulnerability] --- # 安全审计 ## 概述 安全审计、漏洞扫描、合规检查技能。 ## auditd 审计系统 ### 安装与管理 ```bash # 安装 apt install auditd audispd-plugins # Debian/Ubuntu yum install audit
SEO Technical
openclaw/skills
Technical SEO audit across 8 categories: crawlability, indexability, security, URL structure, mobile, Core Web Vitals, structured data, and JavaScript rendering.
Auto-Update Systems Expert
martinholovsky/claude-skills-generator
Expert in Tauri auto-update implementation with focus on signature verification, rollback mechanisms, staged rollouts, and secure update distribution