xiaohongshu
xiaohongshu
erpnext-permissions
Complete guide for Frappe/ERPNext permission system - roles, user permissions, perm levels, data masking, and permission hooks
Full skill instructions
Deterministic patterns for implementing robust permission systems in Frappe/ERPNext applications.
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+ |
| 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+) |
| Role | Assigned To |
|---|---|
Guest | Everyone (including anonymous) |
All | All registered users |
Administrator | Only Administrator user |
Desk User | System Users (v15+) |
# 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="[email protected]")
# 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")
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, ...}
from frappe.permissions import add_user_permission, remove_user_permission
# Restrict user to specific company
add_user_permission(
doctype="Company",
name="My Company",
user="[email protected]",
is_default=1
)
# Remove restriction
remove_user_permission("Company", "My Company", "[email protected]")
# Get user's permissions
from frappe.permissions import get_user_permissions
perms = get_user_permissions("[email protected]")
from frappe.share import add as add_share
# Share document with user
add_share(
doctype="Sales Order",
name="SO-00001",
user="[email protected]",
read=1,
write=1
)
Data Masking protects sensitive field values while keeping fields visible. Users without mask permission see masked values (e.g., ****, +91-811XXXXXXX).
Via DocType (Developer Mode) or Customize Form:
{
"fieldname": "phone_number",
"fieldtype": "Data",
"options": "Phone",
"mask": 1
}
Supported Field Types:
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}
]
}
┌─────────────────────────────────────────────────────────────────────┐
│ 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 │
│ │
└─────────────────────────────────────────────────────────────────────┘
Data Masking does NOT apply to:
frappe.db.sql() callsYou 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 "****"
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
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!
| 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"})
{
"fieldname": "salary",
"fieldtype": "Currency",
"permlevel": 1
}
{
"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.
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!)
{
"role": "Sales User",
"read": 1, "write": 1, "create": 1,
"if_owner": 1
}
@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()
@frappe.whitelist()
def sensitive_action():
frappe.only_for(["Manager", "Administrator"])
# Only reaches here if user has role
frappe.db.escape(user)frappe.clear_cache()| ❌ 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 |
| Feature | v14 | v15 | v16 |
|---|---|---|---|
select permission | ✅ | ✅ | ✅ |
Desk User role | ❌ | ✅ | ✅ |
| Custom Permission Types | ❌ | ❌ | ✅ (experimental) |
| Data Masking | ❌ | ❌ | ✅ |
mask permission type | ❌ | ❌ | ✅ |
# 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="[email protected]")
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 mistakeserpnext-database - Database operations that respect permissionserpnext-syntax-controllers - Controller permission checkserpnext-syntax-hooks - Hook configurationLast updated: 2026-01-18 | Frappe v14/v15/v16
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience