fix(appointment): verify portal booking without switching user

Verification ran the save as Administrator through frappe.set_user, which
semgrep flags. Only the agent assignment needed more than the Guest has:
assign_to.add checks read permission on the appointment. Assign with
ignore_permissions instead; every other insert or save in the chain
already passes ignore_permissions. The visitor's session is no longer
touched at all.

(cherry picked from commit 3773a8e482)

# Conflicts:
#	erpnext/crm/doctype/appointment/appointment.py
This commit is contained in:
Nabin Hait
2026-10-22 09:33:49 +00:00
committed by Mergify
parent 758a5694de
commit c469dfd95f
3 changed files with 20 additions and 17 deletions
@@ -8,7 +8,12 @@ from urllib.parse import urlencode
import frappe
from frappe import _
<<<<<<< HEAD
from frappe.desk.form.assign_to import add as add_assignment
=======
from frappe.desk.form.assign_to import _add as add_assignment
from frappe.desk.form.assign_to import clear as clear_assignments
>>>>>>> 3773a8e (fix(appointment): verify portal booking without switching user)
from frappe.model.document import Document
from frappe.share import add_docshare
from frappe.utils import add_to_date, cint, date_diff, get_datetime, get_url, getdate, now, now_datetime
@@ -312,7 +317,10 @@ class Appointment(Document):
if not frappe.has_permission(doc=self, user=agent):
add_docshare(self.doctype, self.name, agent, flags={"ignore_share_permission": True})
add_assignment({"doctype": self.doctype, "name": self.name, "assign_to": [agent]})
# the assigning user may be the Guest verifying a portal booking
add_assignment(
{"doctype": self.doctype, "name": self.name, "assign_to": [agent]}, ignore_permissions=True
)
def create_calendar_event(self):
if self.calendar_event:
@@ -213,12 +213,12 @@ class TestAppointment(FrappeTestCase):
stored = frappe.db.get_value("Appointment", appointment.name, "verification_token")
self.assertEqual(stored, sha256_hash(params["key"]))
def test_verification_keeps_the_visitor_session(self):
def test_guest_verification_keeps_the_visitor_session(self):
appointment = self._create_portal_appointment("portal_visitor_session@example.com")
_parsed, params = parse_verify_url(appointment._get_verify_url())
self.addCleanup(frappe.set_user, frappe.session.user)
frappe.set_user("test@example.com")
frappe.set_user("Guest")
frappe.local.session.data = frappe._dict(csrf_token="token")
session = frappe.local.session
with patch.object(Appointment, "send_appointment_confirmed_email"):
@@ -228,7 +228,11 @@ class TestAppointment(FrappeTestCase):
self.assertTrue(context.success)
self.assertIs(frappe.local.session, session)
self.assertEqual((session.user, session.data.csrf_token), ("test@example.com", "token"))
self.assertEqual((session.user, session.data.csrf_token), ("Guest", "token"))
appointment.reload()
self.assertEqual(appointment.status, "Open")
self.assertTrue(appointment.party and appointment.calendar_event)
self.assertEqual(get_assignees(appointment.name), ["Administrator"])
def test_verify_url_ignores_the_request_host(self):
appointment = self._create_portal_appointment("portal_visitor_host@example.com")
+4 -13
View File
@@ -46,16 +46,7 @@ def get_context(context):
def verify_appointment(appointment):
# the signed link is the authorization; materializing the appointment
# (agent assignment) needs system privileges the Guest visitor lacks
# switch on a detached session so the visitor's own session data is left untouched
session = frappe.local.session
frappe.local.session = frappe._dict()
try:
frappe.set_user("Administrator")
appointment.email_verified = True
appointment.status = "Open"
appointment.save(ignore_permissions=True)
finally:
frappe.set_user(session.user)
frappe.local.session = session
# the signed link is the authorization for the Guest visitor
appointment.email_verified = True
appointment.status = "Open"
appointment.save(ignore_permissions=True)