Skip to content

fix: gracefully handle ephemeral document permissions during GST validation#4167

Open
Aakash1o1 wants to merge 6 commits intoresilient-tech:developfrom
Aakash1o1:fix/ephemeral-doc-permission-develop
Open

fix: gracefully handle ephemeral document permissions during GST validation#4167
Aakash1o1 wants to merge 6 commits intoresilient-tech:developfrom
Aakash1o1:fix/ephemeral-doc-permission-develop

Conversation

@Aakash1o1
Copy link
Copy Markdown

Description

When a user creates a new, unsaved Supplier/Customer/Company and interacts with GST-related fields (e.g., adding a GSTIN), a `DoesNotExistError` is thrown, surfaced to the user as a "Supplier/Customer Not Found" (HTTP 404) error.

Root Cause

`frappe.has_permission(doctype, doc=docname, throw=True)` internally calls `frappe.get_doc(doctype, docname)` when `doc` is a string. For unsaved records, the client passes a temporary ephemeral name (e.g., `new-supplier-1`) which does not exist in the database, causing `frappe.DoesNotExistError` to be raised.

Fix Description

Implements the Permission-First pattern across both affected functions:

  1. Doctype-level permission check first — `has_permission(doctype, doc=None)` with no doc lookup, preventing information disclosure to unauthorized users
  2. Existence guard — `frappe.db.exists()` confirms the record is in the database before any document-level operation. This handles ephemeral UI documents (`new-*`), deleted records, and stale/invalid names (race condition guard)
  3. Document-level permission check — only executed for confirmed real records

Affected Functions

  • `get_gstin_list` in `gst_india/utils/init.py`
  • `make_default_tax_templates` in `gst_india/overrides/company.py`

Steps to Reproduce (Before Fix)

  1. Navigate to Supplier > New
  2. Interact with any GSTIN-related field before saving
  3. Observe `DoesNotExistError` / "Supplier Not Found" error in the UI

Testing

  • Added `test_get_gstin_list_with_ephemeral_document` in `test_utils.py` — asserts `[]` returned for unsaved docname, uses `finally` to restore user context
  • Added `test_make_default_tax_templates_with_ephemeral_company` in `test_company.py` — verifies exact user-friendly error message, uses `finally` to restore user context

…dation

Replaces database existence check with secure string prefix evaluation to prevent
information disclosure while resolving DoesNotExistErrors on unsaved UI documents.

- get_gstin_list: strips ephemeral party name before permission check, returns []
- make_default_tax_templates: throws user-friendly error for unsaved company
Applies Permission-First pattern to both get_gstin_list and make_default_tax_templates:
1. Doctype-level permission check first (doc=None, no DB lookup)
2. startswith('new-') + db.exists() to confirm ephemeral without false positives
3. Full document-level permission check only for confirmed real records

Prevents DoesNotExistError on unsaved UI documents while ensuring real records
named 'new-*' are handled correctly.
Resolved race condition gap, added missing test for tax templates, and improved documentation.

- Replaced startswith('new-') prefix guard with db.exists() covering all missing
  records (ephemeral UI docs, deleted records, stale names)
- Added test_make_default_tax_templates_with_ephemeral_company with try/finally isolation
- Added architectural comment explaining intentional [] return in get_gstin_list
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 2, 2026

Warning

Rate limit exceeded

@Aakash1o1 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 11 minutes and 3 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 11 minutes and 3 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d40820d5-9dfa-43bf-a2ce-bc1ac9fcddf5

📥 Commits

Reviewing files that changed from the base of the PR and between 5324341 and 5009ea2.

📒 Files selected for processing (2)
  • india_compliance/gst_india/overrides/company.py
  • india_compliance/gst_india/utils/test_utils.py
📝 Walkthrough

Walkthrough

The PR adds early doctype-level permission checks and explicit existence guards to GST India functions. make_default_tax_templates() now performs a doctype-level Company permission check (doc=None), verifies the Company exists and raises a translated error for unsaved/missing Companies, then does the document-level permission check inside a try/except to catch frappe.DoesNotExistError. get_gstin_list() now does a doctype-level read permission check before existence lookup, returns an empty list for missing/unsaved records, and wraps the document-level permission check in a try/except to avoid race-condition errors. Integration tests covering these behaviors were added.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: gracefully handle ephemeral document permissions during GST validation' accurately and specifically describes the main change: handling permission checks for unsaved (ephemeral) documents in GST-related functions.
Description check ✅ Passed The description is detailed and directly related to the changeset, explaining the problem, root cause, fix approach, affected functions, reproduction steps, and testing strategy.

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


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.

@codacy-production
Copy link
Copy Markdown

codacy-production bot commented Apr 2, 2026

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 18 complexity

