Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
openaec-foundation avatar

Frappe Core Permissions

  • 1 installs
  • 159 repo stars
  • Updated July 8, 2026
  • openaec-foundation/erpnext_anthropic_claude_development_skill_package

Covers the Frappe permission system including roles, user permissions, perm levels, data masking, and has_permission hooks for access control.

About

A reference skill for implementing the Frappe/ERPNext permission system across roles, user permissions, perm levels, and hooks. A developer uses it to set up access control and avoid security mistakes.

  • Five-layer permission model: roles, user permissions, perm levels
  • Data masking and has_permission hooks for row/field-level access

Frappe Core Permissions by the numbers

  • 1 all-time installs (skills.sh)
  • Ranked #1,835 of 2,203 Security skills by installs in the Skillselion catalog
  • Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/openaec-foundation/erpnext_anthropic_claude_development_skill_package --skill frappe-core-permissions

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs1
repo stars159
Last updatedJuly 8, 2026
Repositoryopenaec-foundation/erpnext_anthropic_claude_development_skill_package

What it does

Covers the Frappe permission system including roles, user permissions, perm levels, data masking, and has_permission hooks for access control.

Files

SKILL.mdMarkdownGitHub ↗

Frappe Permissions

Deterministic patterns for the five-layer Frappe permission system.

---

Permission Layers

LayerControlsConfigured ViaVersion
Role PermissionsWhat users CAN doDocType permissions tableAll
User PermissionsWHICH records users seeUser Permission DocTypeAll
Perm LevelsWHICH fields users see/editField permlevel propertyAll
Permission HooksCustom deny logichooks.pyAll
Data MaskingMasked field valuesField mask property[v16+]

---

Decision Tree

Need to control access?
├── Who can Create/Read/Write/Delete a DocType? → Role Permissions
├── Which specific records can a user see? → User Permissions
├── Which fields should be hidden? → Perm Levels (permlevel 1+)
├── Which fields show masked values? → Data Masking [v16+]
├── Custom runtime deny logic? → has_permission hook
├── Filter list queries dynamically? → permission_query_conditions hook
└── Share one document with one user? → frappe.share

Checking permissions in code?
├── Before action → frappe.has_permission() or doc.has_permission()
├── Raise on denial → doc.check_permission() or throw=True
├── System bypass → doc.flags.ignore_permissions = True (ALWAYS document why)
└── List query → ALWAYS use frappe.get_list() for user-facing data

---

Permission Types

TypeAPI CheckApplies To
readfrappe.has_permission(dt, "read")All DocTypes
writefrappe.has_permission(dt, "write")All DocTypes
createfrappe.has_permission(dt, "create")All DocTypes
deletefrappe.has_permission(dt, "delete")All DocTypes
submitfrappe.has_permission(dt, "submit")Submittable only
cancelfrappe.has_permission(dt, "cancel")Submittable only
amendfrappe.has_permission(dt, "amend")Submittable only
selectfrappe.has_permission(dt, "select")Link fields [v14+]
reportN/AReport Builder access
exportN/AExcel/CSV export
importN/AData Import Tool
shareN/AShare with other users
printN/APrint/PDF generation
emailN/ASend email
maskRole permission for unmasked viewData Masking [v16+]

---

Automatic Roles

RoleAssigned ToNotes
GuestEveryone (including anonymous)Public pages
AllAll registered usersBasic authenticated access
AdministratorOnly the Administrator userALWAYS has all permissions
Desk UserSystem Users only[v15+]

---

Essential API

Check Permission

# DocType-level
frappe.has_permission("Sales Order", "write")

# Document-level (by name or object)
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)

# Debug mode — prints evaluation steps
frappe.has_permission("Sales Order", "read", debug=True)
print(frappe.local.permission_debug_log)

Document Instance Methods

doc = frappe.get_doc("Sales Order", "SO-00001")

# Returns bool
if doc.has_permission("write"):
    doc.status = "Approved"
    doc.save()

# Raises frappe.PermissionError if denied
doc.check_permission("write")

Get Effective Permissions

from frappe.permissions import get_doc_permissions

perms = get_doc_permissions(doc)
# {'read': 1, 'write': 1, 'create': 0, 'delete': 0, ...}

perms = get_doc_permissions(doc, user="john@example.com")

---

User Permissions (Record-Level)

Restrict users to specific Link field values (e.g., specific Company, Territory).

from frappe.permissions import add_user_permission, remove_user_permission

# Restrict user to one company
add_user_permission(
    doctype="Company",
    name="My Company",
    user="john@example.com",
    is_default=1,            # auto-fill in new documents
    applicable_for="Sales Order"  # only for this DocType (optional)
)

# Remove restriction
remove_user_permission("Company", "My Company", "john@example.com")

# Query current restrictions
from frappe.permissions import get_user_permissions
perms = get_user_permissions("john@example.com")
# {"Company": [{"doc": "My Company", "is_default": 1}], ...}

---

Sharing (Document-Level)

Grant access to a single document for a specific user.

from frappe.share import add as add_share, remove as remove_share

add_share("Sales Order", "SO-00001", "jane@example.com",
          read=1, write=1, share=0, notify=1)

