Skip to content

fix!: field-level permissions for agent-only ticket fields - #3544

Open
aerodeval wants to merge 82 commits into
frappe:developfrom
aerodeval:fix/ticket-field-level-perms
Open

aerodeval wants to merge 82 commits into
frappe:developfrom
aerodeval:fix/ticket-field-level-perms

Conversation

@aerodeval

@aerodeval aerodeval commented Jul 13, 2026

Copy link
Copy Markdown
Member

Problem

From the portal a customer could set agent-only fields on their own ticket — priority, sla, response_by — with frappe.client.set_value, and fake raised_by, contact or customer at creation.

Fix

  • Reading — permission levels on HD Ticket: 7 customer-readable, 8 agent-only. Frappe enforces them on every path, /api/resource included. 1–6 stay free for a site's own scheme.
  • Editingprevent_customer_edits() in validate. Levels can't cover this: a customer needs level-0 write just to create a ticket. They keep feedback, feedback_extra and closing the ticket.
  • Per-site hiding — each Default-template row carries a Visible to select (Everyone / Customers / Agents) that hides the field on helpdesk's own pages. It only narrows what the level already allows, never widens.
Level Fields Customers Agents
0 subject, status, description, template, attachment, content_type, feedback, feedback_extra, feedback_rating fill on create, then only status (to close) and feedback edit
7 raised_by, contact, customer, priority, ticket_type, agent_group, sla, status_category, response_by, resolution_by, on_hold_since, first_response_time, first_responded_on, first_response_failed_by, resolution_date, resolution_time, resolution_failed_by read; fill on create if the template shows it edit
8 key, summary, resolution_details, agreement_status, service_level_agreement_creation, opening_date, opening_time, avg_response_time, total_hold_time, user_resolution_time, raised_outside_working_hours, last_agent_response, last_customer_response, email_account, via_customer_portal, is_merged, merged_with, ticket_split_from no access edit

Also fixes the fallout of hiding fields: portal reply no longer reopening a ticket, the closing-email rating link, 500 on tickets with no contact, merge losing is_merged, blank list columns and filters that raised PermissionError, Default template rename, and an agent's whole-document save blanking hidden fields.

Breaking changes

  • Customers can no longer edit ticket fields after creation (closing and feedback still work).
  • hide_from_customer is replaced by Visible to; a patch carries old values over. Update anything reading the flag.
  • Hiding a template field hides it on helpdesk pages only — raise its level in Customize Form to hide it from the REST API too.
  • A non-agent creating a ticket — including a System Manager with no HD Agent record — now gets raised_by forced to their own address and via_customer_portal set. System Manager keeps full field access through the new level-7/8 perm rows.

Existing sites

harden_hd_ticket_field_permissions writes the level-7/8 perm rows (a single Custom DocPerm row makes Frappe ignore shipped perms, so a customised site would read every protected field as empty — agents too). backfill_template_field_visible_to moves old hide_from_customer values across. Both idempotent, neither writes a field's level.

Testing

118 tests in test_hd_ticket.py, 20 in test_field_visibility.py, 2 in test_hd_email_feedback.py.

@codecov-commenter

codecov-commenter commented Jul 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.22888% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.91%. Comparing base (c3ae467) to head (428f0b3).
⚠️ Report is 178 commits behind head on develop.

Files with missing lines Patch % Lines
helpdesk/api/doc.py 68.42% 6 Missing ⚠️
helpdesk/test_utils.py 88.88% 3 Missing ⚠️
helpdesk/helpdesk/doctype/hd_ticket/api.py 95.45% 1 Missing ⚠️
helpdesk/helpdesk/doctype/hd_ticket/hd_ticket.py 97.82% 1 Missing ⚠️
...pdesk/helpdesk/doctype/hd_ticket/test_hd_ticket.py 99.68% 1 Missing ⚠️
...k/doctype/hd_ticket_template/hd_ticket_template.py 97.87% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #3544      +/-   ##
===========================================
+ Coverage    73.76%   77.91%   +4.15%     
===========================================
  Files          148      156       +8     
  Lines        10841    12109    +1268     
===========================================
+ Hits          7997     9435    +1438     
+ Misses        2844     2674     -170     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@aerodeval
aerodeval force-pushed the fix/ticket-field-level-perms branch from 1cd0a80 to 7c1bf5e Compare July 24, 2026 10:19
@aerodeval
aerodeval force-pushed the fix/ticket-field-level-perms branch from 7c1bf5e to 98c998b Compare July 24, 2026 10:52
@aerodeval
aerodeval marked this pull request as ready for review August 10, 2026 08:07
@mergify

mergify Bot commented Aug 10, 2026

Copy link
Copy Markdown

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

@greptile-apps

greptile-apps Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 4/5

The repository query requirement must be satisfied before merging.

Reviews (41) · Last reviewed commit: "fix(perms): drop the per-user cache on g..."

