diff --git a/hknweb/templates/studentservices/course_description.html b/hknweb/templates/studentservices/course_description.html
index 0161d2d1..c4a6f439 100644
--- a/hknweb/templates/studentservices/course_description.html
+++ b/hknweb/templates/studentservices/course_description.html
@@ -16,7 +16,7 @@
{{ course.title|default:"Course Title" }}
- {% if viewer_is_an_officer %}
+ {% if viewer_in_tutoring %}
Last Updated: {{ course.updated_at }}
Edit Page
{% endif %}
diff --git a/hknweb/tutoring/views/courses.py b/hknweb/tutoring/views/courses.py
index e60b7c80..2857ca32 100644
--- a/hknweb/tutoring/views/courses.py
+++ b/hknweb/tutoring/views/courses.py
@@ -1,10 +1,11 @@
from django.shortcuts import render
-from hknweb.utils import login_and_access_level, GROUP_TO_ACCESSLEVEL
+from hknweb.utils import login_and_committee
from hknweb.studentservices.models import CourseDescription
from hknweb.tutoring.forms import AddCourseForm
+from django.conf import settings
-@login_and_access_level(GROUP_TO_ACCESSLEVEL["officer"])
+@login_and_committee(settings.TUTORING_GROUP)
def courses(request):
if request.method == "POST":
new_course = AddCourseForm(request.POST)
diff --git a/hknweb/tutoring/views/tutoringportal.py b/hknweb/tutoring/views/tutoringportal.py
index ea8327e4..a82db00b 100644
--- a/hknweb/tutoring/views/tutoringportal.py
+++ b/hknweb/tutoring/views/tutoringportal.py
@@ -1,7 +1,8 @@
from django.shortcuts import render
-from hknweb.utils import login_and_access_level, GROUP_TO_ACCESSLEVEL
+from hknweb.utils import login_and_committee, GROUP_TO_ACCESSLEVEL
+from django.conf import settings
-@login_and_access_level(GROUP_TO_ACCESSLEVEL["officer"])
+@login_and_committee(settings.TUTORING_GROUP)
def tutoringportal(request):
return render(request, "tutoring/portal.html")
diff --git a/hknweb/utils.py b/hknweb/utils.py
index b6f28809..8a8d78f1 100644
--- a/hknweb/utils.py
+++ b/hknweb/utils.py
@@ -21,8 +21,6 @@
from django.utils.safestring import mark_safe
from pytz import timezone
-###
-
# constants
@@ -99,6 +97,49 @@ def login_and_access_level(access_level):
)
+# Committee Permission Checks
+def committee_required(committee):
+ if committee not in settings.COMMITTEE_GROUPS:
+ return ValueError("Invalid Committee Group")
+
+ def test_user(user):
+ if (
+ not user.groups.filter(name=committee).exists()
+ and not user.groups.filter(name=settings.EXEC_GROUP).exists()
+ ):
+ raise PermissionDenied
+ return True
+
+ return user_passes_test(test_user)
+
+
+def login_and_committee(committee):
+ return _wrap_with_access_check(
+ f"Must be in {committee}",
+ committee_required(committee),
+ )
+
+
+# Exec Permission Checks
+def exec_required(position):
+ if position not in settings.EXEC_GROUPS:
+ return ValueError("Invalid Exec Position")
+
+ def test_user(user):
+ if not user.groups.filter(name=position).exists():
+ raise PermissionDenied
+ return True
+
+ return user_passes_test(test_user)
+
+
+def login_and_exec(position):
+ return _wrap_with_access_check(
+ f"Must be {position}",
+ exec_required(position),
+ )
+
+
def method_login_and_permission(permission_name):
return method_decorator(login_and_permission(permission_name), name="dispatch")
diff --git a/hknweb/views/people.py b/hknweb/views/people.py
index 8dda7b50..996fc43e 100644
--- a/hknweb/views/people.py
+++ b/hknweb/views/people.py
@@ -2,6 +2,8 @@
from django.db.models import QuerySet
from django.contrib.auth.models import User
from django.http import Http404
+from django.conf import settings
+from django.core.exceptions import PermissionDenied
from hknweb.utils import allow_public_access, get_access_level, GROUP_TO_ACCESSLEVEL
@@ -16,6 +18,13 @@ def people(request):
if "semester" in request.GET and not request.GET["semester"].isdigit():
raise Http404
+ is_bridge = request.user.groups.filter(name=settings.BRIDGE_GROUP).exists()
+
+ # Prevents unauthorized users from just typing the url to edit the page
+ if request.GET.get("edit") == "true":
+ if not is_bridge:
+ raise PermissionDenied
+
semester: Semester = Semester.objects.filter(
pk=request.GET.get("semester") or None
).first()
@@ -40,10 +49,8 @@ def people(request):
committee__is_exec=False
).order_by("committee__name")
- is_officer = get_access_level(request.user) <= GROUP_TO_ACCESSLEVEL["officer"]
-
form = ProfilePictureForm(request.POST)
- if is_officer and request.method == "POST":
+ if is_bridge and request.method == "POST":
user = User.objects.get(pk=request.POST["user_id"])
form.instance = user.profile
if form.is_valid():
@@ -52,7 +59,6 @@ def people(request):
context = {
"execs": execs,
"committeeships": committeeships,
- "is_officer": is_officer,
"form": form,
"semester_select_form": SemesterSelectForm({"semester": semester}),
}
diff --git a/hknweb/views/users.py b/hknweb/views/users.py
index 0a0753c2..0185ba9a 100644
--- a/hknweb/views/users.py
+++ b/hknweb/views/users.py
@@ -21,19 +21,26 @@
# context processor for base to know whether a user is in the officer group
def add_officer_context(request):
- return {
- "viewer_is_an_officer": request.user.groups.filter(
- name=settings.OFFICER_GROUP
- ).exists()
+ usergroups = request.user.groups
+ context = {
+ "viewer_is_an_officer": usergroups.filter(name=settings.OFFICER_GROUP).exists()
}
+ for committee in settings.COMMITTEE_GROUPS:
+ context[f"viewer_in_{committee}"] = (
+ usergroups.filter(name=committee).exists()
+ or usergroups.filter(name=settings.EXEC_GROUP).exists()
+ )
+ return context
def add_exec_context(request):
- return {
- "viewer_is_an_exec": request.user.groups.filter(
- name=settings.EXEC_GROUP
- ).exists()
+ usergroups = request.user.groups
+ context = {
+ "viewer_is_an_exec": usergroups.filter(name=settings.EXEC_GROUP).exists()
}
+ for exec in settings.EXEC_GROUPS:
+ context[f"viewer_in_{exec}"] = usergroups.filter(name=exec).exists()
+ return context
def get_current_cand_semester(): # pragma: no cover
@@ -77,7 +84,7 @@ def account_create(request):
candidate_password.lower()
== form.cleaned_data["candidate_password"].lower()
):
- group = Group.objects.get(name=settings.CAND_GROUP)
+ group = Group.objects.get(name=settings.UserGroup.CAND_GROUP)
group.user_set.add(user)
group.save()
diff --git a/tests/views/test_group.py b/tests/views/test_group.py
new file mode 100644
index 00000000..32ae8921
--- /dev/null
+++ b/tests/views/test_group.py
@@ -0,0 +1,126 @@
+from django.conf import settings
+from django.test import TestCase, RequestFactory
+from django.http import HttpResponse
+
+from collections import namedtuple
+from django.core.exceptions import PermissionDenied
+from tests.events.models.utils import ModelFactory
+from django.contrib.auth.models import Group, AnonymousUser
+from hknweb.utils import login_and_committee, login_and_exec
+
+
+class UsersGroupsTest(TestCase):
+ @classmethod
+ def setUpTestData(cls):
+ cls.cand = ModelFactory.create_user(username="cand")
+ cls.officer = ModelFactory.create_user(username="officer")
+ cls.exec = ModelFactory.create_user(username="exec")
+ cls.bridge = ModelFactory.create_user(username="bridge")
+ cls.tutoring = ModelFactory.create_user(username="tutoring")
+ cls.csec = ModelFactory.create_user(username="csec")
+ cls.pres = ModelFactory.create_user(username="pres")
+ cls.anon = AnonymousUser()
+
+ data = namedtuple("data", "user group bridge tutoring csec pres")
+
+ cls.user_data = (
+ data(cls.cand, settings.CAND_GROUP, 403, 403, 403, 403),
+ data(cls.officer, settings.OFFICER_GROUP, 403, 403, 403, 403),
+ data(cls.exec, settings.EXEC_GROUP, 200, 200, 403, 403),
+ data(cls.bridge, settings.BRIDGE_GROUP, 200, 403, 403, 403),
+ data(cls.tutoring, settings.TUTORING_GROUP, 403, 200, 403, 403),
+ data(cls.csec, settings.CSEC_GROUP, 403, 403, 200, 403),
+ data(cls.pres, settings.PRES_GROUP, 403, 403, 403, 200),
+ data(cls.anon, None, 302, 302, 302, 302),
+ )
+
+ for curr in cls.user_data:
+ if curr.user == cls.anon:
+ continue
+ curr_group = Group.objects.create(name=curr.group)
+ curr.user.groups.add(curr_group)
+
+ def setUp(self):
+ # https://docs.djangoproject.com/en/5.1/topics/testing/advanced/#django.test.RequestFactory
+
+ self.factory = RequestFactory()
+
+ def test_bridge_committee_req(self):
+ @login_and_committee(settings.BRIDGE_GROUP)
+ def bridge_test_view(request):
+ return HttpResponse("Access Granted")
+
+ for data in self.user_data:
+ user = data.user
+ target_status = data.bridge
+ with self.subTest(
+ Username=getattr(user, "username", "anon"), goal=target_status
+ ):
+ request = self.factory.get("/test-bridge/")
+ request.user = user
+ if target_status == 403:
+ with self.assertRaises(PermissionDenied):
+ bridge_test_view(request)
+ else:
+ response = bridge_test_view(request)
+ self.assertEqual(response.status_code, target_status)
+
+ def test_tutoring_committee_req(self):
+ @login_and_committee(settings.TUTORING_GROUP)
+ def tutoring_test_view(request):
+ return HttpResponse("Access Granted")
+
+ for data in self.user_data:
+ user = data.user
+ target_status = data.tutoring
+ with self.subTest(
+ Username=getattr(user, "username", "anon"), goal=target_status
+ ):
+ request = self.factory.get("/test-tutoring/")
+ request.user = user
+ if target_status == 403:
+ with self.assertRaises(PermissionDenied):
+ tutoring_test_view(request)
+ else:
+ response = tutoring_test_view(request)
+ self.assertEqual(response.status_code, target_status)
+
+ def test_csec_exec_req(self):
+ @login_and_exec(settings.CSEC_GROUP)
+ def csec_test_view(request):
+ return HttpResponse("Access Granted")
+
+ for data in self.user_data:
+ user = data.user
+ target_status = data.csec
+ with self.subTest(
+ Username=getattr(user, "username", "anon"), goal=target_status
+ ):
+ request = self.factory.get("/test-csec/")
+ request.user = user
+ if target_status == 403:
+ with self.assertRaises(PermissionDenied):
+ csec_test_view(request)
+ else:
+ response = csec_test_view(request)
+ self.assertEqual(response.status_code, target_status)
+
+ def test_pres_exec_req(self):
+ @login_and_exec(settings.PRES_GROUP)
+ def pres_test_view(request):
+ return HttpResponse("Access Granted")
+
+ for data in self.user_data:
+ user = data.user
+ target_status = data.pres
+ with self.subTest(
+ Username=getattr(user, "username", "anon"), goal=target_status
+ ):
+ request = self.factory.get("/test-pres/")
+ request.user = user
+ if target_status == 403:
+ with self.assertRaises(PermissionDenied):
+ pres_test_view(request)
+ else:
+ response = pres_test_view(request)
+ self.assertEqual(response.status_code, target_status)