Skip to content

Commit 63668a5

Browse files
Merge branch 'master' into marslan/10083-export-content-libraries-git
2 parents 233c49d + 110ec0c commit 63668a5

File tree

462 files changed

+6023
-49839
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

462 files changed

+6023
-49839
lines changed

.github/workflows/js-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ jobs:
7373
npm run test
7474
7575
- name: Save Job Artifacts
76-
uses: actions/upload-artifact@v6
76+
uses: actions/upload-artifact@v7
7777
with:
7878
name: Build-Artifacts
7979
path: |

.github/workflows/quality-checks.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,14 @@ jobs:
7878
PIP_SRC: ${{ runner.temp }}
7979
TARGET_BRANCH: ${{ github.base_ref }}
8080
run: |
81-
make pycodestyle
81+
ruff check --output-format=github .
8282
make xsslint
8383
make pii_check
8484
make check_keywords
8585
8686
- name: Save Job Artifacts
8787
if: always()
88-
uses: actions/upload-artifact@v6
88+
uses: actions/upload-artifact@v7
8989
with:
9090
name: Build-Artifacts
9191
path: |

.github/workflows/static-assets-check.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@ jobs:
9898
env:
9999
LMS_CFG: lms/envs/minimal.yml
100100
CMS_CFG: lms/envs/minimal.yml
101-
DJANGO_SETTINGS_MODULE: lms.envs.production
102101
run: |
103-
./manage.py lms collectstatic --noinput
104-
./manage.py cms collectstatic --noinput
102+
DJANGO_SETTINGS_MODULE=lms.envs.production ./manage.py lms collectstatic --noinput
103+
DJANGO_SETTINGS_MODULE=cms.envs.production ./manage.py cms collectstatic --noinput

.github/workflows/unit-tests.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ jobs:
131131
132132
- name: save pytest warnings json file
133133
if: success()
134-
uses: actions/upload-artifact@v6
134+
uses: actions/upload-artifact@v7
135135
with:
136136
name: pytest-warnings-json-${{ matrix.shard_name }}-${{ matrix.python-version }}-${{ matrix.django-version }}-${{ matrix.mongo-version }}-${{ matrix.os-version }}
137137
path: |
@@ -143,7 +143,7 @@ jobs:
143143
mv reports/.coverage reports/${{ matrix.shard_name }}_${{ matrix.python-version }}_${{ matrix.django-version }}_${{ matrix.mongo-version }}_${{ matrix.os-version }}.coverage
144144
145145
- name: Upload coverage
146-
uses: actions/upload-artifact@v6
146+
uses: actions/upload-artifact@v7
147147
with:
148148
name: coverage-${{ matrix.shard_name }}-${{ matrix.python-version }}-${{ matrix.django-version }}-${{ matrix.mongo-version }}-${{ matrix.os-version }}
149149
path: reports/${{ matrix.shard_name }}_${{ matrix.python-version }}_${{ matrix.django-version }}_${{ matrix.mongo-version }}_${{ matrix.os-version }}.coverage
@@ -230,7 +230,7 @@ jobs:
230230
steps:
231231
- uses: actions/checkout@v6
232232
- name: collect pytest warnings files
233-
uses: actions/download-artifact@v7
233+
uses: actions/download-artifact@v8
234234
with:
235235
pattern: pytest-warnings-json-*
236236
merge-multiple: true
@@ -245,7 +245,7 @@ jobs:
245245
246246
- name: save warning report
247247
if: success()
248-
uses: actions/upload-artifact@v6
248+
uses: actions/upload-artifact@v7
249249
with:
250250
name: pytest-warning-report-html
251251
path: |
@@ -257,14 +257,14 @@ jobs:
257257
needs: [compile-warnings-report]
258258
steps:
259259
- name: Merge Pytest Warnings JSON Artifacts
260-
uses: actions/upload-artifact/merge@v6
260+
uses: actions/upload-artifact/merge@v7
261261
with:
262262
name: pytest-warnings-json
263263
pattern: pytest-warnings-json-*
264264
delete-merged: true
265265

266266
- name: Merge Coverage Artifacts
267-
uses: actions/upload-artifact/merge@v6
267+
uses: actions/upload-artifact/merge@v7
268268
with:
269269
name: coverage
270270
pattern: coverage-*
@@ -289,7 +289,7 @@ jobs:
289289
python-version: ${{ matrix.python-version }}
290290

291291
- name: Download all artifacts
292-
uses: actions/download-artifact@v7
292+
uses: actions/download-artifact@v8
293293
with:
294294
pattern: coverage-*
295295
merge-multiple: true

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ lms/envs/private.py
1313
cms/envs/private.py
1414
.venv/
1515
CLAUDE.md
16+
.claude/
1617
AGENTS.md
1718
# end-noclean
1819

.readthedocs.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ sphinx:
1010

