Skip to content

Commit 96b100a

Browse files
committed
fixup! feat: add authz permission for the course authoring list
1 parent ec400f0 commit 96b100a

3 files changed

Lines changed: 104 additions & 89 deletions

File tree

cms/djangoapps/contentstore/tests/test_course_listing.py

Lines changed: 26 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
import random
77
from unittest.mock import Mock, patch
88

9-
import casbin
10-
import pkg_resources
119
import ddt
1210
from ccx_keys.locator import CCXLocator
1311
from django.test import RequestFactory
1412
from opaque_keys.edx.locations import CourseLocator
13+
from openedx_authz.api.users import assign_role_to_user_in_scope
14+
from openedx_authz.constants.roles import COURSE_DATA_RESEARCHER, COURSE_EDITOR, COURSE_STAFF
1515

1616
from cms.djangoapps.contentstore.tests.utils import AjaxEnabledTestClient
1717
from cms.djangoapps.contentstore.utils import delete_course
@@ -34,11 +34,12 @@
3434
UserBasedRole
3535
)
3636
from common.djangoapps.student.tests.factories import UserFactory
37-
from common.djangoapps.util import course
3837
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
3938
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
4039
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
4140
from openedx.core.djangolib.testing.utils import AUTHZ_TABLES
41+
from openedx.core.djangoapps.authz.tests.mixins import AuthzTestMixin
42+
from openedx.core import toggles as core_toggles
4243
from xmodule.modulestore import ModuleStoreEnum # lint-amnesty, pylint: disable=wrong-import-order
4344
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
4445
from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order
@@ -48,81 +49,6 @@
4849

4950
QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES + AUTHZ_TABLES
5051

51-
from rest_framework.test import APIClient
52-
from openedx_authz.api.users import assign_role_to_user_in_scope
53-
from openedx_authz.constants.roles import COURSE_STAFF, COURSE_EDITOR, COURSE_DATA_RESEARCHER
54-
from openedx_authz.engine.enforcer import AuthzEnforcer
55-
from openedx_authz.engine.utils import migrate_policy_between_enforcers
56-
57-
from openedx.core import toggles as core_toggles
58-
59-
60-
class AuthzTestMixin:
61-
"""
62-
Minimal reusable mixin for AuthZ-enabled tests.
63-
"""
64-
65-
@classmethod
66-
def setUpClass(cls):
67-
cls.toggle_patcher = patch.object(
68-
core_toggles.AUTHZ_COURSE_AUTHORING_FLAG,
69-
"is_enabled",
70-
return_value=True,
71-
)
72-
cls.toggle_patcher.start()
73-
super().setUpClass()
74-
75-
@classmethod
76-
def tearDownClass(cls):
77-
cls.toggle_patcher.stop()
78-
super().tearDownClass()
79-
80-
def setUp(self):
81-
super().setUp()
82-
83-
self._seed_policies()
84-
85-
self.authorized_user = UserFactory()
86-
self.unauthorized_user = UserFactory()
87-
88-
self.authorized_client = APIClient()
89-
self.authorized_client.force_authenticate(user=self.authorized_user)
90-
91-
self.unauthorized_client = APIClient()
92-
self.unauthorized_client.force_authenticate(user=self.unauthorized_user)
93-
94-
def tearDown(self):
95-
super().tearDown()
96-
AuthzEnforcer.get_enforcer().clear_policy()
97-
98-
def add_user_to_role(self, user, role, course_key):
99-
"""Helper method to add a user to a role for the course."""
100-
assign_role_to_user_in_scope(
101-
user.username,
102-
role,
103-
str(course_key)
104-
)
105-
AuthzEnforcer.get_enforcer().load_policy()
106-
107-
@classmethod
108-
def _seed_policies(cls):
109-
global_enforcer = AuthzEnforcer.get_enforcer()
110-
global_enforcer.load_policy()
111-
112-
model_path = pkg_resources.resource_filename(
113-
"openedx_authz.engine",
114-
"config/model.conf",
115-
)
116-
117-
policy_path = pkg_resources.resource_filename(
118-
"openedx_authz.engine",
119-
"config/authz.policy",
120-
)
121-
122-
migrate_policy_between_enforcers(
123-
source_enforcer=casbin.Enforcer(model_path, policy_path),
124-
target_enforcer=global_enforcer,
125-
)
12652

12753
@ddt.ddt
12854
class TestCourseListing(ModuleStoreTestCase):
@@ -497,6 +423,7 @@ def setUp(self):
497423
self.factory = RequestFactory()
498424

