Skip to content

Has permission hook#3

Open
elshafei-developer wants to merge 17 commits into
developfrom
has_permission-hook
Open

Has permission hook#3
elshafei-developer wants to merge 17 commits into
developfrom
has_permission-hook

Conversation

@elshafei-developer

@elshafei-developer elshafei-developer commented May 18, 2026

Copy link
Copy Markdown
Owner

Please provide enough information so that others can review your pull request:

Explain the details for making this change. What existing problem does the pull request solve?

Screenshots/GIFs

Summary by CodeRabbit

  • Bug Fixes
    • Refined permission enforcement logic to strengthen access control consistency. The permission system now ensures that permission decisions can only restrict access, not grant additional permissions, improving security and preventing unintended access escalation.
    • Enhanced delete operation security by implementing stricter permission validation checks before the delete option is displayed to users.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR refactors controller permission hooks to enforce that controllers operate on the evaluated permission map and can only deny permissions, not grant them. Additionally, the Delete button visibility is tightened to require explicit model-level and permission-level delete capability.

Changes

Permission Tightening and Enforcement

Layer / File(s) Summary
Controller Permission Map-Based Enforcement
frappe/permissions.py
The has_controller_permissions function signature is extended to accept permissions dict. Implementation deep-copies the permissions, invokes controller hooks with the mutable dict, then post-processes to ensure no permission key transitions from 0 to 1, enforcing that controllers may only deny permissions.
Document Permission Evaluation Integration
frappe/permissions.py
Document-level permission evaluation now calls the controller permission check with the current permissions dict. The owner-check helper is adjusted to properly supply the is_owner argument to role-permission queries.
Delete Action Permission Tightening
frappe/public/js/frappe/form/toolbar.js
The Delete menu item visibility condition is extended to require both frappe.model.can_delete() and this.frm.perm[0].delete permission checks.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 Permissions now flow through hooks with care,
Controllers can only deny, never declare,
The map gets copied, enforced with delight,
No escalation from zero to one in the night!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Has permission hook' is vague and generic, failing to clearly communicate the primary changes (controller permission enforcement, permission map evaluation, and delete permission checks). Consider a more descriptive title that captures the main intent, such as 'Apply controller permission hooks against evaluated permission map' or 'Enforce controller permissions through map evaluation'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch has_permission-hook

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@elshafei-developer

Copy link
Copy Markdown
Owner Author

@claude can check this

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frappe/permissions.py`:
- Around line 494-503: The current logic treats any falsy controller_permission
as a hard deny; change the check after the frappe.call to only deny when the
controller explicitly returns False (i.e., test `controller_permission is
False`) so that None is treated as "no opinion" and does not block access; keep
the debug log and return False only on an explicit False from the hook,
otherwise allow the existing permission-map mutations to take effect.
- Around line 491-500: The loop captures org_permissions only once and treats
missing keys as non-denied, allowing later hooks to re-enable rights; fix by
computing and maintaining a persistent denied_keys set from the initial
permissions (e.g., denied_keys = {k for k,v in permissions.items() if v == 0})
before iterating over methods, and after each controller call (the block around
controller_permission = frappe.call(...)) enforce that any key in denied_keys
remains 0 in permissions, then extend denied_keys with any new keys set to 0 by
the current hook so once-denied rights cannot be re-enabled by later hooks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 20600239-0ae1-4bfe-991b-28820dab2f84

📥 Commits

Reviewing files that changed from the base of the PR and between 91264cb and ccd627f.

📒 Files selected for processing (2)
  • frappe/permissions.py
  • frappe/public/js/frappe/form/toolbar.js

Comment thread frappe/permissions.py
Comment on lines +491 to +500
org_permissions = copy.deepcopy(permissions)

for method in reversed(methods):
controller_permission = frappe.call(method, doc=doc, ptype=ptype, user=user, debug=debug)
controller_permission = frappe.call(
method, doc=doc, ptype=ptype, permissions=permissions, user=user, debug=debug
)
if permissions != org_permissions:
for key, value in permissions.items():
if org_permissions.get(key) == 0 and value == 1:
permissions[key] = 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Preserve deny-only semantics across every hook.

org_permissions is captured only once, so a later hook can restore a right that an earlier hook already denied. It also treats missing keys as non-denied, so after Line 269 collapses the map to {}, a hook can inject permissions[ptype] = 1 and bypass the earlier denial. That reintroduces permission escalation through the controller chain.

Proposed fix
-	org_permissions = copy.deepcopy(permissions)
-
 	for method in reversed(methods):
+		previous_permissions = copy.deepcopy(permissions)
 		controller_permission = frappe.call(
 			method, doc=doc, ptype=ptype, permissions=permissions, user=user, debug=debug
 		)
-		if permissions != org_permissions:
-			for key, value in permissions.items():
-				if org_permissions.get(key) == 0 and value == 1:
-					permissions[key] = 0
+		for key in set(previous_permissions) | set(permissions):
+			if permissions.get(key, 0) and not previous_permissions.get(key, 0):
+				permissions[key] = 0
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frappe/permissions.py` around lines 491 - 500, The loop captures
org_permissions only once and treats missing keys as non-denied, allowing later
hooks to re-enable rights; fix by computing and maintaining a persistent
denied_keys set from the initial permissions (e.g., denied_keys = {k for k,v in
permissions.items() if v == 0}) before iterating over methods, and after each
controller call (the block around controller_permission = frappe.call(...))
enforce that any key in denied_keys remains 0 in permissions, then extend
denied_keys with any new keys set to 0 by the current hook so once-denied rights
cannot be re-enabled by later hooks.

Comment thread frappe/permissions.py
Comment on lines +494 to 503
controller_permission = frappe.call(
method, doc=doc, ptype=ptype, permissions=permissions, user=user, debug=debug
)
if permissions != org_permissions:
for key, value in permissions.items():
if org_permissions.get(key) == 0 and value == 1:
permissions[key] = 0
debug and _debug_log(f"Controller permission check from {method}: {controller_permission}")
if not controller_permission:
return bool(controller_permission)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Treat None as “no opinion”, not denial.

Line 502 still turns any falsy return into a hard deny. With the new permission-map contract, a hook that only mutates permissions and falls through with None will now block access, which contradicts the local "True if not defined" behavior.

Proposed fix
-		if not controller_permission:
-			return bool(controller_permission)
+		if controller_permission is False:
+			return False
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
controller_permission = frappe.call(
method, doc=doc, ptype=ptype, permissions=permissions, user=user, debug=debug
)
if permissions != org_permissions:
for key, value in permissions.items():
if org_permissions.get(key) == 0 and value == 1:
permissions[key] = 0
debug and _debug_log(f"Controller permission check from {method}: {controller_permission}")
if not controller_permission:
return bool(controller_permission)
controller_permission = frappe.call(
method, doc=doc, ptype=ptype, permissions=permissions, user=user, debug=debug
)
if permissions != org_permissions:
for key, value in permissions.items():
if org_permissions.get(key) == 0 and value == 1:
permissions[key] = 0
debug and _debug_log(f"Controller permission check from {method}: {controller_permission}")
if controller_permission is False:
return False
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frappe/permissions.py` around lines 494 - 503, The current logic treats any
falsy controller_permission as a hard deny; change the check after the
frappe.call to only deny when the controller explicitly returns False (i.e.,
test `controller_permission is False`) so that None is treated as "no opinion"
and does not block access; keep the debug log and return False only on an
explicit False from the hook, otherwise allow the existing permission-map
mutations to take effect.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant