-
Notifications
You must be signed in to change notification settings - Fork 7.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: render doctype caster server-script-callable #42160
base: develop
Are you sure you want to change the base?
Conversation
This won't work even if the method was allowed for guest. Passing |
@sagarvora Correct, which is good. But this also the reason for this PR. I couldn't find another way to implement this. Overall, it seems a overly subtle and thus likely unintended distinction in behaviour between class MyDocument(Document):
@frappe.whitelisted()
def make_something(self): # this is *also* server script callable
pass
@frappe.whitelisted() # this is not
def make_something(*args):
pass If I had some design authority on the framework, my recommendation would be to clarify this situation through refactoring. Confusion and subtleness is rarely good for secure practices. |
This comment was marked as outdated.
This comment was marked as outdated.
What about a (hypothetical, rust like) Which also implements EDIT: or rather both should be implementable interchangably (either, or) so that the domain isn't violated, e.g. If I intuit this correctly, permission issues could be wholly encapsulated on the combination of both involved doctypes but without necessity for explicit overrides in most cases. EDIT2: Here we go -> frappe/frappe#26991 |
For context on the motivation of this PR, see also: #40845 (comment) |
75f3748
to
e7749a2
Compare
Sem Grep Rule violation: erpnext/selling/doctype/sales_order/sales_order.py
❯❯❱ frappe-semgrep-rules.rules.require-permission-decorator-on-conversion-methods
'_into_sales_invoice' in 'SalesOrder' crosses doctype boundaries. Explicitly declare its extended
security context with @frappe.requires_permission(<doctype>, <perm>).
744┆ def _into_sales_invoice(self):
745┆ make_sales_invoice(self.name)
BLOCKING CODE RULES FIRED:
frappe-semgrep-rules.rules.require-permission-decorator-on-conversion-methods |
38ce820
to
1432d27
Compare
352c0b7
to
2ac95e2
Compare
2ac95e2
to
b5c4b49
Compare
d.db_set(field, value) | ||
if self.get("_action") in ("submit", "cancel"): | ||
d.db_set(field, value) | ||
else: | ||
d.set(field, value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a bridge pattern, uncovering — thus far — hidden code smells and performance hits, and will disappear after a few years of using the document read only mode.
Kindly help move this PR forward. Many thanks from Yours Sincerely! |
@frappe.requires_permission("Account", "read") | ||
@frappe.requires_permission("Sales Order", "read") | ||
@frappe.requires_permission("Payment Entry", "create") | ||
def _from_sales_order(self, so): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
where are these methods used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This particular method can be used through their entry point(s) like so:
pe = frappe.new_doc_from("Payment Entry", "Sales Order", "SO-001")
# or
so = frappe.get_doc("Sales Order", "SO-001")
pe = so.into("Payment Entry")
# or
pe = frappe.new_doc("Payment Entry")
pe.flags.some_flag = True
# with either
pe.from_doc("Sales Order", "SO-001")
# or with
so = frappe.get_doc("Sales Order", "SO-001")
pe.from_doc(so)
Note, how this _from_*
method also implements into()
in the opposite direction.
While I did originally consider to mangle these methods __*
so that they would be hard to directly invoke outside of the instance, that approach wasn't particularly forward-compatible with inheritance/overrides so I dropped it again and reverted to content with only a conventionalized _*
private method marker.
It should be made clear from this, docs and initial code precedents that users are supposed to use these only through their entry point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The decorators (@frappe.requires_permission
) enable fine-grained access control at the doctype level, not at the record level, as we can't effectively declare record-level ACL in generic code.
This approach achieves two main objectives:
- It slightly enhances the principle of least privilege compared to using a blanket
self.flags.ignore_permissions
. However, the impact is somewhat limited because we only gain granularity in the permission dimension ("read" vs "write"), while doctype granularity was already implemented at the code level before. - More significantly, it establishes a clear security context for conversion methods, which can be understood as a self-contained and well-defined abstraction. This allows code readers to rely on this declaration (assuming correct implementation). This second point has several important implications and benefits:
- It improves code readability and maintainability by making security assumptions explicit.
- It facilitates easier security audits and reviews.
- It reduces the risk of unintended permission escalation or data exposure.
- It promotes a more modular and secure code structure.
- It enables better documentation * and understanding of the security model for each conversion method. * potentially automatable
- It thereby clarifies the nature and scope of core business logic transitions within the framework.
Ensuring the presence of such decorators throughout the code base (unfortunately without being able to check for semantic correctness), was the goal of this sem-grep rule: frappe/semgrep-rules#28
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Additional note, which triggered this PR series:
Unlike combinations of make_*
methods (even if whitelisted), this now works with server scripts, such as the post payment processing methods of: frappe/frappe#25952
Coincidentally, this is also the reason why this PR is heavy on the Payment Entry doctype which is most often involved in these particular hooks.
Code Example from our Production:
if doc.method == "ecommerce_integrations.ecwid.order.sync_order_from_payment_request":
ps = doc.flags.payment_session
if ps.changed and ps.is_success:
si = frappe.new_doc_from("Sales Invoice", doc.reference_doctype, doc.reference_docname, whitelist_permissions=True)
si.naming_series = "EC-INV-.YYYY.-"
si.flags.ignore_permissions=True
si.insert().submit()
gateway = ps.psl.get_gateway()
pe = frappe.new_doc_from("Payment Entry", si, whitelist_permissions=True)
pe.naming_series = "EC-PAY-.YYYY.-"
pe.paid_amount = ps.state.tx_data.amount
pe.receive_amount = ps.state.tx_data.amount
pe.reference_no = doc.name
pe.bank_account = gateway.payment_account
pe.flags.ignore_permissions=True
pe.insert().submit()
payload = json.loads(doc.request_data)
returnUrl = payload["returnUrl"]
if ps.is_success:
doc.flags.payment_session.result = {
"message": "Pago exitoso",
"action": {
"href": returnUrl,
"label": "Revisar Confirmacion",
"redirect_after_milliseconds": 5000,
},
}
else:
doc.flags.payment_session.result = {
"message": "Pago fallido",
"action": {
"href": returnUrl,
"label": "Volver al Carrito",
"redirect_after_milliseconds": 5000,
},
}
Note the use of whitelist_permissions=True
when escalating into the declared security scope.
This pull request has been automatically marked as inactive because it has not had recent activity. It will be closed within 3 days if no further activity occurs, but it only takes a comment to keep a contribution alive :) Also, even if it is closed, you can always reopen the PR when you're ready. Thank you for contributing. |
Context
This strategy didn't work
Now making things doc methods just for this is probably not the best option, but I couldn't find another sane way to achieve this.
This is probably a design flaw in how whitelisting is implemented in combination with servers scripts.
Non-Solutions include:
frappe.set_user("Administrator")
Maybe there's a more elegant way though, which wouldn't require ultimately an involved refactor to solve this problem at the root?