diff --git a/openedx/core/djangoapps/content_libraries/api/libraries.py b/openedx/core/djangoapps/content_libraries/api/libraries.py index c67f8afc4c..8d32e4dbc0 100644 --- a/openedx/core/djangoapps/content_libraries/api/libraries.py +++ b/openedx/core/djangoapps/content_libraries/api/libraries.py @@ -70,6 +70,7 @@ from openedx_learning.api.authoring_models import Component, LearningPackage 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 @@ -107,6 +108,7 @@ __all__ = [ "publish_changes", "revert_changes", "get_backup_task_status", + "assign_library_role_to_user", ] @@ -155,6 +157,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: """ @@ -518,6 +526,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 317b494f9d..9f6cca1994 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 @@ class LibraryRootView(GenericAPIView): 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 670d630e5a..1c78597db9 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 @@ from openedx_events.content_authoring.signals import ( 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 @@ class ContentLibraryExportTest(ContentLibrariesRestApiTest): 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 368f8fa811..28ebe29f5c 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 46af782625..76f31553d2 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 @@ -169,6 +174,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 @@ -228,6 +234,7 @@ django==5.2.7 # help-tokens # jsonfield # lti-consumer-xblock + # openedx-authz # openedx-django-pyfs # openedx-django-wiki # openedx-events @@ -388,6 +395,7 @@ djangorestframework==3.16.1 # edx-organizations # edx-proctoring # edx-submissions + # openedx-authz # openedx-forum # openedx-learning # ora2 @@ -412,6 +420,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 @@ -470,6 +479,7 @@ edx-drf-extensions==10.6.0 # edx-when # edxval # enterprise-integrated-channels + # openedx-authz # openedx-learning edx-enterprise==6.5.1 # via @@ -502,6 +512,7 @@ edx-opaque-keys[django]==3.0.0 # edx-when # enterprise-integrated-channels # lti-consumer-xblock + # openedx-authz # openedx-events # openedx-filters # ora2 @@ -812,7 +823,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 @@ -909,6 +923,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 @@ -1085,6 +1103,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 @@ -1216,6 +1236,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 20636e25c5..6b4d0c201f 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 @@ -333,6 +344,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 @@ -395,6 +407,7 @@ django==5.2.7 # help-tokens # jsonfield # lti-consumer-xblock + # openedx-authz # openedx-django-pyfs # openedx-django-wiki # openedx-events @@ -621,6 +634,7 @@ djangorestframework==3.16.1 # edx-organizations # edx-proctoring # edx-submissions + # openedx-authz # openedx-forum # openedx-learning # ora2 @@ -671,6 +685,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 @@ -743,6 +758,7 @@ edx-drf-extensions==10.6.0 # edx-when # edxval # enterprise-integrated-channels + # openedx-authz # openedx-learning edx-enterprise==6.5.1 # via @@ -788,6 +804,7 @@ edx-opaque-keys[django]==3.0.0 # edx-when # enterprise-integrated-channels # lti-consumer-xblock + # openedx-authz # openedx-events # openedx-filters # ora2 @@ -1352,7 +1369,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 @@ -1534,6 +1556,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 @@ -1885,6 +1913,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 @@ -2190,6 +2223,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 7569e2fee0..84206df256 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 @@ -227,6 +236,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 @@ -286,6 +296,7 @@ django==5.2.7 # help-tokens # jsonfield # lti-consumer-xblock + # openedx-authz # openedx-django-pyfs # openedx-django-wiki # openedx-events @@ -460,6 +471,7 @@ djangorestframework==3.16.1 # edx-organizations # edx-proctoring # edx-submissions + # openedx-authz # openedx-forum # openedx-learning # ora2 @@ -496,6 +508,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 @@ -554,6 +567,7 @@ edx-drf-extensions==10.6.0 # edx-when # edxval # enterprise-integrated-channels + # openedx-authz # openedx-learning edx-enterprise==6.5.1 # via @@ -586,6 +600,7 @@ edx-opaque-keys[django]==3.0.0 # edx-when # enterprise-integrated-channels # lti-consumer-xblock + # openedx-authz # openedx-events # openedx-filters # ora2 @@ -985,7 +1000,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 @@ -1105,6 +1123,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 @@ -1329,6 +1352,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 @@ -1537,6 +1564,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 2b043f71dd..de2667f284 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 2e10cf2b9b..49403f2582 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 @@ -252,6 +261,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 @@ -311,6 +321,7 @@ django==5.2.7 # help-tokens # jsonfield # lti-consumer-xblock + # openedx-authz # openedx-django-pyfs # openedx-django-wiki # openedx-events @@ -485,6 +496,7 @@ djangorestframework==3.16.1 # edx-organizations # edx-proctoring # edx-submissions + # openedx-authz # openedx-forum # openedx-learning # ora2 @@ -516,6 +528,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 @@ -574,6 +587,7 @@ edx-drf-extensions==10.6.0 # edx-when # edxval # enterprise-integrated-channels + # openedx-authz # openedx-learning edx-enterprise==6.5.1 # via @@ -608,6 +622,7 @@ edx-opaque-keys[django]==3.0.0 # edx-when # enterprise-integrated-channels # lti-consumer-xblock + # openedx-authz # openedx-events # openedx-filters # ora2 @@ -1029,7 +1044,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 @@ -1167,6 +1185,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 @@ -1437,6 +1460,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 @@ -1621,6 +1648,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 dec15874f7..c6158d38e9 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