Comment thread helpdesk/helpdesk/doctype/hd_ticket_template/hd_ticket_template.py Outdated
Comment thread helpdesk/helpdesk/doctype/hd_ticket_template/hd_ticket_template.py Outdated
Comment thread helpdesk/helpdesk/doctype/hd_ticket/test_hd_ticket.py
A stale base could undo a restriction the admin set while the field
was listed. Removal now restores the base only when that raises the
level, so exposing a removed field always takes Customize Form.
@aerodeval
aerodeval marked this pull request as draft August 28, 2026 14:22
The template refuses to show key at any permission level, since it
authenticates guest feedback links. The migration also lifts shown
custom fields below the visible level to 7, still never lowering.
@aerodeval
aerodeval marked this pull request as ready for review August 29, 2026 20:07
@aerodeval
aerodeval marked this pull request as draft September 1, 2026 10:51
The Default template used to write custom-field permission levels directly,
which raced with Customize Form and needed a base_permlevel column to undo it.
Permission levels now stay a static security floor; the template's visible_to
tier only narrows what each role sees on helpdesk pages, enforced by the
HDTicket read override and the app's own endpoints. Agent-only fields are
dropped from hardcoded portal rows via a _hidden_fields list, and a cosmetic
patch backfills visible_to on rows that predate the column.
@aerodeval
aerodeval marked this pull request as ready for review September 4, 2026 19:05
creation_fillable_template_fields keyed on hide_from_customer, the flag
synced from visible_to. Read the tier directly so the write path uses the
same source of truth as the rest of the field-visibility code.
@aerodeval aerodeval changed the title fix: field-level permissions for agent-only ticket fields fix!: field-level permissions for agent-only ticket fields Sep 8, 2026
aerodeval and others added 9 commits September 8, 2026 15:26
- prune permlevel-hidden fields in hidden_ticket_fields, so every caller
  drops both sets and not just the template tier
- keep the contact card when raised_by is hidden
- let a portal reply reopen a replied or resolved ticket
- let the closing email's rating reach the closed ticket
- keep the merge link when the agent has no write at the internal level
- stop treating System Manager as an agent
- refuse renaming the Default template

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JwkkywUhXWkgjQuf5ad9XV
Custom DocPerm rows make Frappe ignore the JSON perms, so these sites
never get the new permlevel grants and read the protected fields as empty.

- write the level 7 and 8 rows for roles that can already read a ticket
- add the customer roles, which a June 2026 JSON-only change never gave them
- hide template rows the validator would refuse to save after the backfill

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JwkkywUhXWkgjQuf5ad9XV
Comment thread desk/src/components/ticket/TicketCustomerSidebar.vue Outdated
Comment thread desk/src/pages/ticket/TicketCustomerTemplateFields.vue Outdated
Comment thread desk/src/types.ts Outdated
Comment thread helpdesk/api/doc.py
Comment thread helpdesk/helpdesk/doctype/hd_ticket/hd_ticket.py Outdated
Comment thread desk/src/pages/ticket/TicketNew.vue Outdated
Comment thread helpdesk/helpdesk/doctype/hd_ticket_template/hd_ticket_template.py
aerodeval and others added 9 commits September 16, 2026 17:51
get_fields_meta already strips agent-only rows for customers, so the three
portal filters never removed anything. Visibility now has one owner on the
server and the payload no longer carries visible_to.
The warning tells an admin to raise a permission level but not what that
means. The link opens in a new tab so an unsaved template is not lost.
creation_fillable_template_fields -> customer_fillable_template_fields,
customer_may_fill_at_creation -> can_customer_fill_at_creation, and the
permlevel tuple they read to match.
tearDown runs before every addCleanup, so the deletions no longer need a
wrapper that switches back to Administrator first.
`hidden_ticket_fields`, `unreadable_fields` and `fields_visible_to` were the
only noun-named fetchers beside seventy-odd `get_` ones, including the
framework's `get_permitted_fields` they wrap. The docstring on
`get_hidden_ticket_fields` restated the new name, so it states the contract
instead.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UGwyVdR1M2WGd9bex8TxRR
`get_list_data` looked the hidden set up twice and filtered `rows` twice:
once before the query, and again after the standard fields loop put
`_assign` back. Compute it once, keep hidden standard fields out of that
loop, and drop the `fields` cleanup pass. The set is `hidden_fields` at
every site now; `hidden` named an adjective.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UGwyVdR1M2WGd9bex8TxRR
The result became role-dependent, so the cache went `user=True`. That
splits one entry into one per user, and `clear_cache()` only reaches the
calling user's key, so a Visible to change left everyone else stale for an
hour. The frontend fetches this once per session under frappe-ui's own
cache, so nothing is lost.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UGwyVdR1M2WGd9bex8TxRR
Comment on lines +54 to +61
frappe.get_all(
"HD Ticket Template Field",
pluck="fieldname",
filters={
"parent": DEFAULT_TICKET_TEMPLATE,
"parenttype": "HD Ticket Template",
"visible_to": audience,
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Permission-Aware Queries Required

The new visibility lookup uses frappe.get_all(), as does customer_fillable_template_fields() in hd_ticket.py. This violates the repository directive to prefer permission-aware frappe.get_list() for new code, so the requirement must be satisfied before merging.

Context Used: AGENTS.md (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: helpdesk/field_visibility.py
Line: 54-61

Comment:
**Permission-Aware Queries Required**

The new visibility lookup uses `frappe.get_all()`, as does `customer_fillable_template_fields()` in `hd_ticket.py`. This violates the repository directive to prefer permission-aware `frappe.get_list()` for new code, so the requirement must be satisfied before merging.

**Context Used:** AGENTS.md ([source](https://github.com/frappe/helpdesk/blob/develop/AGENTS.md))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

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!

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.

3 participants