499425
def _create_course(self, course_key):
426+
"""Helper method to create a course and its overview."""
500427
course = CourseFactory.create(
501428
org=course_key.org,
502429
number=course_key.course,
@@ -663,20 +590,38 @@ def mock_is_enabled(*args, **kwargs):
663590
# so these three courses should be returned in the course listing even if the toggle is enabled
664591
# for authz_enable_course_3 but no role is assigned for it.
665592
self.assertEqual(len(courses), 3)
666-
self.assertTrue(all(course.id in [authz_enable_course_1.id, authz_enable_course_2.id, legacy_course_1.id] for course in courses))
593+
self.assertTrue(
594+
all(course.id in [authz_enable_course_1.id, authz_enable_course_2.id, legacy_course_1.id]
595+
for course in courses
596+
)
597+
)
667598

668599
# Case 2: Staff user should have access to all courses regardless of authz permissions.
669600
# Pending authz_course_3 to check as staff
670601
GlobalStaff().add_users(self.authorized_user)
671602
courses_list, _ = get_courses_accessible_to_user(request)
672603
courses = list(courses_list)
673604
self.assertEqual(len(courses), 6)
674-
self.assertTrue(all(course.id in [authz_enable_course_1.id, authz_enable_course_2.id, authz_enable_course_3.id, legacy_course_1.id, legacy_course_2.id, legacy_course_3.id] for course in courses))
605+
self.assertTrue(
606+
all(
607+
course.id in
608+
[authz_enable_course_1.id, authz_enable_course_2.id, authz_enable_course_3.id,
609+
legacy_course_1.id, legacy_course_2.id, legacy_course_3.id]
610+
for course in courses
611+
)
612+
)
675613

676614
# Case 3: Superuser should have access to all courses regardless of authz permissions.
677615
superuser = UserFactory(is_superuser=True)
678616
request.user = superuser
679617
courses_list, _ = get_courses_accessible_to_user(request)
680618
courses = list(courses_list)
681619
self.assertEqual(len(courses), 6)
682-
self.assertTrue(all(course.id in [authz_enable_course_1.id, authz_enable_course_2.id, authz_enable_course_3.id, legacy_course_1.id, legacy_course_2.id, legacy_course_3.id] for course in courses))
620+
self.assertTrue(
621+
all(
622+
course.id in
623+
[authz_enable_course_1.id, authz_enable_course_2.id, authz_enable_course_3.id,
624+
legacy_course_1.id, legacy_course_2.id, legacy_course_3.id]
625+
for course in courses
626+
)
627+
)

cms/djangoapps/contentstore/views/course.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -787,10 +787,12 @@ def _apply_query_filters(request, courses):
787787
"""Applies all query filters to the given courses queryset.
788788
This includes filtering by active/archived status, search query, ordering
789789
and any special filters (e.g. CCX courses, template courses). The filters are applied in the following order:
790-
1. Special filters (e.g. CCX courses, template courses)
791-
2. Active/archived status
792-
3. Search query
793-
4. Ordering
790+
1. Active/archived status
791+
2. Search query
792+
3. Ordering
793+
4. Special filters (e.g. CCX courses, template courses)
794+
The first 3 filters are applied using queryset methods, while the last filter is applied using a Python filter
795+
function since it involves checking the course type (i.e. if it's a CCX course or a template course).
794796
"""
795797

796798
def filter_course(course):
@@ -805,17 +807,16 @@ def filter_course(course):
805807

806808
return include_course
807809

808-
filtered_courses = filter(filter_course, courses)
809-
810810
search_query, order, active_only, archived_only = get_query_params_if_present(request)
811811

812-
return get_filtered_and_ordered_courses(
813-
filtered_courses,
812+
filtered_courses = get_filtered_and_ordered_courses(
813+
courses,
814814
active_only,
815815
archived_only,
816816
search_query,
817817
order,
818818
)
819+
return filter(filter_course, filtered_courses)
819820

820821

821822
def _get_candidate_course_keys(request):

openedx/core/djangoapps/authz/tests/mixins.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,75 @@
1414
from common.djangoapps.student.tests.factories import UserFactory
1515

1616

17+
class AuthzTestMixin:
18+
"""
19+
Minimal reusable mixin for AuthZ-enabled tests.
20+
"""
21+
22+
@classmethod
23+
def setUpClass(cls):
24+
cls.toggle_patcher = patch.object(
25+
core_toggles.AUTHZ_COURSE_AUTHORING_FLAG,
26+
"is_enabled",
27+
return_value=True,
28+
)
29+
cls.toggle_patcher.start()
30+
super().setUpClass()
31+
32+
@classmethod
33+
def tearDownClass(cls):
34+
cls.toggle_patcher.stop()
35+
super().tearDownClass()
36+
37+
def setUp(self):
38+
super().setUp()
39+
40+
self._seed_policies()
41+
42+
self.authorized_user = UserFactory()
43+
self.unauthorized_user = UserFactory()
44+
45+
self.authorized_client = APIClient()
46+
self.authorized_client.force_authenticate(user=self.authorized_user)
47+
48+
self.unauthorized_client = APIClient()
49+
self.unauthorized_client.force_authenticate(user=self.unauthorized_user)
50+
51+
def tearDown(self):
52+
super().tearDown()
53+
AuthzEnforcer.get_enforcer().clear_policy()
54+
55+
def add_user_to_role(self, user, role, course_key):
56+
"""Helper method to add a user to a role for the course."""
57+
assign_role_to_user_in_scope(
58+
user.username,
59+
role,
60+
str(course_key)
61+
)
62+
AuthzEnforcer.get_enforcer().load_policy()
63+
64+
@classmethod
65+
def _seed_policies(cls):
66+
"""Seed the database with AuthZ policies."""
67+
global_enforcer = AuthzEnforcer.get_enforcer()
68+
global_enforcer.load_policy()
69+
70+
model_path = pkg_resources.resource_filename(
71+
"openedx_authz.engine",
72+
"config/model.conf",
73+
)
74+
75+
policy_path = pkg_resources.resource_filename(
76+
"openedx_authz.engine",
77+
"config/authz.policy",
78+
)
79+
80+
migrate_policy_between_enforcers(
81+
source_enforcer=casbin.Enforcer(model_path, policy_path),
82+
target_enforcer=global_enforcer,
83+
)
84+
85+
1786
class CourseAuthzTestMixin:
1887
"""
1988
Reusable mixin for testing course-scoped AuthZ endpoints.

0 commit comments

Comments
 (0)