Metric Results
Complexity 18

View in Codacy

TIP This summary will be updated as you push new changes. Give us feedback

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 2, 2026

Confidence Score: 3/5

Safe to merge for the primary ephemeral-name use case, but the residual TOCTOU window at the document-level has_permission call means the original DoesNotExistError can still appear in rare race-condition scenarios.

The fix correctly handles the common case (ephemeral document names) and is a clear improvement over the status quo. However, the document-level has_permission call in both functions still receives a plain string name and Frappe will attempt to fetch the document internally, meaning a deletion race can reproduce the exact error the PR is meant to prevent. This is a low-probability but real gap that warrants a follow-up fix before merging.

india_compliance/gst_india/utils/init.py (line 116) and india_compliance/gst_india/overrides/company.py (line 78) — the document-level has_permission calls that retain the TOCTOU window.

Important Files Changed

Filename Overview
india_compliance/gst_india/utils/init.py Adds a three-step permission/existence guard to get_gstin_list: doctype-level check, db.exists early-return [], then document-level check. Correct approach for the ephemeral-name bug; minor TOCTOU window remains at the document-level has_permission call.
india_compliance/gst_india/overrides/company.py Same three-step pattern added to make_default_tax_templates; throws a user-friendly ValidationError for ephemeral/missing companies. Same residual TOCTOU concern at the document-level has_permission call.
india_compliance/gst_india/utils/test_utils.py New test asserts [] is returned for an ephemeral supplier name using Administrator context; correctly restores session user in a finally block. Only covers the Administrator path.
india_compliance/gst_india/overrides/test_company.py New test asserts ValidationError with a specific message for an ephemeral company name; uses assertRaisesRegex correctly and restores user in a finally block.

Reviews (1): Last reviewed commit: "fix: address AI review feedback for PR #..." | Re-trigger Greptile

Comment on lines +115 to +116
# 3. Record confirmed to exist — run full document-level permission check
frappe.has_permission(party_type, ptype="read", doc=party, throw=True)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Residual TOCTOU window in document-level permission check

The db.exists() guard in step 2 closes the ephemeral-name case, but the document-level has_permission call on line 116 still passes doc=party as a string. Frappe resolves a string doc by calling frappe.get_doc(doctype, name) internally, so if the record is deleted between the db.exists() check and this call (a race condition), the original DoesNotExistError can still surface — the very error this PR is intended to prevent.

For get_gstin_list the 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 catching frappe.DoesNotExistError:

    # 3. Record confirmed to exist — run full document-level permission check
    try:
        frappe.has_permission(party_type, ptype="read", doc=party, throw=True)
    except frappe.DoesNotExistError:
        return []

The same pattern applies to make_default_tax_templates in company.py (line 78).

Comment on lines +50 to +60
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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Test only exercises the Administrator path

The test sets the user to "Administrator" before calling get_gstin_list. In Frappe, Administrator bypasses most row-level permission checks, so this test does not verify the behaviour for a regular user who has doctype-level read permission but no access to a specific Supplier document. It is worth adding a case with a guest or restricted user to confirm that the doctype-level check (step 1) still fires and raises PermissionError when the user has no access, ensuring the early-return of [] does not silently bypass authorization for non-admin users.

Comment on lines +103 to +116
# 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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Verbose inline comments add noise to production code

The numbered step headings and multi-line rationale blocks (e.g. "Returning [] is an intentional architectural choice: the frontend calls this function in real-time…", the Case A / Case B explanations) are thorough design documentation, but they are more appropriate in the PR description or a contributing guide than in production source code. Consider condensing to a brief single-line comment per step:

    # Doctype-level permission check (no doc lookup)
    frappe.has_permission(party_type, ptype="read", doc=None, throw=True)

    # Return early for ephemeral/deleted documents instead of raising
    if not frappe.db.exists(party_type, party):
        return []

    frappe.has_permission(party_type, ptype="read", doc=party, throw=True)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Adds test_get_gstin_list_raises_permission_error_for_restricted_user to verify
that users without doctype-level access receive PermissionError, confirming the
Permission-First pattern blocks unauthorized access at the doctype check before
any database lookup occurs.
Wraps document-level has_permission() in try/except DoesNotExistError to handle
the window between the db.exists() check and the internal get_doc() call inside
has_permission, where a record could be deleted by a concurrent user.
- Fix TOCTOU error message in make_default_tax_templates to distinguish between
  unsaved documents and records deleted mid-request
- Add tearDownClass to TestUtils to roll back global state after tests
- Add positive test using upstream test fixture (_Test Registered Supplier)
- Add multi-role permission boundary test for make_default_tax_templates
- Remove site-specific Supplier creation from test setup (not portable to upstream CI)
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.

2 participants