remove_share("Sales Order", "SO-00001", "jane@example.com")

# Share with everyone
add_share("Sales Order", "SO-00001", everyone=1, read=1)

---

Field-Level Permissions (Perm Levels)

Group fields by permlevel (0-9). Level 0 MUST be granted before higher levels.

{
  "fields": [
    {"fieldname": "employee_name", "permlevel": 0},
    {"fieldname": "salary",        "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: Levels do NOT imply hierarchy. Level 2 is not "higher" than level 1. They are independent field groups.

---

Data Masking [v16+]

Fields with mask=1 show masked values (e.g., ****, +91-811XXXXXXX) to users without mask permission.

{
  "fieldname": "phone_number", "fieldtype": "Data", "mask": 1
}

Grant mask permission to roles that MUST see unmasked values:

{"role": "HR Manager", "permlevel": 0, "read": 1, "mask": 1}

CRITICAL: Data masking does NOT apply to frappe.db.sql() or Query Reports with raw SQL. You MUST mask manually in custom SQL queries.

---

Permission Hooks

has_permission: Custom Deny Logic

Can only deny access. NEVER returns True to grant. ALWAYS returns None to continue standard checks.

# hooks.py
has_permission = {
    "Sales Order": "myapp.permissions.check_order_permission"
}
# myapp/permissions.py
def check_order_permission(doc, ptype, user):
    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: Filter List Queries

Returns SQL WHERE clause fragment. Only affects get_list(), NOT get_all().

# hooks.py
permission_query_conditions = {
    "Customer": "myapp.permissions.customer_query"
}
def customer_query(user):
    if not user:
        user = frappe.session.user
    if "Sales Manager" in frappe.get_roles(user):
        return ""
    return f"`tabCustomer`.owner = {frappe.db.escape(user)}"

ALWAYS use frappe.db.escape() — NEVER use string concatenation with raw user input.

---

get_list vs get_all

MethodUser PermissionsQuery HookUse For
frappe.get_list()AppliedAppliedUser-facing queries
frappe.get_all()IgnoredIgnoredSystem/background queries

ALWAYS use get_list() when returning data to users. get_all() bypasses ALL permission filtering.

---

Common Patterns

Owner-Only Edit

{"role": "Sales User", "read": 1, "write": 1, "create": 1, "if_owner": 1}

Role-Restricted Endpoint

@frappe.whitelist()
def sensitive_action():
    frappe.only_for(["Manager", "Administrator"])
    # Only reaches here if user has one of these roles

Bypass Permissions (Document Why!)

# On document — ALWAYS add a comment explaining the reason
doc.flags.ignore_permissions = True
doc.save()

# On method call
doc.save(ignore_permissions=True)
doc.insert(ignore_permissions=True)

---

Critical Rules

1. ALWAYS use frappe.has_permission() — NEVER check roles directly for access control 2. ALWAYS use frappe.get_list() for user-facing queries — NEVER get_all() 3. ALWAYS escape SQL in query hooks — frappe.db.escape(user) 4. ALWAYS prefix table names in query hooks — ` tabDocType.fieldname 5. **ALWAYS** return None in has_permission hooks by default — NEVER True 6. **ALWAYS** clear cache after permission changes — frappe.clear_cache() 7. **ALWAYS** document ignore_permissions usage with a comment 8. **NEVER** throw errors in has_permission hooks — return False` to deny 9. NEVER grant permlevel 1+ without granting permlevel 0 first 10. NEVER assume data masking applies to custom SQL queries [v16+]

---

Anti-Patterns

Do NOTDo Instead
if "Role" in frappe.get_roles() for accessfrappe.has_permission(dt, ptype)
frappe.get_all() for user queriesfrappe.get_list()
return True in has_permission hookreturn None
f"owner = '{user}'" in SQLf"owner = {frappe.db.escape(user)}"
frappe.throw() in permission hooksreturn False
frappe.db.set_value() for user-facing updatesdoc.save() with permission check
Sensitive data in error messagesGeneric frappe.PermissionError

---

Version Differences

Featurev14v15v16
select permissionYesYesYes
Desk User roleNoYesYes
Data Masking (mask field)NoNoYes
mask permission typeNoNoYes
Custom Permission TypesNoNoExperimental

---

Permission Precedence

1. Administrator — ALWAYS has all permissions (cannot be restricted) 2. Role Permissions — Based on assigned roles 3. User Permissions — Restricts to specific document values 4. has_permission hook — Can only deny (any False = denied) 5. Sharing — Grants access to shared documents 6. if_owner — Further restricts to owned documents

---

Reference Files

FileContents
permission-types-reference.mdAll permission types with options
permission-api-reference.mdComplete API with all signatures
permission-hooks-reference.mdHook patterns and examples
examples.mdWorking implementation examples
anti-patterns.mdCommon mistakes and fixes

Related Skills

  • frappe-core-database — Database operations that respect permissions
  • frappe-core-api — API endpoints with permission checks
  • frappe-syntax-controllers — Controller permission validation
  • frappe-syntax-hooks — Hook configuration patterns

---

Verified against Frappe docs 2026-03-20 | Frappe v14/v15/v16

Related skills

Securityappsecaudit

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.