Add option to restrict anonymous access to order URLs (#4735)

* Add option to restrict anonymous access to order URLs

By default, users who place orders while logged in can still access
their order URLs without authentication. This raises potential
security risks, particularly if order confirmation emails are
forwarded.

This commit introduces an organiser-level setting to disable anonymous
access for such orders. When enabled, unauthenticated attempts to access
URLs starting with `/order/`, which are intended for the customer, are
redirected to the login page. Upon successful authentication, the user
is redirected back to the original order URL.

It is important to note that this change does not impact routes intended
for attendees (e.g., `/ticket/*`), which remain accessible without
authentication.

* Change name of setting for future clarity

Co-authored-by: Raphael Michel <mail@raphaelmichel.de>

* Update message wording

Co-authored-by: Raphael Michel <mail@raphaelmichel.de>

* Eliminate database query

Co-authored-by: Raphael Michel <mail@raphaelmichel.de>

* Rename feature flag to fix breaking tests

* Refactor order access verification code into `OrderDetailsMixin`

* Add test for logged-in customer accessing another customer's order

* Refactor order access conditions to remove nesting

* Handle case where customer is not yet verified

* Add additional information to help message

* Fix multidomain issue

Co-authored-by: Raphael Michel <mail@raphaelmichel.de>

* Merge order/position variants into single tests

* Add docstring explaining return type of `order` property

* Apply suggestion from @raphaelm

* Fix indentation

---------

Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
Co-authored-by: Raphael Michel <michel@rami.io>
This commit is contained in:
Kian Cross
2026-01-16 12:46:08 +00:00
committed by GitHub
parent 1e0e16642d
commit 0fc2d6134f
7 changed files with 329 additions and 31 deletions

View File

@@ -2611,3 +2611,11 @@ def test_approve_cancellation_request(client, env):
doc = BeautifulSoup(response.content.decode(), "lxml")
assert doc.select('input[name=refund-new-giftcard]')[0]['value'] == '10.00'
assert not env[2].cancellation_requests.exists()
@pytest.mark.django_db
def test_view_as_user(client, env):
client.login(email='dummy@dummy.dummy', password='dummy')
response = client.get('/%s/%s/order/%s/%s/' % (env[0].organizer.slug, env[0].slug, env[2].code, env[2].secret))
assert response.status_code == 200
assert env[2].code in response.content.decode()

View File

@@ -1738,3 +1738,222 @@ class OrdersTest(BaseOrdersTest):
self.order.secret, a.pk, match.group(1))
)
assert response.status_code == 404
def test_require_login_for_order_access_disabled_unauth(self):
self.orga.settings.customer_accounts = True
with scopes_disabled():
customer = self.orga.customers.create(email='john@example.org', is_verified=True)
customer.set_password("foo")
customer.save()
self.order.customer = customer
self.order.save()
self.orga.settings.customer_accounts_require_login_for_order_access = False
response = self.client.get(
'/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)
)
assert response.status_code == 200
doc = BeautifulSoup(response.content.decode(), "lxml")
assert len(doc.select(".cart-row")) > 0
assert "pending" in doc.select(".order-details")[0].text.lower()
assert "Peter" in response.content.decode()
assert "Lukas" not in response.content.decode()
response = self.client.get(
'/%s/%s/ticket/%s/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code,
self.ticket_pos.positionid, self.ticket_pos.web_secret)
)
assert response.status_code == 200
doc = BeautifulSoup(response.content.decode(), "lxml")
assert len(doc.select(".cart-row")) > 0
assert "pending" in doc.select(".order-details")[0].text.lower()
assert "Peter" in response.content.decode()
assert "Lukas" not in response.content.decode()
def test_require_login_for_order_access_enabled_unauth(self):
self.orga.settings.customer_accounts = True
with scopes_disabled():
customer = self.orga.customers.create(email='john@example.org', is_verified=True)
customer.set_password("foo")
customer.save()
self.order.customer = customer
self.order.save()
self.orga.settings.customer_accounts_require_login_for_order_access = True
response = self.client.get(
'/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)
)
assert response.status_code == 302
response = self.client.get(
'/%s/%s/ticket/%s/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code,
self.ticket_pos.positionid, self.ticket_pos.web_secret)
)
assert response.status_code == 200
doc = BeautifulSoup(response.content.decode(), "lxml")
assert len(doc.select(".cart-row")) > 0
assert "pending" in doc.select(".order-details")[0].text.lower()
assert "Peter" in response.content.decode()
assert "Lukas" not in response.content.decode()
def test_require_login_for_order_access_disabled_auth(self):
self.orga.settings.customer_accounts = True
with scopes_disabled():
customer = self.orga.customers.create(email='john@example.org', is_verified=True)
customer.set_password("foo")
customer.save()
self.order.customer = customer
self.order.save()
self.orga.settings.customer_accounts_require_login_for_order_access = False
response = self.client.post('/%s/account/login' % (self.orga.slug), {
'email': 'john@example.org',
'password': 'foo',
})
assert response.status_code == 302
response = self.client.get(
'/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)
)
assert response.status_code == 200
doc = BeautifulSoup(response.content.decode(), "lxml")
assert len(doc.select(".cart-row")) > 0
assert "pending" in doc.select(".order-details")[0].text.lower()
assert "Peter" in response.content.decode()
assert "Lukas" not in response.content.decode()
response = self.client.get(
'/%s/%s/ticket/%s/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code,
self.ticket_pos.positionid, self.ticket_pos.web_secret)
)
assert response.status_code == 200
doc = BeautifulSoup(response.content.decode(), "lxml")
assert len(doc.select(".cart-row")) > 0
assert "pending" in doc.select(".order-details")[0].text.lower()
assert "Peter" in response.content.decode()
assert "Lukas" not in response.content.decode()
def test_require_login_for_order_access_enabled_auth(self):
self.orga.settings.customer_accounts = True
with scopes_disabled():
customer = self.orga.customers.create(email='john@example.org', is_verified=True)
customer.set_password("foo")
customer.save()
self.order.customer = customer
self.order.save()
self.orga.settings.customer_accounts_require_login_for_order_access = True
response = self.client.post('/%s/account/login' % (self.orga.slug), {
'email': 'john@example.org',
'password': 'foo',
})
assert response.status_code == 302
response = self.client.get(
'/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)
)
assert response.status_code == 200
doc = BeautifulSoup(response.content.decode(), "lxml")
assert len(doc.select(".cart-row")) > 0
assert "pending" in doc.select(".order-details")[0].text.lower()
assert "Peter" in response.content.decode()
assert "Lukas" not in response.content.decode()
response = self.client.get(
'/%s/%s/ticket/%s/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code,
self.ticket_pos.positionid, self.ticket_pos.web_secret)
)
assert response.status_code == 200
doc = BeautifulSoup(response.content.decode(), "lxml")
assert len(doc.select(".cart-row")) > 0
assert "pending" in doc.select(".order-details")[0].text.lower()
assert "Peter" in response.content.decode()
assert "Lukas" not in response.content.decode()
def test_require_login_for_order_access_accounts_disabled(self):
self.orga.settings.customer_accounts = True
with scopes_disabled():
customer = self.orga.customers.create(email='john@example.org', is_verified=True)
customer.set_password("foo")
customer.save()
self.order.customer = customer
self.order.save()
self.orga.settings.customer_accounts = False
self.orga.settings.customer_accounts_require_login_for_order_access = True
response = self.client.get(
'/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)
)
assert response.status_code == 200
doc = BeautifulSoup(response.content.decode(), "lxml")
assert len(doc.select(".cart-row")) > 0
assert "pending" in doc.select(".order-details")[0].text.lower()
assert "Peter" in response.content.decode()
assert "Lukas" not in response.content.decode()
def test_require_login_for_order_access_enabled_wrong_customer(self):
self.orga.settings.customer_accounts = True
with scopes_disabled():
customer = self.orga.customers.create(email='john@example.org', is_verified=True)
customer.set_password("foo")
customer.save()
self.order.customer = customer
self.order.save()
with scopes_disabled():
customer = self.orga.customers.create(email='jill@example.org', is_verified=True)
customer.set_password("bar")
customer.save()
self.orga.settings.customer_accounts_require_login_for_order_access = True
response = self.client.post('/%s/account/login' % (self.orga.slug), {
'email': 'jill@example.org',
'password': 'bar',
})
assert response.status_code == 302
response = self.client.get(
'/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)
)
assert response.status_code == 404
def test_require_login_for_order_access_enabled_unverified_account(self):
self.orga.settings.customer_accounts = True
with scopes_disabled():
customer = self.orga.customers.create(email='john@example.org', is_verified=False)
customer.set_password("foo")
customer.save()
self.order.customer = customer
self.order.save()
self.orga.settings.customer_accounts_require_login_for_order_access = True
response = self.client.get(
'/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)
)
assert response.status_code == 200
doc = BeautifulSoup(response.content.decode(), "lxml")
assert len(doc.select(".cart-row")) > 0
assert "pending" in doc.select(".order-details")[0].text.lower()
assert "Peter" in response.content.decode()
assert "Lukas" not in response.content.decode()