mirror of
https://github.com/django-helpdesk/django-helpdesk.git
synced 2024-12-13 18:31:10 +01:00
Merge pull request #1161 from timthelion/view_protectors
Add some convenient functions for protecting views in custom installations
This commit is contained in:
commit
6456a1f951
@ -39,14 +39,32 @@ If you want to override the default settings for your users, create ``HELPDESK_D
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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?
|
- **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``
|
**Default:** ``HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = False``
|
||||||
|
|
||||||
|
- **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']
|
||||||
|
|
||||||
|
- **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?
|
- **HELPDESK_KB_ENABLED** show knowledgebase links?
|
||||||
|
|
||||||
**Default:** ``HELPDESK_KB_ENABLED = True``
|
**Default:** ``HELPDESK_KB_ENABLED = True``
|
||||||
@ -87,18 +105,10 @@ These changes are visible throughout django-helpdesk
|
|||||||
|
|
||||||
**Default:** ``HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE = 512000``
|
**Default:** ``HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE = 512000``
|
||||||
|
|
||||||
- **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']``
|
|
||||||
|
|
||||||
- **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.
|
- **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``
|
**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.
|
- **HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET** If False, disable the dependencies fields on ticket.
|
||||||
|
|
||||||
**Default:** ``HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET = True``
|
**Default:** ``HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET = True``
|
||||||
|
@ -49,6 +49,8 @@ def protect_view(view_func):
|
|||||||
return redirect('helpdesk:login')
|
return redirect('helpdesk:login')
|
||||||
elif not request.user.is_authenticated and helpdesk_settings.HELPDESK_ANON_ACCESS_RAISES_404:
|
elif not request.user.is_authenticated and helpdesk_settings.HELPDESK_ANON_ACCESS_RAISES_404:
|
||||||
raise Http404
|
raise Http404
|
||||||
|
if auth_redirect := helpdesk_settings.HELPDESK_PUBLIC_VIEW_PROTECTOR(request):
|
||||||
|
return auth_redirect
|
||||||
return view_func(request, *args, **kwargs)
|
return view_func(request, *args, **kwargs)
|
||||||
|
|
||||||
return _wrapped_view
|
return _wrapped_view
|
||||||
@ -65,6 +67,8 @@ def staff_member_required(view_func):
|
|||||||
return redirect('helpdesk:login')
|
return redirect('helpdesk:login')
|
||||||
if not helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE and not request.user.is_staff:
|
if not helpdesk_settings.HELPDESK_ALLOW_NON_STAFF_TICKET_UPDATE and not request.user.is_staff:
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
|
if auth_redirect := helpdesk_settings.HELPDESK_STAFF_VIEW_PROTECTOR(request):
|
||||||
|
return auth_redirect
|
||||||
return view_func(request, *args, **kwargs)
|
return view_func(request, *args, **kwargs)
|
||||||
|
|
||||||
return _wrapped_view
|
return _wrapped_view
|
||||||
|
@ -10,6 +10,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import warnings
|
import warnings
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_USER_SETTINGS = {
|
DEFAULT_USER_SETTINGS = {
|
||||||
@ -47,6 +48,14 @@ HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT = getattr(settings,
|
|||||||
'HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT',
|
'HELPDESK_REDIRECT_TO_LOGIN_BY_DEFAULT',
|
||||||
False)
|
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
|
# Enable the Dependencies field on ticket view
|
||||||
HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET = getattr(settings,
|
HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET = getattr(settings,
|
||||||
'HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET',
|
'HELPDESK_ENABLE_DEPENDENCIES_ON_TICKET',
|
||||||
@ -359,6 +368,15 @@ HELPDESK_IMAP_DEBUG_LEVEL = getattr(settings, 'HELPDESK_IMAP_DEBUG_LEVEL', 0)
|
|||||||
# Override it in your own Django settings.py
|
# Override it in your own Django settings.py
|
||||||
HELPDESK_ATTACHMENT_DIR_PERMS = int(getattr(settings, 'HELPDESK_ATTACHMENT_DIR_PERMS', "755"), 8)
|
HELPDESK_ATTACHMENT_DIR_PERMS = int(getattr(settings, 'HELPDESK_ATTACHMENT_DIR_PERMS', "755"), 8)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
def get_followup_webhook_urls():
|
def get_followup_webhook_urls():
|
||||||
urls = os.environ.get('HELPDESK_FOLLOWUP_WEBHOOK_URLS', None)
|
urls = os.environ.get('HELPDESK_FOLLOWUP_WEBHOOK_URLS', None)
|
||||||
if urls:
|
if urls:
|
||||||
|
@ -200,12 +200,11 @@ class GetEmailCommonTests(TestCase):
|
|||||||
# The extractor prepends a part identifier so compare the ending
|
# 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}")
|
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):
|
def test_wrong_extension_attachment(self):
|
||||||
"""
|
"""
|
||||||
Tests if an attachment with a wrong extension doesn't stop the email process
|
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
|
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)
|
extract_email_metadata(message.as_string(), self.queue_public, self.logger)
|
||||||
|
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
"ERROR:helpdesk:['Unsupported file extension: .jpg']",
|
"ERROR:helpdesk:['Unsupported file extension: .exe']",
|
||||||
cm.output
|
cm.output
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -237,12 +236,11 @@ class GetEmailCommonTests(TestCase):
|
|||||||
followup = ticket.followup_set.get()
|
followup = ticket.followup_set.get()
|
||||||
self.assertEqual(2, followup.followupattachment_set.count())
|
self.assertEqual(2, followup.followupattachment_set.count())
|
||||||
|
|
||||||
@override_settings(VALID_EXTENSIONS=['.txt'])
|
|
||||||
def test_multiple_attachments_with_wrong_extension(self):
|
def test_multiple_attachments_with_wrong_extension(self):
|
||||||
"""
|
"""
|
||||||
Tests that a wrong extension won't stop from saving other valid attachment
|
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
|
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)
|
extract_email_metadata(message.as_string(), self.queue_public, self.logger)
|
||||||
|
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
"ERROR:helpdesk:['Unsupported file extension: .jpg']",
|
"ERROR:helpdesk:['Unsupported file extension: .exe']",
|
||||||
cm.output
|
cm.output
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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)
|
part.add_header('Content-Disposition', "attachment; filename=%s" % filename)
|
||||||
return part
|
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:
|
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)
|
msg = MIMEText(body, part_type)
|
||||||
elif "file" == part_type:
|
elif "file" == part_type:
|
||||||
msg = generate_file_mime_part(locale=locale)
|
msg = generate_file_mime_part(locale=locale)
|
||||||
|
elif "executable" == part_type:
|
||||||
|
msg = generate_executable_mime_part(locale=locale)
|
||||||
elif "image" == part_type:
|
elif "image" == part_type:
|
||||||
msg = generate_image_mime_part(locale=locale)
|
msg = generate_image_mime_part(locale=locale)
|
||||||
else:
|
else:
|
||||||
@ -236,7 +253,7 @@ def generate_multipart_email(locale: str="en_US",
|
|||||||
Generates an email including headers with the defined multiparts
|
Generates an email including headers with the defined multiparts
|
||||||
|
|
||||||
:param locale:
|
: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 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
|
:param use_short_email: produces a "To" or "From" that is only the email address if True
|
||||||
"""
|
"""
|
||||||
|
@ -2,9 +2,8 @@
|
|||||||
#
|
#
|
||||||
# validators for file uploads, etc.
|
# validators for file uploads, etc.
|
||||||
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
from helpdesk import settings as helpdesk_settings
|
||||||
|
|
||||||
|
|
||||||
# TODO: can we use the builtin Django validator instead?
|
# 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
|
# TODO: we might improve this with more thorough checks of file types
|
||||||
# rather than just the extensions.
|
# rather than just the extensions.
|
||||||
|
|
||||||
# check if VALID_EXTENSIONS is defined in settings.py
|
if not helpdesk_settings.HELPDESK_VALIDATE_ATTACHMENT_TYPES:
|
||||||
# if not use defaults
|
return
|
||||||
|
|
||||||
if hasattr(settings, 'VALID_EXTENSIONS'):
|
if ext.lower() not in helpdesk_settings.HELPDESK_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:
|
|
||||||
# TODO: one more check in case it is a file with no extension; we
|
# TODO: one more check in case it is a file with no extension; we
|
||||||
# should always allow that?
|
# should always allow that?
|
||||||
if not (ext.lower() == '' or ext.lower() == '.'):
|
if not (ext.lower() == '' or ext.lower() == '.'):
|
||||||
|
@ -7,6 +7,8 @@ from rest_framework.permissions import IsAdminUser, IsAuthenticated
|
|||||||
from rest_framework.viewsets import GenericViewSet
|
from rest_framework.viewsets import GenericViewSet
|
||||||
from rest_framework.pagination import PageNumberPagination
|
from rest_framework.pagination import PageNumberPagination
|
||||||
|
|
||||||
|
from helpdesk import settings as helpdesk_settings
|
||||||
|
|
||||||
|
|
||||||
class ConservativePagination(PageNumberPagination):
|
class ConservativePagination(PageNumberPagination):
|
||||||
page_size = 25
|
page_size = 25
|
||||||
@ -34,6 +36,10 @@ class UserTicketViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
class TicketViewSet(viewsets.ModelViewSet):
|
class TicketViewSet(viewsets.ModelViewSet):
|
||||||
"""
|
"""
|
||||||
A viewset that provides the standard actions to handle Ticket
|
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()
|
queryset = Ticket.objects.all()
|
||||||
serializer_class = TicketSerializer
|
serializer_class = TicketSerializer
|
||||||
@ -42,6 +48,20 @@ class TicketViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
tickets = Ticket.objects.all()
|
tickets = Ticket.objects.all()
|
||||||
|
|
||||||
|
# filter by status
|
||||||
|
status = self.request.query_params.get('status', None)
|
||||||
|
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:
|
for ticket in tickets:
|
||||||
ticket.set_custom_field_values()
|
ticket.set_custom_field_values()
|
||||||
return tickets
|
return tickets
|
||||||
|
Loading…
Reference in New Issue
Block a user