diff --git a/openedx/core/djangoapps/content_libraries/api/libraries.py b/openedx/core/djangoapps/content_libraries/api/libraries.py index 11e9d25fb97c..abee78e10b41 100644 --- a/openedx/core/djangoapps/content_libraries/api/libraries.py +++ b/openedx/core/djangoapps/content_libraries/api/libraries.py @@ -65,6 +65,7 @@ from organizations.models import Organization from user_tasks.models import UserTaskArtifact, UserTaskStatus from xblock.core import XBlock +from openedx_authz.api import assign_role_to_user_in_scope from openedx.core.types import User as UserType @@ -102,6 +103,7 @@ "publish_changes", "revert_changes", "get_backup_task_status", + "assign_library_role_to_user", ] @@ -150,6 +152,12 @@ class AccessLevel: NO_ACCESS = None +ACCESS_LEVEL_TO_LIBRARY_ROLE = { + AccessLevel.ADMIN_LEVEL: "library_admin", + AccessLevel.AUTHOR_LEVEL: "library_author", +} + + @dataclass(frozen=True) class ContentLibraryPermissionEntry: """ @@ -512,6 +520,30 @@ def set_library_user_permissions(library_key: LibraryLocatorV2, user: UserType, ) +def assign_library_role_to_user(library_key: LibraryLocatorV2, user: UserType, access_level: str): + """Grant a role to the specified user for this library. + + Args: + library_key (LibraryLocatorV2): The key of the content library. + user (UserType): The user to whom the role will be granted. + access_level (str | None): The access level to be granted. This access level maps to a specific role. + + Raises: + TypeError: If the user is an instance of AnonymousUser. + """ + if isinstance(user, AnonymousUser): + raise TypeError("Invalid user type") + + role = ACCESS_LEVEL_TO_LIBRARY_ROLE.get(access_level) + if role is None: + raise ValueError(f"Invalid access level: {access_level}") + + if assign_role_to_user_in_scope(user.username, role, str(library_key)): + log.info(f"Assigned role '{role}' to user '{user.username}' for library '{library_key}'") + else: + log.warning(f"Failed to assign role '{role}' to user '{user.username}' for library '{library_key}'") + + def set_library_group_permissions(library_key: LibraryLocatorV2, group, access_level: str): """ Change the specified group's level of access to this library. diff --git a/openedx/core/djangoapps/content_libraries/rest_api/libraries.py b/openedx/core/djangoapps/content_libraries/rest_api/libraries.py index 317b494f9dfb..9f6cca19947a 100644 --- a/openedx/core/djangoapps/content_libraries/rest_api/libraries.py +++ b/openedx/core/djangoapps/content_libraries/rest_api/libraries.py @@ -253,6 +253,12 @@ def post(self, request): result = api.create_library(org=org, **data) # Grant the current user admin permissions on the library: api.set_library_user_permissions(result.key, request.user, api.AccessLevel.ADMIN_LEVEL) + + # Grant the current user the library admin role for this library. + # Other role assignments are handled by openedx-authz and the Console MFE. + # This ensures the creator has access to new libraries. From the library views, + # users can then manage roles for others. + api.assign_library_role_to_user(result.key, request.user, api.AccessLevel.ADMIN_LEVEL) except api.LibraryAlreadyExists: raise ValidationError(detail={"slug": "A library with that ID already exists."}) # lint-amnesty, pylint: disable=raise-missing-from diff --git a/openedx/core/djangoapps/content_libraries/tests/test_api.py b/openedx/core/djangoapps/content_libraries/tests/test_api.py index 670d630e5a3d..1c78597db970 100644 --- a/openedx/core/djangoapps/content_libraries/tests/test_api.py +++ b/openedx/core/djangoapps/content_libraries/tests/test_api.py @@ -28,8 +28,10 @@ LIBRARY_COLLECTION_UPDATED, LIBRARY_CONTAINER_UPDATED, ) +from openedx_authz.api.users import get_user_role_assignments_in_scope from openedx_learning.api import authoring as authoring_api +from common.djangoapps.student.tests.factories import UserFactory from .. import api from ..models import ContentLibrary from .base import ContentLibrariesRestApiTest @@ -1479,3 +1481,126 @@ def test_get_backup_task_status_failed(self) -> None: assert status is not None assert status['state'] == UserTaskStatus.FAILED assert status['file'] is None + + +class ContentLibraryAuthZRoleAssignmentTest(ContentLibrariesRestApiTest): + """ + Tests for Content Library role assignment via the AuthZ Authorization Framework. + + These tests verify that library roles are correctly assigned to users through + the openedx-authz (AuthZ) Authorization Framework when libraries are created or when + explicit role assignments are made. + + See: https://github.com/openedx/openedx-authz/ + """ + + def setUp(self) -> None: + super().setUp() + + # Create Content Libraries + self._create_library("test-lib-role-1", "Test Library Role 1") + + # Fetch the created ContentLibrary objects so we can access their learning_package.id + self.lib1 = ContentLibrary.objects.get(slug="test-lib-role-1") + + def test_assign_library_admin_role_to_user_via_authz(self) -> None: + """ + Test assigning a library admin role to a user via the AuthZ Authorization Framework. + + This test verifies that the openedx-authz Authorization Framework correctly + assigns the library_admin role to a user when explicitly called. + """ + api.assign_library_role_to_user(self.lib1.library_key, self.user, api.AccessLevel.ADMIN_LEVEL) + + roles = get_user_role_assignments_in_scope(self.user.username, str(self.lib1.library_key)) + assert len(roles) == 1 + assert "library_admin" in repr(roles[0].roles[0]) + + def test_assign_library_author_role_to_user_via_authz(self) -> None: + """ + Test assigning a library author role to a user via the AuthZ Authorization Framework. + + This test verifies that the openedx-authz Authorization Framework correctly + assigns the library_author role to a user when explicitly called. + """ + # Create a new user to avoid conflicts with roles assigned during library creation + author_user = UserFactory.create(username="Author", email="author@example.com") + + api.assign_library_role_to_user(self.lib1.library_key, author_user, api.AccessLevel.AUTHOR_LEVEL) + + roles = get_user_role_assignments_in_scope(author_user.username, str(self.lib1.library_key)) + assert len(roles) == 1 + assert "library_author" in repr(roles[0].roles[0]) + + @mock.patch("openedx.core.djangoapps.content_libraries.api.libraries.assign_role_to_user_in_scope") + def test_library_creation_assigns_admin_role_via_authz( + self, + mock_assign_role + ) -> None: + """ + Test that creating a library via REST API assigns admin role via AuthZ. + + This test verifies that when a library is created via the REST API, + the creator is automatically assigned the library_admin role through + the openedx-authz Authorization Framework. + """ + mock_assign_role.return_value = True + + # Create a new library (this should trigger role assignment in the REST API) + self._create_library("test-lib-role-2", "Test Library Role 2") + + # Verify that assign_role_to_user_in_scope was called + mock_assign_role.assert_called_once() + call_args = mock_assign_role.call_args + assert call_args[0][0] == self.user.username # username + assert call_args[0][1] == "library_admin" # role + assert "test-lib-role-2" in call_args[0][2] # library_key (contains slug) + + @mock.patch("openedx.core.djangoapps.content_libraries.api.libraries.assign_role_to_user_in_scope") + def test_library_creation_handles_authz_failure_gracefully( + self, + mock_assign_role + ) -> None: + """ + Test that library creation succeeds even if AuthZ role assignment fails. + + This test verifies that if the openedx-authz Authorization Framework fails to assign + a role (returns False), the library creation still succeeds. This ensures that + the system degrades gracefully and doesn't break library creation if there are + issues with the Authorization Framework. + """ + # Simulate openedx-authz failing to assign the role + mock_assign_role.return_value = False + + # Library creation should still succeed + result = self._create_library("test-lib-role-3", "Test Library Role 3") + assert result is not None + assert result["slug"] == "test-lib-role-3" + + # Verify that the library was created successfully + lib3 = ContentLibrary.objects.get(slug="test-lib-role-3") + assert lib3 is not None + assert lib3.slug == "test-lib-role-3" + + @mock.patch("openedx.core.djangoapps.content_libraries.api.libraries.assign_role_to_user_in_scope") + def test_library_creation_handles_authz_exception( + self, + mock_assign_role + ) -> None: + """ + Test that library creation succeeds even if AuthZ raises an exception. + + This test verifies that if the openedx-authz Authorization Framework raises an + exception during role assignment, the library creation still succeeds. This ensures + robust error handling when the Authorization Framework is unavailable or misconfigured. + """ + # Simulate openedx-authz raising an exception for unknown issues + mock_assign_role.side_effect = Exception("AuthZ unavailable") + + # Library creation should still succeed (the exception should be caught/handled) + # Note: Currently, the code doesn't catch this exception, so we expect it to propagate. + # This test documents the current behavior and can be updated if error handling is added. + with self.assertRaises(Exception) as context: + self._create_library("test-lib-role-4", "Test Library Role 4") + + assert "AuthZ unavailable" in str(context.exception) diff --git a/requirements/common_constraints.txt b/requirements/common_constraints.txt index 368f8fa81166..28ebe29f5cc9 100644 --- a/requirements/common_constraints.txt +++ b/requirements/common_constraints.txt @@ -22,3 +22,10 @@ # elastic search changelog: https://www.elastic.co/guide/en/enterprise-search/master/release-notes-7.14.0.html # See https://github.com/openedx/edx-platform/issues/35126 for more info elasticsearch<7.14.0 + +# pip 25.3 is incompatible with pip-tools hence causing failures during the build process +# Make upgrade command and all requirements upgrade jobs are broken due to this. +# See issue https://github.com/openedx/public-engineering/issues/440 for details regarding the ongoing fix. +# The constraint can be removed once a release (pip-tools > 7.5.1) is available with support for pip 25.3 +# Issue to track this dependency and unpin later on: https://github.com/openedx/edx-lint/issues/503 +pip<25.3 diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 031765bd5adc..466d2e91c637 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -40,6 +40,7 @@ attrs==25.4.0 # edx-ace # jsonschema # lti-consumer-xblock + # openedx-authz # openedx-events # openedx-learning # referencing @@ -81,6 +82,8 @@ botocore==1.40.57 # boto3 # s3transfer # snowflake-connector-python +bracex==2.6 + # via wcmatch bridgekeeper==0.9 # via -r requirements/edx/kernel.in cachecontrol==0.14.3 @@ -91,6 +94,8 @@ cachetools==6.2.1 # google-auth camel-converter[pydantic]==5.0.0 # via meilisearch +casbin-django-orm-adapter==1.7.0 + # via openedx-authz celery==5.5.3 # via # -c requirements/constraints.txt @@ -170,6 +175,7 @@ django==5.2.7 # via # -c requirements/constraints.txt # -r requirements/edx/kernel.in + # casbin-django-orm-adapter # django-appconf # django-autocomplete-light # django-celery-results @@ -229,6 +235,7 @@ django==5.2.7 # help-tokens # jsonfield # lti-consumer-xblock + # openedx-authz # openedx-django-pyfs # openedx-django-wiki # openedx-events @@ -389,6 +396,7 @@ djangorestframework==3.16.1 # edx-organizations # edx-proctoring # edx-submissions + # openedx-authz # openedx-forum # openedx-learning # ora2 @@ -413,6 +421,7 @@ edx-api-doc-tools==2.1.0 # via # -r requirements/edx/kernel.in # edx-name-affirmation + # openedx-authz edx-auth-backends==4.6.2 # via -r requirements/edx/kernel.in edx-bulk-grades==1.2.0 @@ -471,6 +480,7 @@ edx-drf-extensions==10.6.0 # edx-when # edxval # enterprise-integrated-channels + # openedx-authz # openedx-learning edx-enterprise==6.5.1 # via @@ -503,6 +513,7 @@ edx-opaque-keys[django]==3.0.0 # edx-when # enterprise-integrated-channels # lti-consumer-xblock + # openedx-authz # openedx-events # openedx-filters # ora2 @@ -813,7 +824,10 @@ openedx-atlas==0.7.0 # via # -r requirements/edx/kernel.in # enterprise-integrated-channels + # openedx-authz # openedx-forum +openedx-authz==0.11.1 + # via -r requirements/edx/kernel.in openedx-calc==4.0.2 # via -r requirements/edx/kernel.in openedx-django-pyfs==3.8.0 @@ -910,6 +924,10 @@ pyasn1==0.6.1 # rsa pyasn1-modules==0.4.2 # via google-auth +pycasbin==2.4.0 + # via + # casbin-django-orm-adapter + # openedx-authz pycountry==24.6.1 # via -r requirements/edx/kernel.in pycparser==2.23 @@ -1086,6 +1104,8 @@ semantic-version==2.10.0 # via edx-drf-extensions shapely==2.1.2 # via -r requirements/edx/kernel.in +simpleeval==1.0.3 + # via pycasbin simplejson==3.20.2 # via # -r requirements/edx/kernel.in @@ -1218,6 +1238,8 @@ voluptuous==0.15.2 # via ora2 walrus==0.9.5 # via edx-event-bus-redis +wcmatch==10.1 + # via pycasbin wcwidth==0.2.14 # via prompt-toolkit web-fragments==3.1.0 diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index ce956a768cc0..062c7ceb6345 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -89,6 +89,7 @@ attrs==25.4.0 # edx-ace # jsonschema # lti-consumer-xblock + # openedx-authz # openedx-events # openedx-learning # referencing @@ -151,6 +152,11 @@ botocore==1.40.57 # boto3 # s3transfer # snowflake-connector-python +bracex==2.6 + # via + # -r requirements/edx/doc.txt + # -r requirements/edx/testing.txt + # wcmatch bridgekeeper==0.9 # via # -r requirements/edx/doc.txt @@ -176,6 +182,11 @@ camel-converter[pydantic]==5.0.0 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # meilisearch +casbin-django-orm-adapter==1.7.0 + # via + # -r requirements/edx/doc.txt + # -r requirements/edx/testing.txt + # openedx-authz celery==5.5.3 # via # -c requirements/constraints.txt @@ -334,6 +345,7 @@ django==5.2.7 # -c requirements/constraints.txt # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt + # casbin-django-orm-adapter # django-appconf # django-autocomplete-light # django-celery-results @@ -396,6 +408,7 @@ django==5.2.7 # help-tokens # jsonfield # lti-consumer-xblock + # openedx-authz # openedx-django-pyfs # openedx-django-wiki # openedx-events @@ -622,6 +635,7 @@ djangorestframework==3.16.1 # edx-organizations # edx-proctoring # edx-submissions + # openedx-authz # openedx-forum # openedx-learning # ora2 @@ -672,6 +686,7 @@ edx-api-doc-tools==2.1.0 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # edx-name-affirmation + # openedx-authz edx-auth-backends==4.6.2 # via # -r requirements/edx/doc.txt @@ -744,6 +759,7 @@ edx-drf-extensions==10.6.0 # edx-when # edxval # enterprise-integrated-channels + # openedx-authz # openedx-learning edx-enterprise==6.5.1 # via @@ -789,6 +805,7 @@ edx-opaque-keys[django]==3.0.0 # edx-when # enterprise-integrated-channels # lti-consumer-xblock + # openedx-authz # openedx-events # openedx-filters # ora2 @@ -1353,7 +1370,12 @@ openedx-atlas==0.7.0 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # enterprise-integrated-channels + # openedx-authz # openedx-forum +openedx-authz==0.11.1 + # via + # -r requirements/edx/doc.txt + # -r requirements/edx/testing.txt openedx-calc==4.0.2 # via # -r requirements/edx/doc.txt @@ -1535,6 +1557,12 @@ pyasn1-modules==0.4.2 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # google-auth +pycasbin==2.4.0 + # via + # -r requirements/edx/doc.txt + # -r requirements/edx/testing.txt + # casbin-django-orm-adapter + # openedx-authz pycodestyle==2.8.0 # via # -c requirements/constraints.txt @@ -1886,6 +1914,11 @@ shapely==2.1.2 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt +simpleeval==1.0.3 + # via + # -r requirements/edx/doc.txt + # -r requirements/edx/testing.txt + # pycasbin simplejson==3.20.2 # via # -r requirements/edx/doc.txt @@ -2192,6 +2225,11 @@ walrus==0.9.5 # edx-event-bus-redis watchdog==6.0.0 # via -r requirements/edx/development.in +wcmatch==10.1 + # via + # -r requirements/edx/doc.txt + # -r requirements/edx/testing.txt + # pycasbin wcwidth==0.2.14 # via # -r requirements/edx/doc.txt diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index fbcfca89ca12..cd0ab7823c45 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -64,6 +64,7 @@ attrs==25.4.0 # edx-ace # jsonschema # lti-consumer-xblock + # openedx-authz # openedx-events # openedx-learning # referencing @@ -116,6 +117,10 @@ botocore==1.40.57 # boto3 # s3transfer # snowflake-connector-python +bracex==2.6 + # via + # -r requirements/edx/base.txt + # wcmatch bridgekeeper==0.9 # via -r requirements/edx/base.txt cachecontrol==0.14.3 @@ -131,6 +136,10 @@ camel-converter[pydantic]==5.0.0 # via # -r requirements/edx/base.txt # meilisearch +casbin-django-orm-adapter==1.7.0 + # via + # -r requirements/edx/base.txt + # openedx-authz celery==5.5.3 # via # -c requirements/constraints.txt @@ -228,6 +237,7 @@ django==5.2.7 # via # -c requirements/constraints.txt # -r requirements/edx/base.txt + # casbin-django-orm-adapter # django-appconf # django-autocomplete-light # django-celery-results @@ -287,6 +297,7 @@ django==5.2.7 # help-tokens # jsonfield # lti-consumer-xblock + # openedx-authz # openedx-django-pyfs # openedx-django-wiki # openedx-events @@ -461,6 +472,7 @@ djangorestframework==3.16.1 # edx-organizations # edx-proctoring # edx-submissions + # openedx-authz # openedx-forum # openedx-learning # ora2 @@ -497,6 +509,7 @@ edx-api-doc-tools==2.1.0 # via # -r requirements/edx/base.txt # edx-name-affirmation + # openedx-authz edx-auth-backends==4.6.2 # via -r requirements/edx/base.txt edx-bulk-grades==1.2.0 @@ -555,6 +568,7 @@ edx-drf-extensions==10.6.0 # edx-when # edxval # enterprise-integrated-channels + # openedx-authz # openedx-learning edx-enterprise==6.5.1 # via @@ -587,6 +601,7 @@ edx-opaque-keys[django]==3.0.0 # edx-when # enterprise-integrated-channels # lti-consumer-xblock + # openedx-authz # openedx-events # openedx-filters # ora2 @@ -986,7 +1001,10 @@ openedx-atlas==0.7.0 # via # -r requirements/edx/base.txt # enterprise-integrated-channels + # openedx-authz # openedx-forum +openedx-authz==0.11.1 + # via -r requirements/edx/base.txt openedx-calc==4.0.2 # via -r requirements/edx/base.txt openedx-django-pyfs==3.8.0 @@ -1106,6 +1124,11 @@ pyasn1-modules==0.4.2 # via # -r requirements/edx/base.txt # google-auth +pycasbin==2.4.0 + # via + # -r requirements/edx/base.txt + # casbin-django-orm-adapter + # openedx-authz pycountry==24.6.1 # via -r requirements/edx/base.txt pycparser==2.23 @@ -1330,6 +1353,10 @@ semantic-version==2.10.0 # edx-drf-extensions shapely==2.1.2 # via -r requirements/edx/base.txt +simpleeval==1.0.3 + # via + # -r requirements/edx/base.txt + # pycasbin simplejson==3.20.2 # via # -r requirements/edx/base.txt @@ -1539,6 +1566,10 @@ walrus==0.9.5 # via # -r requirements/edx/base.txt # edx-event-bus-redis +wcmatch==10.1 + # via + # -r requirements/edx/base.txt + # pycasbin wcwidth==0.2.14 # via # -r requirements/edx/base.txt diff --git a/requirements/edx/kernel.in b/requirements/edx/kernel.in index 2b043f71dd79..de2667f28438 100644 --- a/requirements/edx/kernel.in +++ b/requirements/edx/kernel.in @@ -161,3 +161,4 @@ wrapt # Better functools.wrapped. TODO: functools XBlock[django] # Courseware component architecture xss-utils # https://github.com/openedx/edx-platform/pull/20633 Fix XSS via Translations unicodeit # Converts mathjax equation to plain text by using unicode symbols +openedx-authz # Authorization Framework for the Open edX Ecosystem diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index 60a60b7014ea..ebf3c5ec4e49 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -63,6 +63,7 @@ attrs==25.4.0 # edx-ace # jsonschema # lti-consumer-xblock + # openedx-authz # openedx-events # openedx-learning # referencing @@ -113,6 +114,10 @@ botocore==1.40.57 # boto3 # s3transfer # snowflake-connector-python +bracex==2.6 + # via + # -r requirements/edx/base.txt + # wcmatch bridgekeeper==0.9 # via -r requirements/edx/base.txt cachecontrol==0.14.3 @@ -129,6 +134,10 @@ camel-converter[pydantic]==5.0.0 # via # -r requirements/edx/base.txt # meilisearch +casbin-django-orm-adapter==1.7.0 + # via + # -r requirements/edx/base.txt + # openedx-authz celery==5.5.3 # via # -c requirements/constraints.txt @@ -253,6 +262,7 @@ django==5.2.7 # via # -c requirements/constraints.txt # -r requirements/edx/base.txt + # casbin-django-orm-adapter # django-appconf # django-autocomplete-light # django-celery-results @@ -312,6 +322,7 @@ django==5.2.7 # help-tokens # jsonfield # lti-consumer-xblock + # openedx-authz # openedx-django-pyfs # openedx-django-wiki # openedx-events @@ -486,6 +497,7 @@ djangorestframework==3.16.1 # edx-organizations # edx-proctoring # edx-submissions + # openedx-authz # openedx-forum # openedx-learning # ora2 @@ -517,6 +529,7 @@ edx-api-doc-tools==2.1.0 # via # -r requirements/edx/base.txt # edx-name-affirmation + # openedx-authz edx-auth-backends==4.6.2 # via -r requirements/edx/base.txt edx-bulk-grades==1.2.0 @@ -575,6 +588,7 @@ edx-drf-extensions==10.6.0 # edx-when # edxval # enterprise-integrated-channels + # openedx-authz # openedx-learning edx-enterprise==6.5.1 # via @@ -609,6 +623,7 @@ edx-opaque-keys[django]==3.0.0 # edx-when # enterprise-integrated-channels # lti-consumer-xblock + # openedx-authz # openedx-events # openedx-filters # ora2 @@ -1030,7 +1045,10 @@ openedx-atlas==0.7.0 # via # -r requirements/edx/base.txt # enterprise-integrated-channels + # openedx-authz # openedx-forum +openedx-authz==0.11.1 + # via -r requirements/edx/base.txt openedx-calc==4.0.2 # via -r requirements/edx/base.txt openedx-django-pyfs==3.8.0 @@ -1168,6 +1186,11 @@ pyasn1-modules==0.4.2 # via # -r requirements/edx/base.txt # google-auth +pycasbin==2.4.0 + # via + # -r requirements/edx/base.txt + # casbin-django-orm-adapter + # openedx-authz pycodestyle==2.8.0 # via # -c requirements/constraints.txt @@ -1438,6 +1461,10 @@ semantic-version==2.10.0 # edx-drf-extensions shapely==2.1.2 # via -r requirements/edx/base.txt +simpleeval==1.0.3 + # via + # -r requirements/edx/base.txt + # pycasbin simplejson==3.20.2 # via # -r requirements/edx/base.txt @@ -1623,6 +1650,10 @@ walrus==0.9.5 # via # -r requirements/edx/base.txt # edx-event-bus-redis +wcmatch==10.1 + # via + # -r requirements/edx/base.txt + # pycasbin wcwidth==0.2.14 # via # -r requirements/edx/base.txt diff --git a/requirements/pip.txt b/requirements/pip.txt index dec15874f740..c6158d38e981 100644 --- a/requirements/pip.txt +++ b/requirements/pip.txt @@ -9,6 +9,8 @@ wheel==0.45.1 # The following packages are considered to be unsafe in a requirements file: pip==25.2 - # via -r requirements/pip.in + # via + # -c requirements/common_constraints.txt + # -r requirements/pip.in setuptools==80.9.0 # via -r requirements/pip.in