From b1b83cd5936cd1fe804bd85d5bb2a74b5de6bc71 Mon Sep 17 00:00:00 2001 From: Timothy Hobbs Date: Sun, 18 Feb 2024 10:04:01 +0100 Subject: [PATCH 1/5] Add some convenient functions for protecting views in custom installations --- helpdesk/decorators.py | 4 ++++ helpdesk/settings.py | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/helpdesk/decorators.py b/helpdesk/decorators.py index e003440a..82ef925d 100644 --- a/helpdesk/decorators.py +++ b/helpdesk/decorators.py @@ -49,6 +49,8 @@ def protect_view(view_func): return redirect('helpdesk:login') elif not request.user.is_authenticated and helpdesk_settings.HELPDESK_ANON_ACCESS_RAISES_404: raise Http404 + if auth_redirect := helpdesk_settings.HELPDESK_PUBLIC_VIEW_PROTECTOR(request): + return auth_redirect return view_func(request, *args, **kwargs) return _wrapped_view @@ -65,6 +67,8 @@ def staff_member_required(view_func): return redirect('helpdesk:login') if not helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE and not request.user.is_staff: raise PermissionDenied() + if auth_redirect := helpdesk_settings.HELPDESK_STAFF_VIEW_PROTECTOR(request): + return auth_redirect return view_func(request, *args, **kwargs) return _wrapped_view diff --git a/helpdesk/settings.py b/helpdesk/settings.py index cf5ca644..2cd0ac4e 100644 --- a/helpdesk/settings.py +++ b/helpdesk/settings.py @@ -47,6 +47,14 @@ HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = getattr(settings, 'HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT', False) +HELPDESK_PUBLIC_VIEW_PROTECTOR = getattr(settings, + 'HELPDESK_PUBLIC_VIEW_PROTECTOR', + lambda _: None) + +HELPDESK_STAFF_VIEW_PROTECTOR = getattr(settings, + 'HELPDESK_STAFF_VIEW_PROTECTOR', + lambda _: None) + # Enable the Dependencies field on ticket view HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET = getattr(settings, 'HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET', From 9a353bf4a5eb9afd31422ee5c28f7f2fa50348ee Mon Sep 17 00:00:00 2001 From: Timothy Hobbs Date: Tue, 20 Feb 2024 22:57:39 +0100 Subject: [PATCH 2/5] You can now turn off file attachment filtering, fixes #1162 --- docs/settings.rst | 6 ++++-- helpdesk/settings.py | 4 ++++ helpdesk/tests/test_get_email.py | 10 ++++------ helpdesk/tests/utils.py | 19 ++++++++++++++++++- helpdesk/validators.py | 15 ++++----------- 5 files changed, 34 insertions(+), 20 deletions(-) diff --git a/docs/settings.rst b/docs/settings.rst index 56199046..94d71bcf 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -85,9 +85,11 @@ These changes are visible throughout django-helpdesk **Default:** ``HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE = 512000`` -- **VALID_EXTENSIONS** Valid extensions for file types that can be attached to tickets +- **HELPDESK_VALID_EXTENSIONS** Valid extensions for file types that can be attached to tickets - **Default:** ``VALID_EXTENSIONS = ['.txt', '.asc', '.htm', '.html', '.pdf', '.doc', '.docx', '.odt', '.jpg', '.png', '.eml'] + **Default:** ``HELPDESK_VALID_EXTENSIONS = ['.txt', '.asc', '.htm', '.html', '.pdf', '.doc', '.docx', '.odt', '.jpg', '.png', '.eml'] + +- **HELPDESK_VALIDATE_ATTACHMENT_TYPES** If you'd like to turn of filtering of helpdesk extension types you can set this to False. - **QUEUE_EMAIL_BOX_UPDATE_ONLY** Only process mail with a valid tracking ID; all other mail will be ignored instead of creating a new ticket. diff --git a/helpdesk/settings.py b/helpdesk/settings.py index 2cd0ac4e..53c3123a 100644 --- a/helpdesk/settings.py +++ b/helpdesk/settings.py @@ -337,6 +337,10 @@ HELPDESK_IMAP_DEBUG_LEVEL = getattr(settings, 'HELPDESK_IMAP_DEBUG_LEVEL', 0) # Override it in your own Django settings.py HELPDESK_ATTACHMENT_DIR_PERMS = int(getattr(settings, 'HELPDESK_ATTACHMENT_DIR_PERMS', "755"), 8) +HELPDESK_VALID_EXTENSIONS = getattr(settings, 'HELPDESK_VALID_EXTENSIONS', ['.txt', '.asc', '.htm', '.html', '.pdf', '.doc', '.docx', '.odt', '.jpg', '.png', '.eml']) + +HELPDESK_VALIDATE_ATTACHMENT_TYPES = getattr(settings, 'HELPDESK_VALIDATE_ATTACHMENT_TYPES', True) + def get_followup_webhook_urls(): urls = os.environ.get('HELPDESK_FOLLOWUP_WEBHOOK_URLS', None) if urls: diff --git a/helpdesk/tests/test_get_email.py b/helpdesk/tests/test_get_email.py index 9c8adb5f..bdd4274a 100644 --- a/helpdesk/tests/test_get_email.py +++ b/helpdesk/tests/test_get_email.py @@ -200,12 +200,11 @@ class GetEmailCommonTests(TestCase): # The extractor prepends a part identifier so compare the ending self.assertTrue(sent_file.name.endswith(filename), f"Filename extracted does not match: {sent_file.name}") - @override_settings(VALID_EXTENSIONS=['.png']) def test_wrong_extension_attachment(self): """ Tests if an attachment with a wrong extension doesn't stop the email process """ - message, _, _ = utils.generate_multipart_email(type_list=['plain', 'image']) + message, _, _ = utils.generate_multipart_email(type_list=['plain', 'executable']) self.assertEqual(len(mail.outbox), 0) # @UndefinedVariable @@ -213,7 +212,7 @@ class GetEmailCommonTests(TestCase): extract_email_metadata(message.as_string(), self.queue_public, self.logger) self.assertIn( - "ERROR:helpdesk:['Unsupported file extension: .jpg']", + "ERROR:helpdesk:['Unsupported file extension: .exe']", cm.output ) @@ -237,12 +236,11 @@ class GetEmailCommonTests(TestCase): followup = ticket.followup_set.get() self.assertEqual(2, followup.followupattachment_set.count()) - @override_settings(VALID_EXTENSIONS=['.txt']) def test_multiple_attachments_with_wrong_extension(self): """ Tests that a wrong extension won't stop from saving other valid attachment """ - message, _, _ = utils.generate_multipart_email(type_list=['plain', 'image', 'file', 'image']) + message, _, _ = utils.generate_multipart_email(type_list=['plain', 'executable', 'file', 'executable']) self.assertEqual(len(mail.outbox), 0) # @UndefinedVariable @@ -250,7 +248,7 @@ class GetEmailCommonTests(TestCase): extract_email_metadata(message.as_string(), self.queue_public, self.logger) self.assertIn( - "ERROR:helpdesk:['Unsupported file extension: .jpg']", + "ERROR:helpdesk:['Unsupported file extension: .exe']", cm.output ) diff --git a/helpdesk/tests/utils.py b/helpdesk/tests/utils.py index e3011b2b..51cf3c2a 100644 --- a/helpdesk/tests/utils.py +++ b/helpdesk/tests/utils.py @@ -156,6 +156,21 @@ def generate_file_mime_part(locale: str="en_US",filename: str = None, content: s part.add_header('Content-Disposition', "attachment; filename=%s" % filename) return part +def generate_executable_mime_part(locale: str="en_US",filename: str = None, content: str = None) -> Message: + """ + + :param locale: change this to generate locale specific file name and attachment content + :param filename: pass a file name if you want to specify a specific name otherwise a random name will be generated + :param content: pass a string value if you want have specific content otherwise a random string will be generated + """ + part = MIMEBase('application', 'vnd.microsoft.portable-executable') + part.set_payload(get_fake("text", locale=locale, min_length=1024) if content is None else content) + encoders.encode_base64(part) + if not filename: + filename = get_fake("word", locale=locale, min_length=8) + ".exe" + part.add_header('Content-Disposition', "attachment; filename=%s" % filename) + return part + def generate_image_mime_part(locale: str="en_US",imagename: str = None, disposition_primary_type: str = "attachment") -> Message: """ @@ -221,6 +236,8 @@ def generate_mime_part(locale: str="en_US", msg = MIMEText(body, part_type) elif "file" == part_type: msg = generate_file_mime_part(locale=locale) + elif "executable" == part_type: + msg = generate_executable_mime_part(locale=locale) elif "image" == part_type: msg = generate_image_mime_part(locale=locale) else: @@ -236,7 +253,7 @@ def generate_multipart_email(locale: str="en_US", Generates an email including headers with the defined multiparts :param locale: - :param type_list: options are plain, html, image (attachment), file (attachment) + :param type_list: options are plain, html, image (attachment), file (attachment), and executable (.exe attachment) :param sub_type: multipart sub type that defaults to "mixed" if not specified :param use_short_email: produces a "To" or "From" that is only the email address if True """ diff --git a/helpdesk/validators.py b/helpdesk/validators.py index 72589111..0fc58703 100644 --- a/helpdesk/validators.py +++ b/helpdesk/validators.py @@ -2,9 +2,8 @@ # # validators for file uploads, etc. - -from django.conf import settings from django.utils.translation import gettext as _ +from helpdesk import settings as helpdesk_settings # TODO: can we use the builtin Django validator instead? @@ -19,16 +18,10 @@ def validate_file_extension(value): # TODO: we might improve this with more thorough checks of file types # rather than just the extensions. - # check if VALID_EXTENSIONS is defined in settings.py - # if not use defaults + if not helpdesk_settings.HELPDESK_VALIDATE_ATTACHMENT_TYPES: + return - if hasattr(settings, 'VALID_EXTENSIONS'): - valid_extensions = settings.VALID_EXTENSIONS - else: - valid_extensions = ['.txt', '.asc', '.htm', '.html', - '.pdf', '.doc', '.docx', '.odt', '.jpg', '.png', '.eml'] - - if ext.lower() not in valid_extensions: + if ext.lower() not in helpdesk_settings.HELPDESK_VALID_EXTENSIONS: # TODO: one more check in case it is a file with no extension; we # should always allow that? if not (ext.lower() == '' or ext.lower() == '.'): From 50b5069399f028d43612b5d156dd2c76bb3eb810 Mon Sep 17 00:00:00 2001 From: Timothy Hobbs Date: Mon, 26 Feb 2024 20:31:37 +0100 Subject: [PATCH 3/5] Add documentation for new redirect gates for views --- docs/settings.rst | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/docs/settings.rst b/docs/settings.rst index 94d71bcf..4db13dcd 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -37,14 +37,32 @@ If you want to override the default settings for your users, create ``HELPDESK_D 'tickets_per_page': 25 } -Generic Options +Access controll & Security --------------- -These changes are visible throughout django-helpdesk +These settings can be used to change who can access the helpdesk. + +- **HELPDESK_PUBLIC_VIEW_PROTECTOR** This is a function that takes a request and can either return `None` granting access to to a public view or a redirect denying access. + +- **HELPDESK_STAFF_VIEW_PROTECTOR** This is a function that takes a request and can either return `None` granting access to to a staff view or a redirect denying access. - **HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT** When a user visits "/", should we redirect to the login page instead of the default homepage? **Default:** ``HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = False`` +- **HELPDESK_VALID_EXTENSIONS** Valid extensions for file types that can be attached to tickets + + **Default:** ``HELPDESK_VALID_EXTENSIONS = ['.txt', '.asc', '.htm', '.html', '.pdf', '.doc', '.docx', '.odt', '.jpg', '.png', '.eml'] + +- **HELPDESK_VALIDATE_ATTACHMENT_TYPES** If you'd like to turn of filtering of helpdesk extension types you can set this to False. + +- **HELPDESK_ANON_ACCESS_RAISES_404** If True, redirects user to a 404 page when attempting to reach ticket pages while not logged in, rather than redirecting to a login screen. + + **Default:** ``HELPDESK_ANON_ACCESS_RAISES_404 = False`` + +Generic Options +--------------- +These changes are visible throughout django-helpdesk + - **HELPDESK_KB_ENABLED** show knowledgebase links? **Default:** ``HELPDESK_KB_ENABLED = True`` @@ -85,20 +103,10 @@ These changes are visible throughout django-helpdesk **Default:** ``HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE = 512000`` -- **HELPDESK_VALID_EXTENSIONS** Valid extensions for file types that can be attached to tickets - - **Default:** ``HELPDESK_VALID_EXTENSIONS = ['.txt', '.asc', '.htm', '.html', '.pdf', '.doc', '.docx', '.odt', '.jpg', '.png', '.eml'] - -- **HELPDESK_VALIDATE_ATTACHMENT_TYPES** If you'd like to turn of filtering of helpdesk extension types you can set this to False. - - **QUEUE_EMAIL_BOX_UPDATE_ONLY** Only process mail with a valid tracking ID; all other mail will be ignored instead of creating a new ticket. **Default:** ``QUEUE_EMAIL_BOX_UPDATE_ONLY = False`` -- **HELPDESK_ANON_ACCESS_RAISES_404** If True, redirects user to a 404 page when attempting to reach ticket pages while not logged in, rather than redirecting to a login screen. - - **Default:** ``HELPDESK_ANON_ACCESS_RAISES_404 = False`` - - **HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET** If False, disable the dependencies fields on ticket. **Default:** ``HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET = True`` From 128a465d44fa35cd07fb5a7da3cb5f85e44aa3ca Mon Sep 17 00:00:00 2001 From: Timothy Hobbs Date: Sat, 2 Mar 2024 00:28:00 +0100 Subject: [PATCH 4/5] Enabling filtering of tickets by status in API --- helpdesk/views/api.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/helpdesk/views/api.py b/helpdesk/views/api.py index fce0e816..657832e7 100644 --- a/helpdesk/views/api.py +++ b/helpdesk/views/api.py @@ -7,6 +7,8 @@ from rest_framework.permissions import IsAdminUser, IsAuthenticated from rest_framework.viewsets import GenericViewSet from rest_framework.pagination import PageNumberPagination +from helpdesk import settings as helpdesk_settings + class ConservativePagination(PageNumberPagination): page_size = 25 @@ -34,6 +36,10 @@ class UserTicketViewSet(viewsets.ReadOnlyModelViewSet): class TicketViewSet(viewsets.ModelViewSet): """ A viewset that provides the standard actions to handle Ticket + + You can filter the tickets by status using the `status` query parameter. For example: + + `/api/tickets/?status=Open,Resolved` will return all the tickets that are Open or Resolved. """ queryset = Ticket.objects.all() serializer_class = TicketSerializer @@ -42,6 +48,19 @@ class TicketViewSet(viewsets.ModelViewSet): def get_queryset(self): tickets = Ticket.objects.all() + + # filter by status + status = self.request.query_params.get('status', None) + statuses = status.split(',') if status else [] + status_choices = helpdesk_settings.TICKET_STATUS_CHOICES + number_statuses = [] + for status in statuses: + for choice in status_choices: + if choice[1] == status: + number_statuses.append(choice[0]) + if number_statuses: + tickets = tickets.filter(status__in=number_statuses) + for ticket in tickets: ticket.set_custom_field_values() return tickets From ebbbcdb2aa5ed0e13662038ce2b225259163a58f Mon Sep 17 00:00:00 2001 From: Timothy Hobbs Date: Mon, 22 Apr 2024 18:33:58 +0200 Subject: [PATCH 5/5] Deprecate VALID_EXTENSIONS properly --- docs/settings.rst | 2 +- helpdesk/settings.py | 8 +++++++- helpdesk/views/api.py | 19 ++++++++++--------- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/docs/settings.rst b/docs/settings.rst index 4db13dcd..d00fb7d6 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -49,7 +49,7 @@ These settings can be used to change who can access the helpdesk. **Default:** ``HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = False`` -- **HELPDESK_VALID_EXTENSIONS** Valid extensions for file types that can be attached to tickets +- **HELPDESK_VALID_EXTENSIONS** Valid extensions for file types that can be attached to tickets. Note: This used to be calle **VALID_EXTENSIONS** which is now deprecated. **Default:** ``HELPDESK_VALID_EXTENSIONS = ['.txt', '.asc', '.htm', '.html', '.pdf', '.doc', '.docx', '.odt', '.jpg', '.png', '.eml'] diff --git a/helpdesk/settings.py b/helpdesk/settings.py index 53c3123a..c10d52c0 100644 --- a/helpdesk/settings.py +++ b/helpdesk/settings.py @@ -10,6 +10,7 @@ from django.utils.translation import gettext_lazy as _ import os import re import warnings +import sys DEFAULT_USER_SETTINGS = { @@ -337,7 +338,12 @@ HELPDESK_IMAP_DEBUG_LEVEL = getattr(settings, 'HELPDESK_IMAP_DEBUG_LEVEL', 0) # Override it in your own Django settings.py HELPDESK_ATTACHMENT_DIR_PERMS = int(getattr(settings, 'HELPDESK_ATTACHMENT_DIR_PERMS', "755"), 8) -HELPDESK_VALID_EXTENSIONS = getattr(settings, 'HELPDESK_VALID_EXTENSIONS', ['.txt', '.asc', '.htm', '.html', '.pdf', '.doc', '.docx', '.odt', '.jpg', '.png', '.eml']) +HELPDESK_VALID_EXTENSIONS = getattr(settings, 'VALID_EXTENSIONS', None) +if HELPDESK_VALID_EXTENSIONS: + # Print to stderr + print("VALID_EXTENSIONS is deprecated, use HELPDESK_VALID_EXTENSIONS instead", file=sys.stderr) +else: + HELPDESK_VALID_EXTENSIONS = getattr(settings, 'HELPDESK_VALID_EXTENSIONS', ['.txt', '.asc', '.htm', '.html', '.pdf', '.doc', '.docx', '.odt', '.jpg', '.png', '.eml']) HELPDESK_VALIDATE_ATTACHMENT_TYPES = getattr(settings, 'HELPDESK_VALIDATE_ATTACHMENT_TYPES', True) diff --git a/helpdesk/views/api.py b/helpdesk/views/api.py index 657832e7..8e2e9edd 100644 --- a/helpdesk/views/api.py +++ b/helpdesk/views/api.py @@ -51,15 +51,16 @@ class TicketViewSet(viewsets.ModelViewSet): # filter by status status = self.request.query_params.get('status', None) - statuses = status.split(',') if status else [] - status_choices = helpdesk_settings.TICKET_STATUS_CHOICES - number_statuses = [] - for status in statuses: - for choice in status_choices: - if choice[1] == status: - number_statuses.append(choice[0]) - if number_statuses: - tickets = tickets.filter(status__in=number_statuses) + if status: + statuses = status.split(',') if status else [] + status_choices = helpdesk_settings.TICKET_STATUS_CHOICES + number_statuses = [] + for status in statuses: + for choice in status_choices: + if str(choice[0]) == status: + number_statuses.append(choice[0]) + if number_statuses: + tickets = tickets.filter(status__in=number_statuses) for ticket in tickets: ticket.set_custom_field_values()