|
17 | 17 | from edx_django_utils.cache import RequestCache |
18 | 18 | from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator, LibraryCollectionLocator, LibraryContainerLocator |
19 | 19 | from openedx_authz.constants import permissions as authz_permissions |
20 | | -from openedx_authz.constants.roles import COURSE_STAFF |
| 20 | +from openedx_authz.constants.roles import COURSE_ADMIN, COURSE_STAFF |
21 | 21 | from openedx_tagging.models import Tag, Taxonomy |
22 | 22 | from openedx_tagging.models.system_defined import SystemDefinedTaxonomy |
23 | 23 | from openedx_tagging.rest_api.v1.serializers import TaxonomySerializer |
@@ -2136,6 +2136,166 @@ def test_superuser_allowed(self): |
2136 | 2136 | resp = client.get(self.get_url(self.course_key)) |
2137 | 2137 | self.assertEqual(resp.status_code, status.HTTP_200_OK) |
2138 | 2138 |
|
| 2139 | +@skip_unless_cms |
| 2140 | +class TestTaxonomyEndpointsWithAuthz(CourseAuthzTestMixin, SharedModuleStoreTestCase, APITestCase): |
| 2141 | + """ |
| 2142 | + Tests taxonomy management endpoints with openedx-authz. |
| 2143 | +
|
| 2144 | + When the AUTHZ_COURSE_AUTHORING_FLAG is globally enabled, taxonomy CRUD |
| 2145 | + endpoints should enforce courses.manage_taxonomies / courses.manage_tags |
| 2146 | + permissions via openedx-authz. |
| 2147 | +
|
| 2148 | + Uses COURSE_ADMIN role because only course_admin has manage_taxonomies permission. |
| 2149 | + """ |
| 2150 | + |
| 2151 | + authz_roles_to_assign = [COURSE_ADMIN.external_key] |
| 2152 | + |
| 2153 | + @classmethod |
| 2154 | + def setUpClass(cls): |
| 2155 | + super().setUpClass() |
| 2156 | + cls.password = 'test' |
| 2157 | + cls.course = CourseFactory.create() |
| 2158 | + cls.course_key = cls.course.id |
| 2159 | + cls.staff = StaffFactory(course_key=cls.course_key, password=cls.password) |
| 2160 | + |
| 2161 | + def setUp(self): |
| 2162 | + super().setUp() |
| 2163 | + self.orgA = Organization.objects.create(name="Organization A", short_name="orgA") |
| 2164 | + self.taxonomy = tagging_api.create_taxonomy(name="Test Taxonomy") |
| 2165 | + tagging_api.set_taxonomy_orgs(self.taxonomy, all_orgs=False, orgs=[self.orgA]) |
| 2166 | + |
| 2167 | + def test_delete_taxonomy_authorized(self): |
| 2168 | + """Authorized user can delete a taxonomy.""" |
| 2169 | + url = TAXONOMY_ORG_DETAIL_URL.format(pk=self.taxonomy.id) |
| 2170 | + resp = self.authorized_client.delete(url) |
| 2171 | + self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT) |
| 2172 | + |
| 2173 | + def test_delete_taxonomy_unauthorized(self): |
| 2174 | + """Unauthorized user cannot delete a taxonomy.""" |
| 2175 | + url = TAXONOMY_ORG_DETAIL_URL.format(pk=self.taxonomy.id) |
| 2176 | + resp = self.unauthorized_client.delete(url) |
| 2177 | + self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN) |
| 2178 | + |
| 2179 | + def test_update_orgs_authorized(self): |
| 2180 | + """Authorized user can update taxonomy orgs.""" |
| 2181 | + url = TAXONOMY_ORG_UPDATE_ORG_URL.format(pk=self.taxonomy.id) |
| 2182 | + resp = self.authorized_client.put(url, {"all_orgs": True}, format="json") |
| 2183 | + self.assertEqual(resp.status_code, status.HTTP_200_OK) |
| 2184 | + |
| 2185 | + def test_update_orgs_unauthorized(self): |
| 2186 | + """Unauthorized user cannot update taxonomy orgs.""" |
| 2187 | + url = TAXONOMY_ORG_UPDATE_ORG_URL.format(pk=self.taxonomy.id) |
| 2188 | + resp = self.unauthorized_client.put(url, {"all_orgs": True}, format="json") |
| 2189 | + self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN) |
| 2190 | + |
| 2191 | + def test_create_import_authorized(self): |
| 2192 | + """Authorized user can create a taxonomy via import.""" |
| 2193 | + url = TAXONOMY_CREATE_IMPORT_URL |
| 2194 | + file = SimpleUploadedFile( |
| 2195 | + "taxonomy.csv", |
| 2196 | + b"id,value\ntag_1,Tag 1\n", |
| 2197 | + content_type="text/csv", |
| 2198 | + ) |
| 2199 | + resp = self.authorized_client.post( |
| 2200 | + url, |
| 2201 | + {"taxonomy_name": "Imported", "taxonomy_description": "", "file": file}, |
| 2202 | + format="multipart", |
| 2203 | + ) |
| 2204 | + self.assertEqual(resp.status_code, status.HTTP_201_CREATED) |
| 2205 | + |
| 2206 | + def test_create_import_unauthorized(self): |
| 2207 | + """Unauthorized user cannot create a taxonomy via import.""" |
| 2208 | + url = TAXONOMY_CREATE_IMPORT_URL |
| 2209 | + file = SimpleUploadedFile( |
| 2210 | + "taxonomy.csv", |
| 2211 | + b"id,value\ntag_1,Tag 1\n", |
| 2212 | + content_type="text/csv", |
| 2213 | + ) |
| 2214 | + resp = self.unauthorized_client.post( |
| 2215 | + url, |
| 2216 | + {"taxonomy_name": "Imported", "taxonomy_description": "", "file": file}, |
| 2217 | + format="multipart", |
| 2218 | + ) |
| 2219 | + self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN) |
| 2220 | + |
| 2221 | + def test_taxonomy_list_permission_fields_authorized(self): |
| 2222 | + """Authorized user sees correct permission fields in taxonomy list.""" |
| 2223 | + url = TAXONOMY_ORG_LIST_URL |
| 2224 | + resp = self.authorized_client.get(url) |
| 2225 | + self.assertEqual(resp.status_code, status.HTTP_200_OK) |
| 2226 | + for taxonomy in resp.data["results"]: |
| 2227 | + self.assertTrue(taxonomy["can_change_taxonomy"]) |
| 2228 | + self.assertTrue(taxonomy["can_delete_taxonomy"]) |
| 2229 | + self.assertTrue(taxonomy["can_tag_object"]) |
| 2230 | + |
| 2231 | + def test_taxonomy_list_permission_fields_unauthorized(self): |
| 2232 | + """Unauthorized user sees restricted permission fields in taxonomy list.""" |
| 2233 | + url = TAXONOMY_ORG_LIST_URL |
| 2234 | + resp = self.unauthorized_client.get(url) |
| 2235 | + self.assertEqual(resp.status_code, status.HTTP_200_OK) |
| 2236 | + for taxonomy in resp.data["results"]: |
| 2237 | + self.assertFalse(taxonomy["can_change_taxonomy"]) |
| 2238 | + self.assertFalse(taxonomy["can_delete_taxonomy"]) |
| 2239 | + self.assertFalse(taxonomy["can_tag_object"]) |
| 2240 | + |
| 2241 | + |
| 2242 | +@skip_unless_cms |
| 2243 | +class TestObjectTagUpdateWithAuthz(CourseAuthzTestMixin, SharedModuleStoreTestCase, APITestCase): |
| 2244 | + """ |
| 2245 | + Tests object tag update endpoint with openedx-authz. |
| 2246 | +
|
| 2247 | + When the AUTHZ_COURSE_AUTHORING_FLAG is enabled for a course, |
| 2248 | + PUT /object_tags/{course_id}/ should enforce courses.manage_tags. |
| 2249 | + """ |
| 2250 | + |
| 2251 | + authz_roles_to_assign = [COURSE_STAFF.external_key] |
| 2252 | + |
| 2253 | + @classmethod |
| 2254 | + def setUpClass(cls): |
| 2255 | + super().setUpClass() |
| 2256 | + cls.password = 'test' |
| 2257 | + cls.course = CourseFactory.create() |
| 2258 | + cls.course_key = cls.course.id |
| 2259 | + cls.staff = StaffFactory(course_key=cls.course_key, password=cls.password) |
| 2260 | + |
| 2261 | + def setUp(self): |
| 2262 | + super().setUp() |
| 2263 | + self.taxonomy = tagging_api.create_taxonomy(name="Test Taxonomy") |
| 2264 | + tagging_api.set_taxonomy_orgs(self.taxonomy, all_orgs=True, orgs=[]) |
| 2265 | + Tag.objects.create(taxonomy=self.taxonomy, value="Tag 1") |
| 2266 | + |
| 2267 | + def test_update_object_tags_authorized(self): |
| 2268 | + """Authorized user can update object tags.""" |
| 2269 | + url = OBJECT_TAG_UPDATE_URL.format(object_id=self.course_key) |
| 2270 | + resp = self.authorized_client.put( |
| 2271 | + url, |
| 2272 | + {"tagsData": [{"taxonomy": self.taxonomy.id, "tags": ["Tag 1"]}]}, |
| 2273 | + format="json", |
| 2274 | + ) |
| 2275 | + self.assertEqual(resp.status_code, status.HTTP_200_OK) |
| 2276 | + |
| 2277 | + def test_update_object_tags_unauthorized(self): |
| 2278 | + """Unauthorized user cannot update object tags.""" |
| 2279 | + url = OBJECT_TAG_UPDATE_URL.format(object_id=self.course_key) |
| 2280 | + resp = self.unauthorized_client.put( |
| 2281 | + url, |
| 2282 | + {"tagsData": [{"taxonomy": self.taxonomy.id, "tags": ["Tag 1"]}]}, |
| 2283 | + format="json", |
| 2284 | + ) |
| 2285 | + self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN) |
| 2286 | + |
| 2287 | + def test_update_object_tags_scoped_to_course(self): |
| 2288 | + """Authorization should only apply to the assigned course.""" |
| 2289 | + other_course = self.store.create_course("OtherOrg", "OtherCourse", "Run", self.staff.id) |
| 2290 | + url = OBJECT_TAG_UPDATE_URL.format(object_id=other_course.id) |
| 2291 | + resp = self.authorized_client.put( |
| 2292 | + url, |
| 2293 | + {"tagsData": [{"taxonomy": self.taxonomy.id, "tags": ["Tag 1"]}]}, |
| 2294 | + format="json", |
| 2295 | + ) |
| 2296 | + self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN) |
| 2297 | + |
| 2298 | + |
2139 | 2299 | @skip_unless_cms |
2140 | 2300 | @ddt.ddt |
2141 | 2301 | class TestDownloadTemplateView(APITestCase): |
|
0 commit comments