Compare commits

..
Author SHA1 Message Date
Raphael Michel 96a4b3d7f6 Navigation and dashboard: Do not crash page if a plugin fails
Generally, I don't think it's a good approach for plugins to fail
silently. A missing menu item or button is a weird failure mode, people
will search for it and the system will feel buggy. However, a single bug
in a plugin should not cause the entire system to feel "down" since the
entire backend can't load, not even the dashboard right after login. So
here, I think the trade-off is warranted to fail silently -- and
escalate quickly via Sentry.
2026-11-03 17:12:23 +01:00
Raphael Michel 0a73faa8bd Bump to i18nfield 1.12.0 2026-11-03 10:38:37 +01:00
Tobias Kunze def5042661 Ignore spaces in bank transfer queries (#6724)
When copying an order code from e.g. the order list, it's easy to
include spaces when pasting, and then this does not match anything. And
as the JavaScript side of the search *does* compare the stripped string
(normalizeQuery), just deleting the whitespace does not trigger the
order to be found either, you have delete one more letter.
2026-11-03 09:12:38 +01:00
Richard Schreiber e6f8f178af Extend log_action filter and make recursive (Z#23247315) (#6598)
* Extend log_action filter and make recursive

* fix flake8

* fix type mismatch

* add "salt" to sensitive keys

* match "key" (e.g. see payone)

* add "auth" to sensitive keys

* clean lists recursively

* improve clean method name as it also cleans list recursively

* Add test
2026-10-29 10:52:00 +01:00
Raphael Michel 5f5380e8b1 Bump version to 2026.10.0.dev0 2026-10-29 09:59:11 +01:00
8 changed files with 145 additions and 16 deletions

No files matched your search

+1 -1
View File
@@ -46,7 +46,7 @@ dependencies = [
"django-formtools==2.7",
"django-hierarkey==2.0.*,>=2.0.2",
"django-hijack==3.7.*",
"django-i18nfield==1.11.*",
"django-i18nfield==1.12.*",
"django-libsass==0.9",
"django-localflavor==5.1",
"django-markup",
+1 -1
View File
@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
__version__ = "2026.9.0"
__version__ = "2026.10.0.dev0"
+21 -4
View File
@@ -151,12 +151,29 @@ class LoggingMixin:
logentry = LogEntry(content_object=self, user=user, action_type=action, event=event,
organizer_id=organizer_id, **kwargs)
if isinstance(data, dict):
sensitivekeys = ['password', 'secret', 'api_key']
# sensitivekeys are matched whether contained, not exactly
sensitivekeys = [
'auth',
'key',
'token',
'password',
'Password',
'_pass',
'salt',
'secret',
]
for sensitivekey in sensitivekeys:
for k, v in data.items():
if (sensitivekey in k) and v:
def clean_recursive(d):
data = d
data_iterator = d.items() if isinstance(d, dict) else enumerate(d)
for k, v in data_iterator:
if v and any([sensitivekey in str(k) for sensitivekey in sensitivekeys]):
data[k] = "********"
elif isinstance(v, (list, dict)):
data[k] = clean_recursive(v)
return data
data = clean_recursive(data)
logentry.data = json.dumps(data, cls=CustomJSONEncoder, sort_keys=True)
elif data:
+38 -3
View File
@@ -147,7 +147,7 @@ class PluginSignal(Generic[T], django.dispatch.Signal):
def _is_receiver_active(self, sender, receiver):
return is_receiver_active(sender, receiver)
def send(self, sender: T, **named) -> List[Tuple[Callable, Any]]:
def send(self, sender: T, skip_exceptions=False, **named) -> List[Tuple[Callable, Any]]:
"""
Send signal from sender to all connected receivers that belong to
plugins enabled for the given event / organizer.
@@ -164,8 +164,19 @@ class PluginSignal(Generic[T], django.dispatch.Signal):
for receiver in self._live_receivers(sender)[0]:
if self._is_receiver_active(sender, receiver):
response = receiver(signal=self, sender=sender, **named)
responses.append((receiver, response))
try:
response = receiver(signal=self, sender=sender, **named)
except Exception as e:
if skip_exceptions:
logger.exception("Signal handler skipped due to exception")
if settings.SENTRY_ENABLED:
import sentry_sdk
sentry_sdk.capture_exception(e)
else:
raise
else:
responses.append((receiver, response))
return responses
def send_chained(self, sender: T, chain_kwarg_name, **named) -> List[Tuple[Callable, Any]]:
@@ -312,6 +323,30 @@ class OrganizerPluginSignal(PluginSignal[Organizer]):
class GlobalSignal(django.dispatch.Signal):
def send(self, sender, skip_exceptions=False, **named):
if (
not self.receivers
or self.sender_receivers_cache.get(sender) is NO_RECEIVERS
):
return []
responses = []
sync_receivers, _ = self._live_receivers(sender)
for receiver in sync_receivers:
try:
response = receiver(signal=self, sender=sender, **named)
except Exception as e:
if skip_exceptions:
logger.exception("Signal handler skipped due to exception")
if settings.SENTRY_ENABLED:
import sentry_sdk
sentry_sdk.capture_exception(e)
else:
raise
else:
responses.append((receiver, response))
return responses
def send_chained(self, sender: Event, chain_kwarg_name, **named) -> List[Tuple[Callable, Any]]:
"""
Send signal from sender to all connected receivers. The return value of the first receiver
+3 -3
View File
@@ -135,7 +135,7 @@ def get_event_navigation(request: HttpRequest):
# It would be better to allow plugins to handle the permission themselves, but for backwards compatibility
# we need to have it in the "if" statement
event_settings += sorted(
sum((list(a[1]) for a in nav_event_settings.send(request.event, request=request)), []),
sum((list(a[1]) for a in nav_event_settings.send(request.event, skip_exceptions=True, request=request)), []),
key=lambda r: r['label']
)
if event_settings:
@@ -487,7 +487,7 @@ def get_global_navigation(request):
})
merge_in(nav, sorted(
sum((list(a[1]) for a in nav_global.send(request, request=request)), []),
sum((list(a[1]) for a in nav_global.send(request, skip_exceptions=True, request=request)), []),
key=lambda r: (1 if r.get('parent') else 0, r['label'])
))
return nav
@@ -718,7 +718,7 @@ def get_organizer_navigation(request):
})
merge_in(nav, sorted(
sum((list(a[1]) for a in nav_organizer.send(request.organizer, request=request, organizer=request.organizer)),
sum((list(a[1]) for a in nav_organizer.send(request.organizer, skip_exceptions=True, request=request, organizer=request.organizer)),
[]),
key=lambda r: (1 if r.get('parent') else 0, r['label'])
))
+3 -3
View File
@@ -370,7 +370,7 @@ def event_index(request, organizer, event):
widgets = []
if can_view_orders:
for r, result in event_dashboard_widgets.send(sender=request.event, subevent=subevent, lazy=True):
for r, result in event_dashboard_widgets.send(sender=request.event, subevent=subevent, lazy=True, skip_exceptions=True):
widgets.extend(result)
ctx = {
@@ -406,7 +406,7 @@ def event_index_widgets_lazy(request, organizer, event):
request=request)
widgets = []
if can_view_orders:
for r, result in event_dashboard_widgets.send(sender=request.event, subevent=subevent, lazy=False):
for r, result in event_dashboard_widgets.send(sender=request.event, subevent=subevent, lazy=False, skip_exceptions=True):
widgets.extend(result)
return build_json_response(widgets)
@@ -666,7 +666,7 @@ def user_index_widgets_lazy(request):
def user_index(request):
widgets = []
for r, result in user_dashboard_widgets.send(request, user=request.user):
for r, result in user_dashboard_widgets.send(request, skip_exceptions=True, user=request.user):
widgets.extend(result)
ctx = {
+1 -1
View File
@@ -239,7 +239,7 @@ class ActionView(View):
})
def get(self, request, *args, **kwargs):
u = request.GET.get('query', '')
u = request.GET.get('query', '').strip()
if len(u) < 2:
return JsonResponse({'results': []})
+77
View File
@@ -94,6 +94,83 @@ def question(event, item):
return q
@pytest.mark.django_db
def test_log_action_filter(event, order):
sensitivekeys = [
'auth',
'key',
'token',
'password',
'Password',
'_pass',
'salt',
'secret',
]
log_data = log_data_filtered = {
'foo': 'bar',
}
for d in (log_data, log_data_filtered):
value = '********' if d == log_data_filtered else '__sensitive_info__'
for k in sensitivekeys:
d[k] = d['prefix' + k] = d['prefix' + k + 'suffix'] = d[k + 'suffix'] = value
d['recursive_dict'] = {
'test': 'dummy',
'test_token': value,
'd': {
'baz': 'foo',
'mypassword': value,
'l': [
'foo',
'bar',
{
'baz': True,
'superkey': value,
},
],
},
'l': [
'okay',
{
'salt_to_secret': value,
'name': 'okay'
},
'element',
[
'another',
{
'api_password': value,
},
'list'
]
]
}
d['recursive_list'] = [
'foo',
'bar',
[
'foo',
'bar',
{
'foo': 'bar',
'api_token': value,
},
'baz',
],
{
'foo': 'bar',
'MyPassword': value,
}
]
l1 = order.log_action(
'dummy.log',
data=log_data,
)
l1.refresh_from_db()
assert json.loads(l1.data) == log_data_filtered
@pytest.mark.django_db
def test_email_shredder(event, order):
l1 = order.log_action(