-
Notifications
You must be signed in to change notification settings - Fork 230
fix: gracefully handle ephemeral document permissions during GST validation #4167
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
base: develop
Are you sure you want to change the base?
Changes from 3 commits
7fcf787
066e9b4
b874a34
1b53488
5324341
5009ea2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -100,7 +100,20 @@ def get_gstin_list(party: str, party_type: str = "Company", exclude_isd: bool = | |
| """ | ||
| Returns a list the party's GSTINs. | ||
| """ | ||
| frappe.has_permission(party_type, doc=party, throw=True) | ||
| # 1. Doctype-level permission check first (no doc lookup, safe from disclosure) | ||
| frappe.has_permission(party_type, ptype="read", doc=None, throw=True) | ||
|
|
||
| # 2. Gracefully handle unsaved or missing documents. | ||
| # Returning [] is an intentional architectural choice: the frontend calls this | ||
| # function in real-time as the user types, including before the document is saved. | ||
| # An empty list signals "no GSTINs yet" without blocking the UI or leaking existence info. | ||
| # Case A: ephemeral UI name (new-*) that isn't a real saved record | ||
| # Case B: record deleted by another user or a stale/invalid name (race condition guard) | ||
| if not frappe.db.exists(party_type, party): | ||
| return [] | ||
|
|
||
| # 3. Record confirmed to exist — run full document-level permission check | ||
| frappe.has_permission(party_type, ptype="read", doc=party, throw=True) | ||
|
||
|
|
||
| filters = { | ||
| "link_doctype": party_type, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -46,3 +46,15 @@ def test_timespan_date_range(self, getdate_mock): | |
|
|
||
| for i, expected_date in enumerate(expected_date_range): | ||
| self.assertEqual(expected_date, actual_date_range[i]) | ||
|
|
||
| def test_get_gstin_list_with_ephemeral_document(self): | ||
| """get_gstin_list should return empty list for unsaved (ephemeral) documents.""" | ||
| from india_compliance.gst_india.utils import get_gstin_list | ||
|
|
||
| original_user = frappe.session.user | ||
| try: | ||
| frappe.set_user("Administrator") | ||
| result = get_gstin_list("new-supplier-1", "Supplier") | ||
| self.assertEqual(result, []) | ||
| finally: | ||
| frappe.set_user(original_user) | ||
|
Comment on lines
+81
to
+91
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The test sets the user to |
||
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
db.exists()guard in step 2 closes the ephemeral-name case, but the document-levelhas_permissioncall on line 116 still passesdoc=partyas a string. Frappe resolves a stringdocby callingfrappe.get_doc(doctype, name)internally, so if the record is deleted between thedb.exists()check and this call (a race condition), the originalDoesNotExistErrorcan still surface — the very error this PR is intended to prevent.For
get_gstin_listthe impact is low (the subsequent DB queries would simply return no rows anyway), but the error would still propagate as an unhandled 404. Consider wrapping the step-3 check in a guard, or catchingfrappe.DoesNotExistError:The same pattern applies to
make_default_tax_templatesincompany.py(line 78).