1111
python:
1212
install:
13+
# Need to install this to set the correct version of steuptools for now
14+
# because it is needed by fs
15+
# See https://github.com/openedx/openedx-platform/issues/38068 for details.
16+
- requirements: "requirements/pip-tools.txt"
1317
- requirements: "requirements/edx/doc.txt"
1418
- method: pip
1519
path: .

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,8 @@ xsslint: ## check xss for quality issuest
171171
--config=scripts.xsslint_config \
172172
--thresholds=scripts/xsslint_thresholds.json
173173

174-
pycodestyle: ## check python files for quality issues
175-
pycodestyle .
174+
ruff: ## check python files with ruff
175+
ruff check .
176176

177177
## Re-enable --lint flag when this issue https://github.com/openedx/edx-platform/issues/35775 is resolved
178178
pii_check: ## check django models for pii annotations

cms/djangoapps/contentstore/models.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,9 @@ class EntityLinkBase(models.Model):
9797
)
9898
# A downstream entity can only link to single upstream entity
9999
# whereas an entity can be upstream for multiple downstream entities.
100-
downstream_usage_key = UsageKeyField(max_length=255, unique=True)
100+
downstream_usage_key = UsageKeyField(unique=True)
101101
# Search by course/downstream key
102-
downstream_context_key = CourseKeyField(max_length=255, db_index=True)
102+
downstream_context_key = CourseKeyField(db_index=True)
103103
# This is present if the creation of this link is a consequence of
104104
# importing a container that has one or more levels of children.
105105
# This represents the parent (container) in the top level
@@ -152,7 +152,6 @@ class ComponentLink(EntityLinkBase):
152152
blank=True,
153153
)
154154
upstream_usage_key = UsageKeyField(
155-
max_length=255,
156155
help_text=_(
157156
"Upstream block usage key, this value cannot be null"
158157
" and useful to track upstream library blocks that do not exist yet"
@@ -324,7 +323,6 @@ class ContainerLink(EntityLinkBase):
324323
blank=True,
325324
)
326325
upstream_container_key = ContainerKeyField(
327-
max_length=255,
328326
help_text=_(
329327
"Upstream block key (e.g. lct:...), this value cannot be null "
330328
"and is useful to track upstream library blocks that do not exist yet "
@@ -564,7 +562,6 @@ class LearningContextLinksStatus(models.Model):
564562
course or a learning context.
565563
"""
566564
context_key = CourseKeyField(
567-
max_length=255,
568565
# Single entry for a learning context or course
569566
unique=True,
570567
help_text=_("Linking status for course context key"),

cms/djangoapps/contentstore/rest_api/v0/tests/test_advanced_settings.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,23 @@
22
Tests for the course advanced settings API.
33
"""
44
import json
5+
import pkg_resources
6+
from unittest.mock import patch
57

8+
import casbin
69
import ddt
710
from django.test import override_settings
811
from django.urls import reverse
912
from milestones.tests.utils import MilestonesTestCaseMixin
13+
from rest_framework.test import APIClient
1014

1115
from cms.djangoapps.contentstore.tests.utils import CourseTestCase
16+
from common.djangoapps.student.tests.factories import UserFactory
17+
from openedx.core import toggles as core_toggles
18+
from openedx_authz.api.users import assign_role_to_user_in_scope
19+
from openedx_authz.constants.roles import COURSE_STAFF
20+
from openedx_authz.engine.enforcer import AuthzEnforcer
21+
from openedx_authz.engine.utils import migrate_policy_between_enforcers
1222

1323

1424
@ddt.ddt
@@ -91,3 +101,105 @@ def test_disabled_fetch_all_query_param(self, setting, excluded_field):
91101
with override_settings(FEATURES={setting: False}):
92102
resp = self.client.get(self.url, {"fetch_all": 0})
93103
assert excluded_field not in resp.data
104+
105+
106+
@patch.object(core_toggles.AUTHZ_COURSE_AUTHORING_FLAG, 'is_enabled', return_value=True)
107+
class AdvancedSettingsAuthzTest(CourseTestCase):
108+
"""
109+
Tests for AdvancedCourseSettingsView authorization with openedx-authz.
110+
111+
These tests enable the AUTHZ_COURSE_AUTHORING_FLAG by default.
112+
"""
113+
114+
def setUp(self):
115+
super().setUp()
116+
self._seed_database_with_policies()
117+
self.url = reverse(
118+
"cms.djangoapps.contentstore:v0:course_advanced_settings",
119+
kwargs={"course_id": self.course.id},
120+
)
121+
122+
# Create test users
123+
self.authorized_user = UserFactory()
124+
self.unauthorized_user = UserFactory()
125+
126+
# Assign role to authorized user
127+
assign_role_to_user_in_scope(
128+
self.authorized_user.username,
129+
COURSE_STAFF.external_key,
130+
str(self.course.id)
131+
)
132+
AuthzEnforcer.get_enforcer().load_policy()
133+
134+
# Create API clients and force_authenticate
135+
self.authorized_client = APIClient()
136+
self.authorized_client.force_authenticate(user=self.authorized_user)
137+
self.unauthorized_client = APIClient()
138+
self.unauthorized_client.force_authenticate(user=self.unauthorized_user)
139+
140+
def tearDown(self):
141+
super().tearDown()
142+
AuthzEnforcer.get_enforcer().clear_policy()
143+
144+
@classmethod
145+
def _seed_database_with_policies(cls):
146+
"""Seed the database with policies from the policy file."""
147+
global_enforcer = AuthzEnforcer.get_enforcer()
148+
global_enforcer.load_policy()
149+
model_path = pkg_resources.resource_filename("openedx_authz.engine", "config/model.conf")
150+
policy_path = pkg_resources.resource_filename("openedx_authz.engine", "config/authz.policy")
151+
migrate_policy_between_enforcers(
152+
source_enforcer=casbin.Enforcer(model_path, policy_path),
153+
target_enforcer=global_enforcer,
154+
)
155+
156+
def test_authorized_for_specific_course(self, mock_flag):
157+
"""User authorized for specific course can access."""
158+
response = self.authorized_client.get(self.url)
159+
self.assertEqual(response.status_code, 200)
160+
161+
def test_unauthorized_for_specific_course(self, mock_flag):
162+
"""User without authorization for specific course cannot access."""
163+
response = self.unauthorized_client.get(self.url)
164+
self.assertEqual(response.status_code, 403)
165+
166+
def test_unauthorized_for_different_course(self, mock_flag):
167+
"""User authorized for one course cannot access another course."""
168+
other_course = self.store.create_course("OtherOrg", "OtherCourse", "Run", self.user.id)
169+
other_url = reverse(
170+
"cms.djangoapps.contentstore:v0:course_advanced_settings",
171+
kwargs={"course_id": other_course.id},
172+
)
173+
response = self.authorized_client.get(other_url)
174+
self.assertEqual(response.status_code, 403)
175+
176+
def test_staff_authorized_by_default(self, mock_flag):
177+
"""Staff users are authorized by default."""
178+
response = self.client.get(self.url)
179+
self.assertEqual(response.status_code, 200)
180+
181+
def test_superuser_authorized_by_default(self, mock_flag):
182+
"""Superusers are authorized by default."""
183+
superuser = UserFactory(is_superuser=True, is_staff=False)
184+
superuser_client = APIClient()
185+
superuser_client.force_authenticate(user=superuser)
186+
response = superuser_client.get(self.url)
187+
self.assertEqual(response.status_code, 200)
188+
189+
def test_patch_authorized_for_specific_course(self, mock_flag):
190+
"""User authorized for specific course can PATCH."""
191+
response = self.authorized_client.patch(
192+
self.url,
193+
{"display_name": {"value": "Test"}},
194+
content_type="application/json"
195+
)
196+
self.assertEqual(response.status_code, 200)
197+
198+
def test_patch_unauthorized_for_specific_course(self, mock_flag):
199+
"""User without authorization for specific course cannot PATCH."""
200+
response = self.unauthorized_client.patch(
201+
self.url,
202+
{"display_name": {"value": "Test"}},
203+
content_type="application/json"
204+
)
205+
self.assertEqual(response.status_code, 403)

cms/djangoapps/contentstore/rest_api/v0/views/advanced_settings.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from cms.djangoapps.models.settings.course_metadata import CourseMetadata
1313
from cms.djangoapps.contentstore.api.views.utils import get_bool_param
14-
from common.djangoapps.student.auth import has_studio_read_access, has_studio_write_access
14+
from common.djangoapps.student.auth import check_course_advanced_settings_access
1515
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, verify_course_exists, view_auth_classes
1616
from ..serializers import CourseAdvancedSettingsSerializer
1717
from ....views.course import update_course_advanced_settings
@@ -115,7 +115,7 @@ def get(self, request: Request, course_id: str):
115115
if not filter_query_data.is_valid():
116116
raise ValidationError(filter_query_data.errors)
117117
course_key = CourseKey.from_string(course_id)
118-
if not has_studio_read_access(request.user, course_key):
118+
if not check_course_advanced_settings_access(request.user, course_key, access_type='read'):
119119
self.permission_denied(request)
120120
course_block = modulestore().get_course(course_key)
121121
fetch_all = get_bool_param(request, 'fetch_all', True)
@@ -184,7 +184,7 @@ def patch(self, request: Request, course_id: str):
184184
along with all the course's settings similar to a ``GET`` request.
185185
"""
186186
course_key = CourseKey.from_string(course_id)
187-
if not has_studio_write_access(request.user, course_key):
187+
if not check_course_advanced_settings_access(request.user, course_key, access_type='write'):
188188
self.permission_denied(request)
189189
course_block = modulestore().get_course(course_key)
190190
updated_data = update_course_advanced_settings(course_block, request.data, request.user)

0 commit comments

Comments
 (0)