diff --git a/manage.py b/manage.py index f45575c0..e6954dd0 100644 --- a/manage.py +++ b/manage.py @@ -9,7 +9,7 @@ PWD = os.path.abspath(os.path.dirname(__file__)) if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_settings") + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "openedx_authz.settings.test") sys.path.append(PWD) try: from django.core.management import execute_from_command_line diff --git a/openedx_authz/__init__.py b/openedx_authz/__init__.py index 15602551..203c0264 100644 --- a/openedx_authz/__init__.py +++ b/openedx_authz/__init__.py @@ -2,4 +2,8 @@ One-line description for README and other doc files. """ +import os + __version__ = "0.1.0" + +ROOT_DIRECTORY = os.path.dirname(os.path.abspath(__file__)) diff --git a/openedx_authz/admin.py b/openedx_authz/admin.py new file mode 100644 index 00000000..1693aad3 --- /dev/null +++ b/openedx_authz/admin.py @@ -0,0 +1,8 @@ +""" +Admin for openedx_authz. +""" + +from django.contrib import admin +from .models import Library + +admin.site.register(Library) diff --git a/openedx_authz/apps.py b/openedx_authz/apps.py index eb3e05eb..8f2154b4 100644 --- a/openedx_authz/apps.py +++ b/openedx_authz/apps.py @@ -11,3 +11,70 @@ class OpenedxAuthzConfig(AppConfig): """ name = "openedx_authz" + verbose_name = "Open edX AuthZ" + default_auto_field = "django.db.models.BigAutoField" + + plugin_app = { + "url_config": { + "lms.djangoapp": { + "namespace": "openedx-authz", + "regex": r"^openedx-authz/", + "relative_path": "urls", + }, + "cms.djangoapp": { + "namespace": "openedx-authz", + "regex": r"^openedx-authz/", + "relative_path": "urls", + }, + }, + "settings_config": { + "lms.djangoapp": { + "test": {"relative_path": "settings.test"}, + "common": {"relative_path": "settings.common"}, + "production": {"relative_path": "settings.production"}, + }, + "cms.djangoapp": { + "test": {"relative_path": "settings.test"}, + "common": {"relative_path": "settings.common"}, + "production": {"relative_path": "settings.production"}, + }, + }, + } + + def ready(self): + """ + Add admin users to the authorization policy. + """ + # pylint: disable=import-outside-toplevel + from django.contrib.auth import get_user_model + + from openedx_authz.custom_enforcer import get_enforcer + + enforcer = get_enforcer() + + # Add minimum policies for anonymous users + anonymous_policies = [ + ("/", "*"), + ("/login", "*"), + ("/api/mfe_config/v1", "*"), + ("/login_refresh", "*"), + ("/csrf/api/v1/token", "*"), + ("/api/user/v2/account/login_session/", "*"), + ("/dashboard", "*"), + ("/__debug__/history_sidebar/", "*"), + ("/theming/asset/images/no_course_image.png", "*"), + ] + + for resource, action in anonymous_policies: + if not enforcer.has_policy("anonymous", resource, action): + enforcer.add_policy("anonymous", resource, action) + + # Ensure admin users have access to all resources + User = get_user_model() + + enforcer.add_policy("admin", "*", "*") + admin_users = User.objects.filter(is_staff=True, is_superuser=True) + for user in admin_users: + enforcer.add_role_for_user(user.username, "admin") + + print("\n\nAdded default policies!\n\n") diff --git a/openedx_authz/custom_enforcer.py b/openedx_authz/custom_enforcer.py new file mode 100644 index 00000000..c3b8fc46 --- /dev/null +++ b/openedx_authz/custom_enforcer.py @@ -0,0 +1,27 @@ +""" +Enforcer instance for openedx_authz. +""" + +from dauthz.core import enforcer +from redis_watcher import WatcherOptions, new_watcher + + +def callback_function(event): + """ + Callback function for the enforcer. + """ + print("\n\nUpdate for remove filtered policy callback, event: {}".format(event)) + + +def get_enforcer(): + """ + Get the enforcer instance. + """ + enforcer.enable_auto_save(True) + watcher_options = WatcherOptions() + watcher_options.host = "redis" + watcher_options.port = 6379 + watcher_options.optional_update_callback = callback_function + watcher = new_watcher(watcher_options) + enforcer.set_watcher(watcher) + return enforcer diff --git a/openedx_authz/migrations/0001_initial.py b/openedx_authz/migrations/0001_initial.py new file mode 100644 index 00000000..3c73c058 --- /dev/null +++ b/openedx_authz/migrations/0001_initial.py @@ -0,0 +1,32 @@ +# Generated by Django 4.2.23 on 2025-09-04 00:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Library", + fields=[ + ( + "id", + models.CharField( + help_text="Library ID in format lib:ORG:SLUG", max_length=255, primary_key=True, serialize=False + ), + ), + ("org", models.CharField(help_text="Organization name", max_length=255)), + ("slug", models.CharField(help_text="Library slug/identifier", max_length=255)), + ("title", models.CharField(help_text="Library title", max_length=255)), + ("description", models.TextField(blank=True, help_text="Library description")), + ], + options={ + "verbose_name": "Library", + "verbose_name_plural": "Libraries", + "ordering": ["title"], + }, + ), + ] diff --git a/openedx_authz/migrations/__init__.py b/openedx_authz/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/openedx_authz/model.conf b/openedx_authz/model.conf new file mode 100644 index 00000000..067bc87d --- /dev/null +++ b/openedx_authz/model.conf @@ -0,0 +1,14 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub) && (p.obj == '*' || r.obj == p.obj) && (p.act == '*' || regexMatch(r.act, p.act)) diff --git a/openedx_authz/models.py b/openedx_authz/models.py index 8297668b..274ec290 100644 --- a/openedx_authz/models.py +++ b/openedx_authz/models.py @@ -1,3 +1,32 @@ """ Database models for openedx_authz. """ + +from django.db import models + + +class Library(models.Model): + """ + Model representing an OpenedX Library with basic information. + """ + + id = models.CharField(max_length=255, primary_key=True, help_text="Library ID in format lib:ORG:SLUG") + org = models.CharField(max_length=255, help_text="Organization name") + slug = models.CharField(max_length=255, help_text="Library slug/identifier") + title = models.CharField(max_length=255, help_text="Library title") + description = models.TextField(blank=True, help_text="Library description") + + def save(self, *args, **kwargs): + """ + Override save method to automatically generate ID from org and slug. + """ + self.id = f"lib:{self.org}:{self.slug}".replace(" ", "_") + super().save(*args, **kwargs) + + class Meta: + verbose_name = "Library" + verbose_name_plural = "Libraries" + ordering = ["title"] + + def __str__(self): + return str(self.title) diff --git a/openedx_authz/serializers.py b/openedx_authz/serializers.py new file mode 100644 index 00000000..62a3cf6e --- /dev/null +++ b/openedx_authz/serializers.py @@ -0,0 +1,23 @@ +""" +Serializers for openedx_authz DRF API. +""" + +from rest_framework import serializers +from .models import Library + + +class LibrarySerializer(serializers.ModelSerializer): + """ + Serializer for OpenedX Library model. + """ + + class Meta: + model = Library + fields = [ + "id", + "org", + "slug", + "title", + "description", + ] + read_only_fields = ["id"] diff --git a/openedx_authz/settings/__init__.py b/openedx_authz/settings/__init__.py new file mode 100644 index 00000000..8403e6de --- /dev/null +++ b/openedx_authz/settings/__init__.py @@ -0,0 +1,3 @@ +""" +Settings package for openedx_authz plugin. +""" diff --git a/openedx_authz/settings/common.py b/openedx_authz/settings/common.py new file mode 100644 index 00000000..e8714827 --- /dev/null +++ b/openedx_authz/settings/common.py @@ -0,0 +1,49 @@ +""" +Common settings for openedx_authz plugin. +""" + +import os + +from openedx_authz import ROOT_DIRECTORY + + +def plugin_settings(settings): + """ + Configure plugin settings for Open edX. + + This function is called by the Open edX plugin system to configure + the Django settings for this plugin. + + Args: + settings: The Django settings object + """ + # Add external third-party apps to INSTALLED_APPS + external_apps = [ + "casbin_adapter.apps.CasbinAdapterConfig", # Casbin Adapter + "dauthz.apps.DauthzConfig", # Django Authorization library + ] + for app in external_apps: + if app not in settings.INSTALLED_APPS: + settings.INSTALLED_APPS.append(app) + + # Add middleware for authorization + middleware_class = "dauthz.middlewares.request_middleware.RequestMiddleware" + settings.MIDDLEWARE = settings.MIDDLEWARE + [middleware_class] + + # Add authorization configuration + settings.CASBIN_MODEL = os.path.join(ROOT_DIRECTORY, "model.conf") + settings.DAUTHZ = { + "DEFAULT": { + "MODEL": { + "CONFIG_TYPE": "file", + "CONFIG_FILE_PATH": settings.CASBIN_MODEL, + "CONFIG_TEXT": "", + }, + "ADAPTER": { + "NAME": "casbin_adapter.adapter.Adapter", + }, + "LOG": { + "ENABLED": True, + }, + }, + } diff --git a/openedx_authz/settings/production.py b/openedx_authz/settings/production.py new file mode 100644 index 00000000..4f80cf42 --- /dev/null +++ b/openedx_authz/settings/production.py @@ -0,0 +1,15 @@ +""" +Production settings for openedx_authz plugin. +""" + + +def plugin_settings(settings): + """ + Configure plugin settings for Open edX. + + This function is called by the Open edX plugin system to configure + the Django settings for this plugin. + + Args: + settings: The Django settings object + """ diff --git a/openedx_authz/settings/test.py b/openedx_authz/settings/test.py new file mode 100644 index 00000000..c4729d53 --- /dev/null +++ b/openedx_authz/settings/test.py @@ -0,0 +1,50 @@ +""" +Test settings for openedx_authz plugin. +""" + +import os + +from openedx_authz import ROOT_DIRECTORY + + +def plugin_settings(settings): + """ + Configure plugin settings for Open edX. + + This function is called by the Open edX plugin system to configure + the Django settings for this plugin. + + Args: + settings: The Django settings object + """ + # Add external third-party apps to INSTALLED_APPS + external_apps = [ + "casbin_adapter.apps.CasbinAdapterConfig", # Casbin Adapter + "dauthz.apps.DauthzConfig", # Django Authorization library + ] + + for app in external_apps: + if app not in settings.INSTALLED_APPS: + settings.INSTALLED_APPS.append(app) + + # Add middleware for authorization + middleware_class = "dauthz.middlewares.request_middleware.RequestMiddleware" + settings.MIDDLEWARE = settings.MIDDLEWARE + [middleware_class] + + # Add authorization configuration + settings.CASBIN_MODEL = os.path.join(ROOT_DIRECTORY, "model.conf") + settings.DAUTHZ = { + "DEFAULT": { + "MODEL": { + "CONFIG_TYPE": "file", + "CONFIG_FILE_PATH": settings.CASBIN_MODEL, + "CONFIG_TEXT": "", + }, + "ADAPTER": { + "NAME": "casbin_adapter.adapter.Adapter", + }, + "LOG": { + "ENABLED": True, + }, + }, + } diff --git a/openedx_authz/urls.py b/openedx_authz/urls.py index 615cef5b..dfd803b9 100644 --- a/openedx_authz/urls.py +++ b/openedx_authz/urls.py @@ -2,10 +2,17 @@ URLs for openedx_authz. """ -from django.urls import re_path # pylint: disable=unused-import -from django.views.generic import TemplateView # pylint: disable=unused-import +from django.urls import include, re_path +from rest_framework.routers import DefaultRouter + +from .views import AdminRoleAssignmentViewSet, LibraryViewSet, PolicyBulkViewSet, PolicySingleViewSet + +router = DefaultRouter() +router.register(r"libraries", LibraryViewSet, basename="library") +router.register(r"admin-roles", AdminRoleAssignmentViewSet, basename="admin-roles") +router.register(r"policy-bulk", PolicyBulkViewSet, basename="policy-bulk") +router.register(r"policy-single", PolicySingleViewSet, basename="policy-single") urlpatterns = [ - # TODO: Fill in URL patterns and views here. - # re_path(r'', TemplateView.as_view(template_name="openedx_authz/base.html")), + re_path(r"^api/", include(router.urls)), ] diff --git a/openedx_authz/views.py b/openedx_authz/views.py new file mode 100644 index 00000000..fd2a808b --- /dev/null +++ b/openedx_authz/views.py @@ -0,0 +1,261 @@ +""" +Views for openedx_authz DRF API. +""" + +from django.shortcuts import get_object_or_404 +from rest_framework import status, viewsets +from rest_framework.response import Response + +from openedx_authz.custom_enforcer import get_enforcer + +from .models import Library +from .serializers import LibrarySerializer + +enforcer = get_enforcer() + + +class LibraryViewSet(viewsets.ViewSet): + """ + A ViewSet for handling Library operations using the Library model. + Provides all HTTP methods: GET, POST, PUT, PATCH, DELETE + """ + + def list(self, request): + """ + GET /libraries/ + Return a list of all libraries. + """ + libraries = Library.objects.all() + serializer = LibrarySerializer(libraries, many=True) + return Response({"count": libraries.count(), "results": serializer.data}) + + def create(self, request): + """ + POST /libraries/ + Create a new library. + + Example request body: + + ```json + { + "title": "Title 1", + "org": "org1", + "slug": "slug1", + "description": "Description 1" + } + ``` + """ + serializer = LibrarySerializer(data=request.data) + if serializer.is_valid(): + library = serializer.save() + enforcer.add_policy( + self.request.user.username, + f"{self.request.path}{library.id}/", + "(GET)|(PUT)|(DELETE)|(PATCH)", + ) + return Response(LibrarySerializer(library).data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def retrieve(self, request, pk=None): + """ + GET /libraries/{id}/ + Retrieve a specific library by ID. + """ + library = get_object_or_404(Library, id=pk) + serializer = LibrarySerializer(library) + return Response(serializer.data) + + def update(self, request, pk=None): + """ + PUT /libraries/{id}/ + Update a library completely. + """ + library = get_object_or_404(Library, id=pk) + serializer = LibrarySerializer(library, data=request.data) + + if serializer.is_valid(): + serializer.save() + return Response(serializer.data) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def partial_update(self, request, pk=None): + """ + PATCH /libraries/{id}/ + Partially update a library. + """ + library = get_object_or_404(Library, id=pk) + serializer = LibrarySerializer(library, data=request.data, partial=True) + + if serializer.is_valid(): + serializer.save() + return Response(serializer.data) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, pk=None): + """ + DELETE /libraries/{id}/ + Delete a library. + """ + library = get_object_or_404(Library, id=pk) + library.delete() + enforcer.remove_filtered_policy(1, self.request.user.username, f"{self.request.path}{library.id}/", "") + + return Response( + {"detail": f'Library "{library}" has been deleted.'}, + status=status.HTTP_204_NO_CONTENT, + ) + + +class AdminRoleAssignmentViewSet(viewsets.ViewSet): + """ + ViewSet for managing admin role assignments using Casbin. + """ + + def create(self, request): + """ + POST /admin-roles/ + Assign admin role to a user. + + Example request body: + ```json + { + "username": "john_doe" + } + ``` + """ + username = request.data["username"] + enforcer.add_role_for_user(username, "admin") + return Response(f"Admin role assigned to user {username}", status=status.HTTP_201_CREATED) + + def destroy(self, request, pk=None): + """ + DELETE /admin-roles/{username}/ + Remove admin role from a user. + """ + username = pk + enforcer.delete_role_for_user(username, "admin") + return Response(f"Admin role removed from user {username}", status=status.HTTP_204_NO_CONTENT) + + +class PolicyBulkViewSet(viewsets.ViewSet): + """ + ViewSet for bulk policy operations using Casbin's add_policies and remove_policies. + This is a simple testing interface for bulk policy management. + """ + + def create(self, request): + """ + POST /policy-bulk/ + Add multiple policies at once using add_policies. + + Example request body: + ```json + { + "policies": [ + ["user1", "/api/resource1/", "GET"], + ["user2", "/api/resource2/", "POST"], + ["user3", "/api/resource3/", "DELETE"] + ] + } + ``` + """ + policies = request.data.get("policies", []) + result = enforcer.add_policies(policies) + return Response( + { + "message": "Bulk policy addition completed", + "success": result, + "policies_added": len(policies), + "policies": policies, + }, + status=status.HTTP_201_CREATED if result else status.HTTP_200_OK, + ) + + def destroy(self, request, pk=None): # pylint: disable=unused-argument + """ + DELETE /policy-bulk/remove/ + Remove multiple policies at once using remove_policies. + Uses request body instead of pk since we need to pass multiple policies. + + Example request body: + ```json + { + "policies": [ + ["user1", "/api/resource1/", "GET"], + ["user2", "/api/resource2/", "POST"] + ] + } + ``` + """ + policies = request.data.get("policies", []) + result = enforcer.remove_policies(policies) + return Response( + { + "message": "Bulk policy removal completed", + "success": result, + "policies_removed": len(policies), + "policies": policies, + }, + status=status.HTTP_204_NO_CONTENT if result else status.HTTP_200_OK, + ) + + +class PolicySingleViewSet(viewsets.ViewSet): + """ + ViewSet for single policy operations using Casbin's add_policy and remove_policy. + Simple testing interface for individual policy management. + """ + + def create(self, request): + """ + POST /policy-single/ + Add a single policy using add_policy. + + Example request body: + ```json + { + "subject": "user1", + "object": "/api/resource1/", + "action": "GET" + } + ``` + """ + subject = request.data.get("subject") + obj = request.data.get("object") + action = request.data.get("action") + result = enforcer.add_policy(subject, obj, action) + return Response( + { + "message": "Policy added successfully" if result else "Policy already exists", + "success": result, + "policy": [subject, obj, action], + }, + status=status.HTTP_201_CREATED if result else status.HTTP_200_OK, + ) + + def destroy(self, request, pk=None): # pylint: disable=unused-argument + """ + DELETE /policy-single/remove/ + Remove a single policy using remove_policy. + + Example request body: + ```json + { + "subject": "user1", + "object": "/api/resource1/", + "action": "GET" + } + ``` + """ + subject = request.data.get("subject") + obj = request.data.get("object") + action = request.data.get("action") + result = enforcer.remove_policy(subject, obj, action) + return Response( + { + "message": "Policy removed successfully" if result else "Policy not found", + "success": result, + "policy": [subject, obj, action], + }, + status=status.HTTP_204_NO_CONTENT if result else status.HTTP_404_NOT_FOUND, + ) diff --git a/requirements/base.in b/requirements/base.in index 9f4002ee..d484b1b0 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -1,7 +1,11 @@ # Core requirements for using this application -c constraints.txt -Django # Web application framework +Django # Web application framework +djangorestframework # Django REST framework for API development openedx-atlas +casbin-django-orm-adapter # Casbin Django ORM adapter +django_authorization # Django Authorization library +redis-watcher # Redis Watcher for Casbin diff --git a/requirements/base.txt b/requirements/base.txt index d7690f80..73bf18eb 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -6,11 +6,34 @@ # asgiref==3.9.1 # via django +casbin==1.43.0 + # via django-authorization +casbin-django-orm-adapter==1.7.0 + # via -r requirements/base.in django==4.2.23 # via # -c https:/raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/base.in + # casbin-django-orm-adapter + # django-authorization + # djangorestframework +django-authorization==1.4.0 + # via -r requirements/base.in +djangorestframework==3.16.1 + # via -r requirements/base.in openedx-atlas==0.7.0 # via -r requirements/base.in +pycasbin==2.2.0 + # via + # casbin-django-orm-adapter + # redis-watcher +redis==6.4.0 + # via redis-watcher +redis-watcher==1.8.0 + # via -r requirements/base.in +simpleeval==1.0.3 + # via + # casbin + # pycasbin sqlparse==0.5.3 # via django diff --git a/requirements/dev.txt b/requirements/dev.txt index 47bf74ab..a4f13092 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -21,6 +21,12 @@ cachetools==6.2.0 # via # -r requirements/ci.txt # tox +casbin==1.43.0 + # via + # -r requirements/quality.txt + # django-authorization +casbin-django-orm-adapter==1.7.0 + # via -r requirements/quality.txt chardet==5.2.0 # via # -r requirements/ci.txt @@ -64,7 +70,14 @@ django==4.2.23 # via # -c https:/raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/quality.txt + # casbin-django-orm-adapter + # django-authorization + # djangorestframework # edx-i18n-tools +django-authorization==1.4.0 + # via -r requirements/quality.txt +djangorestframework==3.16.1 + # via -r requirements/quality.txt edx-i18n-tools==1.9.0 # via -r requirements/dev.in edx-lint==5.6.0 @@ -133,6 +146,11 @@ pluggy==1.6.0 # tox polib==1.2.0 # via edx-i18n-tools +pycasbin==2.2.0 + # via + # -r requirements/quality.txt + # casbin-django-orm-adapter + # redis-watcher pycodestyle==2.14.0 # via -r requirements/quality.txt pydocstyle==6.3.0 @@ -189,6 +207,17 @@ pyyaml==6.0.2 # -r requirements/quality.txt # code-annotations # edx-i18n-tools +redis==6.4.0 + # via + # -r requirements/quality.txt + # redis-watcher +redis-watcher==1.8.0 + # via -r requirements/quality.txt +simpleeval==1.0.3 + # via + # -r requirements/quality.txt + # casbin + # pycasbin six==1.17.0 # via # -r requirements/quality.txt diff --git a/requirements/doc.txt b/requirements/doc.txt index a16c0732..76db7399 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -22,6 +22,12 @@ beautifulsoup4==4.13.5 # via pydata-sphinx-theme build==1.3.0 # via -r requirements/doc.in +casbin==1.43.0 + # via + # -r requirements/test.txt + # django-authorization +casbin-django-orm-adapter==1.7.0 + # via -r requirements/test.txt certifi==2025.8.3 # via requests cffi==1.17.1 @@ -44,6 +50,13 @@ django==4.2.23 # via # -c https:/raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/test.txt + # casbin-django-orm-adapter + # django-authorization + # djangorestframework +django-authorization==1.4.0 + # via -r requirements/test.txt +djangorestframework==3.16.1 + # via -r requirements/test.txt doc8==2.0.0 # via -r requirements/doc.in docutils==0.21.2 @@ -111,6 +124,11 @@ pluggy==1.6.0 # -r requirements/test.txt # pytest # pytest-cov +pycasbin==2.2.0 + # via + # -r requirements/test.txt + # casbin-django-orm-adapter + # redis-watcher pycparser==2.22 # via cffi pydata-sphinx-theme==0.15.4 @@ -146,6 +164,12 @@ pyyaml==6.0.2 # code-annotations readme-renderer==44.0 # via twine +redis==6.4.0 + # via + # -r requirements/test.txt + # redis-watcher +redis-watcher==1.8.0 + # via -r requirements/test.txt requests==2.32.5 # via # id @@ -164,6 +188,11 @@ roman-numerals-py==3.1.0 # via sphinx secretstorage==3.3.3 # via keyring +simpleeval==1.0.3 + # via + # -r requirements/test.txt + # casbin + # pycasbin snowballstemmer==3.0.1 # via sphinx soupsieve==2.8 diff --git a/requirements/quality.txt b/requirements/quality.txt index 0340e588..b408955f 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -12,6 +12,12 @@ astroid==3.3.11 # via # pylint # pylint-celery +casbin==1.43.0 + # via + # -r requirements/test.txt + # django-authorization +casbin-django-orm-adapter==1.7.0 + # via -r requirements/test.txt click==8.2.1 # via # -r requirements/test.txt @@ -34,6 +40,13 @@ django==4.2.23 # via # -c https:/raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/test.txt + # casbin-django-orm-adapter + # django-authorization + # djangorestframework +django-authorization==1.4.0 + # via -r requirements/test.txt +djangorestframework==3.16.1 + # via -r requirements/test.txt edx-lint==5.6.0 # via -r requirements/quality.in iniconfig==2.1.0 @@ -67,6 +80,11 @@ pluggy==1.6.0 # -r requirements/test.txt # pytest # pytest-cov +pycasbin==2.2.0 + # via + # -r requirements/test.txt + # casbin-django-orm-adapter + # redis-watcher pycodestyle==2.14.0 # via -r requirements/quality.in pydocstyle==6.3.0 @@ -106,6 +124,17 @@ pyyaml==6.0.2 # via # -r requirements/test.txt # code-annotations +redis==6.4.0 + # via + # -r requirements/test.txt + # redis-watcher +redis-watcher==1.8.0 + # via -r requirements/test.txt +simpleeval==1.0.3 + # via + # -r requirements/test.txt + # casbin + # pycasbin six==1.17.0 # via edx-lint snowballstemmer==3.0.1 diff --git a/requirements/test.txt b/requirements/test.txt index 9cf395ae..e4c77010 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -8,6 +8,12 @@ asgiref==3.9.1 # via # -r requirements/base.txt # django +casbin==1.43.0 + # via + # -r requirements/base.txt + # django-authorization +casbin-django-orm-adapter==1.7.0 + # via -r requirements/base.txt click==8.2.1 # via code-annotations code-annotations==2.3.0 @@ -17,6 +23,13 @@ coverage[toml]==7.10.5 # via # -c https:/raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/base.txt + # casbin-django-orm-adapter + # django-authorization + # djangorestframework +django-authorization==1.4.0 + # via -r requirements/base.txt +djangorestframework==3.16.1 + # via -r requirements/base.txt iniconfig==2.1.0 # via pytest jinja2==3.1.6 @@ -31,6 +44,11 @@ pluggy==1.6.0 # via # pytest # pytest-cov +pycasbin==2.2.0 + # via + # -r requirements/base.txt + # casbin-django-orm-adapter + # redis-watcher pygments==2.19.2 # via pytest pytest==8.4.1 @@ -45,6 +63,17 @@ python-slugify==8.0.4 # via code-annotations pyyaml==6.0.2 # via code-annotations +redis==6.4.0 + # via + # -r requirements/base.txt + # redis-watcher +redis-watcher==1.8.0 + # via -r requirements/base.txt +simpleeval==1.0.3 + # via + # -r requirements/base.txt + # casbin + # pycasbin sqlparse==0.5.3 # via # -r requirements/base.txt diff --git a/setup.py b/setup.py index b0f66915..217bb067 100755 --- a/setup.py +++ b/setup.py @@ -159,4 +159,12 @@ def is_requirement(line): "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", ], + entry_points={ + "lms.djangoapp": [ + "openedx_authz = openedx_authz.apps:OpenedxAuthzConfig", + ], + "cms.djangoapp": [ + "openedx_authz = openedx_authz.apps:OpenedxAuthzConfig", + ], + }, )