Enforce and test TODO_STAFF_ONLY setting
This commit is contained in:
parent
6953085285
commit
91b9a099a3
14 changed files with 81 additions and 51 deletions
|
@ -30,7 +30,7 @@ The assumption is that your organization/publication/company has multiple groups
|
||||||
|
|
||||||
You must have at least one Group set up in Django admin, and that group must have at least one User as a member. This is true even if you're the sole user of django-todo.
|
You must have at least one Group set up in Django admin, and that group must have at least one User as a member. This is true even if you're the sole user of django-todo.
|
||||||
|
|
||||||
Users can view and modify all to-do lists belonging to their group(s). Only users with `is_staff()` can add or delete lists.
|
Users can view and modify all to-do lists belonging to their group(s). Only users with `is_staff` can add or delete lists.
|
||||||
|
|
||||||
Identical list names can exist in different groups, but not in the same group.
|
Identical list names can exist in different groups, but not in the same group.
|
||||||
|
|
||||||
|
@ -108,8 +108,9 @@ If you wish to use the public ticket-filing system, first create the list into w
|
||||||
Optional configuration options:
|
Optional configuration options:
|
||||||
|
|
||||||
```
|
```
|
||||||
# Restrict access to todo lists/views to `is_staff()` users.
|
# Restrict access to ALL todo lists/views to `is_staff` users.
|
||||||
# False here falls back to `is_authenticated()` users.
|
# If False or unset, all users can see all views (but more granular permissions are still enforced
|
||||||
|
# within views, such as requiring staff for adding and deleting lists).
|
||||||
TODO_STAFF_ONLY = True
|
TODO_STAFF_ONLY = True
|
||||||
|
|
||||||
# If you use the "public" ticket filing option, to whom should these tickets be assigned?
|
# If you use the "public" ticket filing option, to whom should these tickets be assigned?
|
||||||
|
@ -167,6 +168,8 @@ The previous `tox` system was removed with the v2 release, since we no longer ai
|
||||||
|
|
||||||
# Version History
|
# Version History
|
||||||
|
|
||||||
|
**2.2.0** Re-instate enforcement of TODO_STAFF_ONLY setting
|
||||||
|
|
||||||
**2.1.1** Correct Python version requirement in documentation to Python 3.6
|
**2.1.1** Correct Python version requirement in documentation to Python 3.6
|
||||||
|
|
||||||
**2.1.1** Split up views into separate modules.
|
**2.1.1** Split up views into separate modules.
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"""
|
"""
|
||||||
A multi-user, multi-group task management and assignment system for Django.
|
A multi-user, multi-group task management and assignment system for Django.
|
||||||
"""
|
"""
|
||||||
__version__ = '2.1.2'
|
__version__ = '2.2.0'
|
||||||
|
|
||||||
__author__ = 'Scot Hacker'
|
__author__ = 'Scot Hacker'
|
||||||
__email__ = 'shacker@birdhouse.org'
|
__email__ = 'shacker@birdhouse.org'
|
||||||
|
|
|
@ -144,18 +144,11 @@ def test_no_javascript_in_comments(todo_setup, client):
|
||||||
|
|
||||||
# ### PERMISSIONS ###
|
# ### PERMISSIONS ###
|
||||||
|
|
||||||
"""
|
|
||||||
Some views are for staff users only.
|
|
||||||
We've already smoke-tested with Admin user - try these with normal user.
|
|
||||||
These exercise our custom @staff_only decorator without calling that function explicitly.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def test_view_add_list_nonadmin(todo_setup, client):
|
def test_view_add_list_nonadmin(todo_setup, client):
|
||||||
url = reverse("todo:add_list")
|
url = reverse("todo:add_list")
|
||||||
client.login(username="you", password="password")
|
client.login(username="you", password="password")
|
||||||
response = client.get(url)
|
response = client.get(url)
|
||||||
assert response.status_code == 403
|
assert response.status_code == 302 # Redirected to login
|
||||||
|
|
||||||
|
|
||||||
def test_view_del_list_nonadmin(todo_setup, client):
|
def test_view_del_list_nonadmin(todo_setup, client):
|
||||||
|
@ -163,7 +156,7 @@ def test_view_del_list_nonadmin(todo_setup, client):
|
||||||
url = reverse("todo:del_list", kwargs={"list_id": tlist.id, "list_slug": tlist.slug})
|
url = reverse("todo:del_list", kwargs={"list_id": tlist.id, "list_slug": tlist.slug})
|
||||||
client.login(username="you", password="password")
|
client.login(username="you", password="password")
|
||||||
response = client.get(url)
|
response = client.get(url)
|
||||||
assert response.status_code == 403
|
assert response.status_code == 302 # Fedirected to login
|
||||||
|
|
||||||
|
|
||||||
def test_view_list_mine(todo_setup, client):
|
def test_view_list_mine(todo_setup, client):
|
||||||
|
@ -220,3 +213,22 @@ def test_view_task_not_in_my_group(todo_setup, client):
|
||||||
response = client.get(url)
|
response = client.get(url)
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
def test_setting_TODO_STAFF_ONLY_False(todo_setup, client, settings):
|
||||||
|
# We use Django's user_passes_test to call `staff_check` utility function on all views.
|
||||||
|
# Just testing one view here; if it works, it works for all of them.
|
||||||
|
settings.TODO_STAFF_ONLY = False
|
||||||
|
url = reverse("todo:lists")
|
||||||
|
client.login(username="u2", password="password")
|
||||||
|
response = client.get(url)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_setting_TODO_STAFF_ONLY_True(todo_setup, client, settings):
|
||||||
|
# We use Django's user_passes_test to call `staff_check` utility function on all views.
|
||||||
|
# Just testing one view here; if it works, it works for all of them.
|
||||||
|
settings.TODO_STAFF_ONLY = True
|
||||||
|
url = reverse("todo:lists")
|
||||||
|
client.login(username="u2", password="password")
|
||||||
|
response = client.get(url)
|
||||||
|
assert response.status_code == 302 # Redirected to login view
|
||||||
|
|
|
@ -1,26 +1,22 @@
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.sites.models import Site
|
from django.contrib.sites.models import Site
|
||||||
from django.core.exceptions import PermissionDenied
|
|
||||||
from django.core.mail import send_mail
|
from django.core.mail import send_mail
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
|
|
||||||
from todo.models import Comment, Task
|
from todo.models import Comment, Task
|
||||||
|
|
||||||
|
|
||||||
def staff_only(function):
|
def staff_check(user):
|
||||||
"""
|
"""If TODO_STAFF_ONLY is set to True, limit view access to staff users only.
|
||||||
Custom view decorator allows us to raise 403 on insufficient permissions,
|
# FIXME: More granular access control is needed... but need to do it generically,
|
||||||
rather than redirect user to login view.
|
# to satisfy all possible todo implementations.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def wrap(request, *args, **kwargs):
|
if hasattr(settings, "TODO_STAFF_ONLY") and settings.TODO_STAFF_ONLY:
|
||||||
if request.user.is_staff:
|
return user.is_staff
|
||||||
return function(request, *args, **kwargs)
|
|
||||||
else:
|
else:
|
||||||
raise PermissionDenied
|
# If unset or False, allow all logged in users
|
||||||
|
return True
|
||||||
wrap.__doc__ = function.__doc__
|
|
||||||
wrap.__name__ = function.__name__
|
|
||||||
return wrap
|
|
||||||
|
|
||||||
|
|
||||||
def send_notify_mail(new_task):
|
def send_notify_mail(new_task):
|
||||||
|
|
|
@ -1,20 +1,25 @@
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required, user_passes_test
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
|
|
||||||
from todo.forms import AddTaskListForm
|
from todo.forms import AddTaskListForm
|
||||||
from todo.utils import staff_only
|
from todo.utils import staff_check
|
||||||
|
|
||||||
|
|
||||||
@staff_only
|
|
||||||
@login_required
|
@login_required
|
||||||
|
@user_passes_test(staff_check)
|
||||||
def add_list(request) -> HttpResponse:
|
def add_list(request) -> HttpResponse:
|
||||||
"""Allow users to add a new todo list to the group they're in.
|
"""Allow users to add a new todo list to the group they're in.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Only staffers can add lists.
|
||||||
|
if not request.user.is_staff:
|
||||||
|
raise PermissionDenied
|
||||||
|
|
||||||
if request.POST:
|
if request.POST:
|
||||||
form = AddTaskListForm(request.user, request.POST)
|
form = AddTaskListForm(request.user, request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
@ -33,6 +38,7 @@ def add_list(request) -> HttpResponse:
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if request.user.groups.all().count() == 1:
|
if request.user.groups.all().count() == 1:
|
||||||
|
# FIXME: Assuming first of user's groups here; better to prompt for group
|
||||||
form = AddTaskListForm(request.user, initial={"group": request.user.groups.all()[0]})
|
form = AddTaskListForm(request.user, initial={"group": request.user.groups.all()[0]})
|
||||||
else:
|
else:
|
||||||
form = AddTaskListForm(request.user)
|
form = AddTaskListForm(request.user)
|
||||||
|
|
|
@ -1,25 +1,22 @@
|
||||||
import datetime
|
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required, user_passes_test
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import render, redirect, get_object_or_404
|
from django.shortcuts import render, redirect, get_object_or_404
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
|
|
||||||
from todo.models import Task, TaskList
|
from todo.models import Task, TaskList
|
||||||
from todo.utils import staff_only
|
from todo.utils import staff_check
|
||||||
|
|
||||||
|
|
||||||
@staff_only
|
|
||||||
@login_required
|
@login_required
|
||||||
|
@user_passes_test(staff_check)
|
||||||
def del_list(request, list_id: int, list_slug: str) -> HttpResponse:
|
def del_list(request, list_id: int, list_slug: str) -> HttpResponse:
|
||||||
"""Delete an entire list. Only staff members should be allowed to access this view.
|
"""Delete an entire list. Only staff members should be allowed to access this view.
|
||||||
"""
|
"""
|
||||||
task_list = get_object_or_404(TaskList, id=list_id)
|
task_list = get_object_or_404(TaskList, id=list_id)
|
||||||
|
|
||||||
# Ensure user has permission to delete list. Admins can delete all lists.
|
# Ensure user has permission to delete list. Get the group this list belongs to,
|
||||||
# Get the group this list belongs to, and check whether current user is a member of that group.
|
# and check whether current user is a member of that group AND a staffer.
|
||||||
# FIXME: This means any group member can delete lists, which is probably too permissive.
|
|
||||||
if task_list.group not in request.user.groups.all() and not request.user.is_staff:
|
if task_list.group not in request.user.groups.all() and not request.user.is_staff:
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required, user_passes_test
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from todo.models import Task
|
from todo.models import Task
|
||||||
|
from todo.utils import staff_check
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
@user_passes_test(staff_check)
|
||||||
def delete_task(request, task_id: int) -> HttpResponse:
|
def delete_task(request, task_id: int) -> HttpResponse:
|
||||||
"""Delete specified task.
|
"""Delete specified task.
|
||||||
Redirect to the list from which the task came.
|
Redirect to the list from which the task came.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required, user_passes_test
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.sites.models import Site
|
from django.contrib.sites.models import Site
|
||||||
from django.core.mail import send_mail
|
from django.core.mail import send_mail
|
||||||
|
@ -10,9 +10,11 @@ from django.template.loader import render_to_string
|
||||||
|
|
||||||
from todo.forms import AddExternalTaskForm
|
from todo.forms import AddExternalTaskForm
|
||||||
from todo.models import TaskList
|
from todo.models import TaskList
|
||||||
|
from todo.utils import staff_check
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
@user_passes_test(staff_check)
|
||||||
def external_add(request) -> HttpResponse:
|
def external_add(request) -> HttpResponse:
|
||||||
"""Allow authenticated users who don't have access to the rest of the ticket system to file a ticket
|
"""Allow authenticated users who don't have access to the rest of the ticket system to file a ticket
|
||||||
in the list specified in settings (e.g. django-todo can be used a ticket filing system for a school, where
|
in the list specified in settings (e.g. django-todo can be used a ticket filing system for a school, where
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import bleach
|
import bleach
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required, user_passes_test
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
|
@ -8,10 +8,11 @@ from django.utils import timezone
|
||||||
|
|
||||||
from todo.forms import AddEditTaskForm
|
from todo.forms import AddEditTaskForm
|
||||||
from todo.models import Task, TaskList
|
from todo.models import Task, TaskList
|
||||||
from todo.utils import send_notify_mail
|
from todo.utils import send_notify_mail, staff_check
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
@user_passes_test(staff_check)
|
||||||
def list_detail(request, list_id=None, list_slug=None, view_completed=False) -> HttpResponse:
|
def list_detail(request, list_id=None, list_slug=None, view_completed=False) -> HttpResponse:
|
||||||
"""Display and manage tasks in a todo list.
|
"""Display and manage tasks in a todo list.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required, user_passes_test
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
|
||||||
from todo.forms import SearchForm
|
from todo.forms import SearchForm
|
||||||
from todo.models import Task, TaskList
|
from todo.models import Task, TaskList
|
||||||
|
from todo.utils import staff_check
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
@user_passes_test(staff_check)
|
||||||
def list_lists(request) -> HttpResponse:
|
def list_lists(request) -> HttpResponse:
|
||||||
"""Homepage view - list of lists a user can view, and ability to add a list.
|
"""Homepage view - list of lists a user can view, and ability to add a list.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required, user_passes_test
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
|
||||||
from todo.models import Task
|
from todo.models import Task
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from todo.utils import staff_check
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
@login_required
|
@login_required
|
||||||
|
@user_passes_test(staff_check)
|
||||||
def reorder_tasks(request) -> HttpResponse:
|
def reorder_tasks(request) -> HttpResponse:
|
||||||
"""Handle task re-ordering (priorities) from JQuery drag/drop in list_detail.html
|
"""Handle task re-ordering (priorities) from JQuery drag/drop in list_detail.html
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,18 +1,22 @@
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required, user_passes_test
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
|
||||||
from todo.models import Task
|
from todo.models import Task
|
||||||
|
from todo.utils import staff_check
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
@user_passes_test(staff_check)
|
||||||
def search(request) -> HttpResponse:
|
def search(request) -> HttpResponse:
|
||||||
"""Search for tasks user has permission to see.
|
"""Search for tasks user has permission to see.
|
||||||
"""
|
"""
|
||||||
if request.GET:
|
|
||||||
|
|
||||||
query_string = ""
|
query_string = ""
|
||||||
|
|
||||||
|
if request.GET:
|
||||||
|
|
||||||
found_tasks = None
|
found_tasks = None
|
||||||
if ("q" in request.GET) and request.GET["q"].strip():
|
if ("q" in request.GET) and request.GET["q"].strip():
|
||||||
query_string = request.GET["q"]
|
query_string = request.GET["q"]
|
||||||
|
@ -29,7 +33,6 @@ def search(request) -> HttpResponse:
|
||||||
found_tasks = found_tasks.exclude(completed=True)
|
found_tasks = found_tasks.exclude(completed=True)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
query_string = None
|
|
||||||
found_tasks = None
|
found_tasks = None
|
||||||
|
|
||||||
# Only include tasks that are in groups of which this user is a member:
|
# Only include tasks that are in groups of which this user is a member:
|
||||||
|
|
|
@ -2,17 +2,18 @@ import datetime
|
||||||
|
|
||||||
import bleach
|
import bleach
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required, user_passes_test
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
|
|
||||||
from todo.forms import AddEditTaskForm
|
from todo.forms import AddEditTaskForm
|
||||||
from todo.models import Comment, Task
|
from todo.models import Comment, Task
|
||||||
from todo.utils import send_email_to_thread_participants, toggle_task_completed
|
from todo.utils import send_email_to_thread_participants, toggle_task_completed, staff_check
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
@user_passes_test(staff_check)
|
||||||
def task_detail(request, task_id: int) -> HttpResponse:
|
def task_detail(request, task_id: int) -> HttpResponse:
|
||||||
"""View task details. Allow task details to be edited. Process new comments on task.
|
"""View task details. Allow task details to be edited. Process new comments on task.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required, user_passes_test
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
|
@ -7,9 +7,11 @@ from django.urls import reverse
|
||||||
|
|
||||||
from todo.models import Task
|
from todo.models import Task
|
||||||
from todo.utils import toggle_task_completed
|
from todo.utils import toggle_task_completed
|
||||||
|
from todo.utils import staff_check
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
@user_passes_test(staff_check)
|
||||||
def toggle_done(request, task_id: int) -> HttpResponse:
|
def toggle_done(request, task_id: int) -> HttpResponse:
|
||||||
"""Toggle the completed status of a task from done to undone, or vice versa.
|
"""Toggle the completed status of a task from done to undone, or vice versa.
|
||||||
Redirect to the list from which the task came.
|
Redirect to the list from which the task came.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue