diff --git a/.annotation_safe_list.yml b/.annotation_safe_list.yml index 62eaaa74..c2aa2fd5 100644 --- a/.annotation_safe_list.yml +++ b/.annotation_safe_list.yml @@ -39,3 +39,5 @@ waffle.Sample: ".. no_pii:": "This model has no PII" waffle.Switch: ".. no_pii:": "This model has no PII" +casbin_adapter.CasbinRule: + ".. no_pii:": "This model stores authorization policy rules and contains no PII" diff --git a/.coveragerc b/.coveragerc index 13fb7fbb..db1f3f1a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -3,9 +3,9 @@ branch = True data_file = .coverage source=openedx_authz omit = - test_settings.py */migrations/* *admin.py */static/* */templates/* */tests/* + */settings/* diff --git a/.gitignore b/.gitignore index 4fdf3930..3eea6ae5 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,6 @@ docs/openedx_authz.*.rst # Private requirements requirements/private.in requirements/private.txt + +# Sqlite Database +*.db diff --git a/docs/conf.py b/docs/conf.py index ab818ce6..1e1cde8d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -41,7 +41,7 @@ def get_version(*file_paths): VERSION = get_version("../openedx_authz", "__init__.py") # Configure Django for autodoc usage -os.environ["DJANGO_SETTINGS_MODULE"] = "test_settings" +os.environ["DJANGO_SETTINGS_MODULE"] = "openedx_authz.settings.test" django_setup() # If extensions (or modules to document with autodoc) are in another directory, @@ -407,7 +407,7 @@ def get_version(*file_paths): documentation_title, author, project_title, - "One-line description for README and other doc files.", + "Open edX AuthZ provides the architecture and foundations of the authorization framework.", "Miscellaneous", ), ] diff --git a/docs/index.rst b/docs/index.rst index 155afeb8..af59ee74 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,7 +6,7 @@ openedx-authz ============= -One-line description for README and other doc files. +Open edX AuthZ provides the architecture and foundations of the authorization framework. Contents: 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/apps.py b/openedx_authz/apps.py index 208599c9..74dc1df7 100644 --- a/openedx_authz/apps.py +++ b/openedx_authz/apps.py @@ -11,4 +11,31 @@ class OpenedxAuthzConfig(AppConfig): """ name = "openedx_authz" - plugin_app = {} + 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"}, + }, + }, + } diff --git a/openedx_authz/engine/__init__.py b/openedx_authz/engine/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/openedx_authz/engine/adapter.py b/openedx_authz/engine/adapter.py new file mode 100644 index 00000000..c68cfe82 --- /dev/null +++ b/openedx_authz/engine/adapter.py @@ -0,0 +1,123 @@ +""" +Extended Casbin Adapter with Filtering Support. + +This module provides an enhanced adapter implementation for Casbin that extends +the base Django adapter with filtering capabilities. The ExtendedAdapter allows +for efficient loading of policy rules from the database with support for +filtering based on policy attributes. + +The adapter combines functionality from both the base Adapter (for Django ORM +integration) and FilteredAdapter (for selective policy loading) to provide +optimized policy management for authorization systems. +""" + +from enum import Enum + +from casbin import persist +from casbin.model import Model +from casbin.persist import FilteredAdapter +from casbin_adapter.adapter import Adapter +from casbin_adapter.models import CasbinRule +from django.db.models import QuerySet + +from openedx_authz.engine.filter import Filter + + +class PolicyAttribute(Enum): + """ + Enumeration of Casbin policy attributes. + + These attributes map to the columns of the CasbinRule table, but their meaning + depends on the policy type (ptype). Check the ``openedx_authz.engine.Filter`` class + for more details. + """ + + PTYPE = "ptype" + """ptype (str): Type of policy""" + + V0 = "v0" + """v0 (str): First policy value.""" + + V1 = "v1" + """v1 (str): Second policy value.""" + + V2 = "v2" + """v2 (str): Third policy value.""" + + V3 = "v3" + """v3 (str): Fourth policy value.""" + + V4 = "v4" + """v4 (str): Fifth policy value.""" + + V5 = "v5" + """v5 (str): Sixth policy value.""" + + +class ExtendedAdapter(Adapter, FilteredAdapter): + """ + Extended Casbin adapter with filtering capabilities. + + This adapter extends the base Django ORM Casbin adapter to support filtered + policy loading, allowing for more efficient policy management by loading + only relevant policy rules based on specified filter criteria. + + Inherits from: + Adapter: Base Django adapter for Casbin policy persistence. + FilteredAdapter: Interface for filtered policy loading. + """ + + def is_filtered(self) -> bool: + """ + Check if the adapter supports filtering. + + Returns: + bool: True if the adapter supports filtered policy loading, False otherwise. + """ + return True + + def load_filtered_policy(self, model: Model, filter: Filter) -> None: # pylint: disable=redefined-builtin + """ + Load policy rules from storage with filtering applied. + + This method loads policy rules from the database and applies the specified + filter to load only relevant rules. The filtered rules are then loaded + into the provided Casbin model. + + IMPORTANT: This method is used internally by the ``enforcer.load_filtered_policy()`` + method. Do not call this method directly. If you need to load policy rules, use + the ``enforcer.load_filtered_policy()`` method. + + Args: + model (Model): The Casbin model to load policy rules into. + filter (Filter): Filter object containing criteria for policy selection. + Should have attributes like ptype, v0, v1, etc. with lists + of values to filter by. + """ + queryset = CasbinRule.objects.using(self.db_alias) + filtered_queryset = self.filter_query(queryset, filter) + for line in filtered_queryset: + persist.load_policy_line(str(line), model) + + def filter_query(self, queryset: QuerySet, filter: Filter) -> QuerySet: # pylint: disable=redefined-builtin + """ + Apply filter criteria to the policy queryset. + + This method takes a Django queryset of CasbinRule objects and applies + filtering based on the provided filter object's attributes. It supports + filtering by policy type (ptype) and policy values (v0-v5). + + Args: + queryset (QuerySet): Django queryset of CasbinRule objects to filter. + filter (Filter): Filter object with attributes (ptype, v0, v1, v2, v3, v4, v5) + containing lists of values to filter by. Empty lists are ignored. + + Returns: + QuerySet: Filtered and ordered queryset of CasbinRule objects. + """ + for attr in PolicyAttribute: + filter_values = getattr(filter, attr.value) + if len(filter_values) > 0: + filter_kwargs = {f"{attr.value}__in": filter_values} + queryset = queryset.filter(**filter_kwargs) + return queryset.order_by("id") diff --git a/openedx_authz/engine/enforcer.py b/openedx_authz/engine/enforcer.py new file mode 100644 index 00000000..f9c8a335 --- /dev/null +++ b/openedx_authz/engine/enforcer.py @@ -0,0 +1,38 @@ +""" +Core authorization enforcer for Open edX AuthZ system. + +Provides a Casbin FastEnforcer instance with extended adapter for database policy +storage and Redis watcher for distributed policy synchronization. + +Components: + - Enforcer: Main FastEnforcer instance for policy evaluation + - Adapter: ExtendedAdapter for filtered database policy loading + - Watcher: Redis-based watcher for real-time policy updates + +Usage: + from openedx_authz.engine.enforcer import enforcer + allowed = enforcer.enforce(user, resource, action) + +Requires `CASBIN_MODEL` setting and Redis configuration for watcher functionality. +""" + +import logging + +from casbin import FastEnforcer +from django.conf import settings + +from openedx_authz.engine.adapter import ExtendedAdapter +from openedx_authz.engine.watcher import Watcher + +logger = logging.getLogger(__name__) + +adapter = ExtendedAdapter() +enforcer = FastEnforcer(settings.CASBIN_MODEL, adapter, enable_log=True) +enforcer.enable_auto_save(True) + +if Watcher: + try: + enforcer.set_watcher(Watcher) + logger.info("Watcher successfully set on Casbin enforcer") + except Exception as e: # pylint: disable=broad-exception-caught + logger.error(f"Failed to set watcher on Casbin enforcer: {e}") diff --git a/openedx_authz/engine/filter.py b/openedx_authz/engine/filter.py new file mode 100644 index 00000000..417ea5de --- /dev/null +++ b/openedx_authz/engine/filter.py @@ -0,0 +1,80 @@ +""" +Filter Implementation for Casbin Policy Selection. + +This module provides a Filter class used to specify criteria for selective +loading of Casbin policy rules. The Filter class allows for efficient policy +management by enabling the loading of only relevant policy rules based on +policy type and attribute values. + +The Filter class is designed to work with the ExtendedAdapter to provide +optimized policy loading in scenarios where only a subset of policies +is needed, such as loading policies for a specific user, course, or role. +""" + +from typing import Optional + +import attr + + +@attr.define +class Filter: + """ + Filter class for selective Casbin policy loading. + + This class defines filtering criteria used to load only specific policy rules + from the database instead of loading all policies. Each attribute corresponds + to a column in the Casbin policy storage schema and accepts a list of values + to filter by. + + Note: + - Empty lists for any attribute means no filtering on that attribute + - Non-empty lists create an "IN" filter for that attribute + - All non-empty filters are combined with AND logic + """ + + ptype: Optional[list[str]] = attr.field(factory=list) + """ptype (Optional[list[str]]): Policy type filter. + + - ``p`` → Policy rule (permissions). + - ``g`` → Grouping rule (user ↔ role). + - ``g2`` → Action grouping (parent action ↔ child action). + """ + + v0: Optional[list[str]] = attr.field(factory=list) + """v0 (Optional[list[str]]): First policy value filter. + + - For ``p`` → Subject (e.g., ``role:org_admin``, ``user:alice``). + - For ``g`` → User (e.g., ``user:alice``). + - For ``g2`` → Parent action (e.g., ``act:manage``). + """ + + v1: Optional[list[str]] = attr.field(factory=list) + """v1 (Optional[list[str]]): Second policy value filter. + + - For ``p`` → Action (e.g., ``act:manage``, ``act:edit``). + - For ``g`` → Role (e.g., ``role:org_admin``). + - For ``g2`` → Child action (e.g., ``act:edit``). + """ + + v2: Optional[list[str]] = attr.field(factory=list) + """v2 (Optional[list[str]]): Third policy value filter. + + - For ``p`` → Object or resource (e.g., ``lib:*``, ``org:MIT``). + - For ``g`` → Scope or resource (e.g., ``org:MIT``). + - For ``g2`` → Not used. + """ + + v3: Optional[list[str]] = attr.field(factory=list) + """v3 (Optional[list[str]]): Fourth policy value filter. + + - For ``p`` → Effect (allow or deny). + - Otherwise unused. + """ + + v4: Optional[list[str]] = attr.field(factory=list) + """v4 (Optional[list[str]]): Fifth policy value filter (optional additional context). + """ + + v5: Optional[list[str]] = attr.field(factory=list) + """v5 (Optional[list[str]]): Sixth policy value filter (optional additional context). + """ diff --git a/openedx_authz/engine/watcher.py b/openedx_authz/engine/watcher.py new file mode 100644 index 00000000..5cc945b3 --- /dev/null +++ b/openedx_authz/engine/watcher.py @@ -0,0 +1,59 @@ +""" +Redis-based policy change watcher for the authorization enforcer. + +This module provides functionality to monitor policy changes in real-time using Redis +as a message broker. It enables automatic policy reloading across multiple instances +of the authorization system to maintain consistency and synchronization. + +The watcher connects to Redis on the configured host and port, listens for policy +change events, and automatically triggers policy reloads when changes are detected. +This ensures that all running instances of the authorization system stay synchronized +with the latest policy configurations. +""" + +import logging + +from django.conf import settings +from redis_watcher import WatcherOptions, new_watcher + +logger = logging.getLogger(__name__) + + +def callback_function(event) -> None: + """ + Enhanced callback function for the enforcer that reloads policies on changes. + + This function is called whenever a policy change event is received through Redis. + It reloads the policies in the enforcer to ensure all instances stay synchronized. + + Args: + event: The policy change event from Redis + """ + logger.info(f"Policy change event received: {event}") + + +def create_watcher(): + """ + Create and configure the Redis watcher for policy changes. + + Returns: + The configured watcher instance + """ + watcher_options = WatcherOptions() + watcher_options.host = settings.REDIS_HOST + watcher_options.port = settings.REDIS_PORT + watcher_options.optional_update_callback = callback_function + + try: + watcher = new_watcher(watcher_options) + logger.info("Redis watcher created successfully") + return watcher + except Exception as e: + logger.error(f"Failed to create Redis watcher: {e}") + raise + + +if settings.CASBIN_WATCHER_ENABLED: + Watcher = create_watcher() +else: + Watcher = None diff --git a/openedx_authz/settings/__init__.py b/openedx_authz/settings/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/openedx_authz/settings/common.py b/openedx_authz/settings/common.py new file mode 100644 index 00000000..22feabd3 --- /dev/null +++ b/openedx_authz/settings/common.py @@ -0,0 +1,30 @@ +""" +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 + casbin_adapter_app = "casbin_adapter.apps.CasbinAdapterConfig" + if casbin_adapter_app not in settings.INSTALLED_APPS: + settings.INSTALLED_APPS.append(casbin_adapter_app) + + # Add Casbin configuration + settings.CASBIN_MODEL = os.path.join(ROOT_DIRECTORY, "engine", "config", "model.conf") + settings.CASBIN_WATCHER_ENABLED = True + # TODO: Replace with a more dynamic configuration + # Redis host and port are temporarily loaded here for the MVP + settings.REDIS_HOST = "redis" + settings.REDIS_PORT = 6379 diff --git a/openedx_authz/settings/production.py b/openedx_authz/settings/production.py new file mode 100644 index 00000000..48045938 --- /dev/null +++ b/openedx_authz/settings/production.py @@ -0,0 +1,14 @@ +""" +Production settings for openedx_authz plugin. +""" + + +def plugin_settings(settings): # pylint: disable=unused-argument + """ + 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/test_settings.py b/openedx_authz/settings/test.py similarity index 84% rename from test_settings.py rename to openedx_authz/settings/test.py index 88f20dd5..3d90b291 100644 --- a/test_settings.py +++ b/openedx_authz/settings/test.py @@ -1,12 +1,11 @@ """ -These settings are here to use during tests, because django requires them. - -In a real-world use case, apps in this project are installed into other -Django applications, so these settings will not be used. +Test settings for openedx_authz plugin. """ from os.path import abspath, dirname, join +from openedx_authz import ROOT_DIRECTORY + def root(*args): """ @@ -32,6 +31,7 @@ def root(*args): "django.contrib.contenttypes", "django.contrib.messages", "django.contrib.sessions", + "casbin_adapter", "openedx_authz", ) @@ -61,3 +61,9 @@ def root(*args): }, } ] + + +CASBIN_MODEL = join(ROOT_DIRECTORY, "engine", "config", "model.conf") +CASBIN_WATCHER_ENABLED = False +REDIS_HOST = "redis" +REDIS_PORT = 6379 diff --git a/openedx_authz/tests/test_filter.py b/openedx_authz/tests/test_filter.py new file mode 100644 index 00000000..a36ebb44 --- /dev/null +++ b/openedx_authz/tests/test_filter.py @@ -0,0 +1,173 @@ +""" +Tests for the Filter class used in selective Casbin policy loading. + +This module contains unit tests for the Filter class, which is used to specify +criteria for loading only relevant policy rules from the database instead of +loading all policies. The tests verify proper initialization, attribute handling, +and various filtering scenarios. +""" + +import unittest + +from openedx_authz.engine.filter import Filter + + +class TestFilter(unittest.TestCase): + """Tests for Filter class instantiation and default values.""" + + def test_default_initialization(self): + """Test that Filter initializes with empty lists by default.""" + f = Filter() + self.assertEqual(f.ptype, []) + self.assertEqual(f.v0, []) + self.assertEqual(f.v1, []) + self.assertEqual(f.v2, []) + self.assertEqual(f.v3, []) + self.assertEqual(f.v4, []) + self.assertEqual(f.v5, []) + + def test_initialization_with_ptype(self): + """Test Filter initialization with ptype parameter.""" + f = Filter(ptype=["p", "g"]) + self.assertEqual(f.ptype, ["p", "g"]) + self.assertEqual(f.v0, []) + self.assertEqual(f.v1, []) + + def test_initialization_with_multiple_attributes(self): + """Test Filter initialization with multiple attributes.""" + f = Filter(ptype=["p"], v0=["user:alice"], v1=["act:read"], v2=["org:MIT"]) + self.assertEqual(f.ptype, ["p"]) + self.assertEqual(f.v0, ["user:alice"]) + self.assertEqual(f.v1, ["act:read"]) + self.assertEqual(f.v2, ["org:MIT"]) + + def test_initialization_with_all_attributes(self): + """Test Filter initialization with all attributes.""" + f = Filter( + ptype=["p", "g"], + v0=["user:alice"], + v1=["act:read"], + v2=["org:MIT"], + v3=["allow"], + v4=["context1"], + v5=["context2"], + ) + self.assertEqual(f.ptype, ["p", "g"]) + self.assertEqual(f.v0, ["user:alice"]) + self.assertEqual(f.v1, ["act:read"]) + self.assertEqual(f.v2, ["org:MIT"]) + self.assertEqual(f.v3, ["allow"]) + self.assertEqual(f.v4, ["context1"]) + self.assertEqual(f.v5, ["context2"]) + + def test_modify_ptype_after_creation(self): + """Test modifying ptype attribute after Filter creation.""" + f = Filter() + f.ptype = ["p"] + self.assertEqual(f.ptype, ["p"]) + + def test_modify_multiple_attributes(self): + """Test modifying multiple attributes after creation.""" + f = Filter() + f.ptype = ["g"] + f.v0 = ["user:bob"] + f.v1 = ["role:admin"] + self.assertEqual(f.ptype, ["g"]) + self.assertEqual(f.v0, ["user:bob"]) + self.assertEqual(f.v1, ["role:admin"]) + + def test_empty_list_assignment(self): + """Test assigning empty lists to attributes.""" + f = Filter(ptype=["p"]) + f.ptype = [] + self.assertEqual(f.ptype, []) + + def test_none_assignment(self): + """Test assigning None to attributes.""" + f = Filter() + f.ptype = None + self.assertIsNone(f.ptype) + + def test_filter_policy_rules_only(self): + """Test filter for policy rules (p) only.""" + f = Filter(ptype=["p"]) + self.assertEqual(f.ptype, ["p"]) + self.assertIn("p", f.ptype) + + def test_filter_grouping_rules_only(self): + """Test filter for grouping rules (g) only.""" + f = Filter(ptype=["g"]) + self.assertEqual(f.ptype, ["g"]) + self.assertIn("g", f.ptype) + + def test_filter_action_grouping_only(self): + """Test filter for action grouping (g2) only.""" + f = Filter(ptype=["g2"]) + self.assertEqual(f.ptype, ["g2"]) + self.assertIn("g2", f.ptype) + + def test_filter_multiple_policy_types(self): + """Test filter for multiple policy types.""" + f = Filter(ptype=["p", "g", "g2"]) + self.assertEqual(len(f.ptype), 3) + self.assertIn("p", f.ptype) + self.assertIn("g", f.ptype) + self.assertIn("g2", f.ptype) + + def test_filter_user_permissions(self): + """Test filter for a specific user's permissions.""" + f = Filter(ptype=["p"], v0=["user:alice"]) + self.assertEqual(f.ptype, ["p"]) + self.assertEqual(f.v0, ["user:alice"]) + + def test_filter_role_assignments(self): + """Test filter for role assignments for a user.""" + f = Filter(ptype=["g"], v0=["user:alice"], v1=["role:admin"], v2=["org:MIT"]) + self.assertEqual(f.ptype, ["g"]) + self.assertEqual(f.v0, ["user:alice"]) + self.assertEqual(f.v1, ["role:admin"]) + self.assertEqual(f.v2, ["org:MIT"]) + + def test_filter_organization_policies(self): + """Test filter for all policies related to an organization.""" + f = Filter(v2=["org:MIT"]) + self.assertEqual(f.v2, ["org:MIT"]) + self.assertEqual(f.ptype, []) + + def test_filter_specific_action(self): + """Test filter for policies with a specific action.""" + f = Filter(ptype=["p"], v1=["act:edit", "act:delete"]) + self.assertEqual(f.ptype, ["p"]) + self.assertEqual(f.v1, ["act:edit", "act:delete"]) + + def test_filter_action_hierarchy(self): + """Test filter for action grouping hierarchy.""" + f = Filter(ptype=["g2"], v0=["act:manage"]) + self.assertEqual(f.ptype, ["g2"]) + self.assertEqual(f.v0, ["act:manage"]) + + def test_filter_deny_policies(self): + """Test filter for deny effect policies.""" + f = Filter(ptype=["p"], v3=["deny"]) + self.assertEqual(f.ptype, ["p"]) + self.assertEqual(f.v3, ["deny"]) + + def test_filter_wildcard_resources(self): + """Test filter for wildcard resource patterns.""" + f = Filter(ptype=["p"], v2=["lib:*", "course:*"]) + self.assertEqual(f.ptype, ["p"]) + self.assertIn("lib:*", f.v2) + self.assertIn("course:*", f.v2) + + def test_complex_permission_filter(self): + """Test complex filter combining multiple criteria.""" + f = Filter( + ptype=["p"], + v0=["role:instructor", "role:admin"], + v1=["act:edit", "act:delete"], + v2=["course:CS101", "course:CS102"], + ) + self.assertEqual(len(f.ptype), 1) + self.assertEqual(len(f.v0), 2) + self.assertEqual(len(f.v1), 2) + self.assertEqual(len(f.v2), 2) diff --git a/requirements/base.in b/requirements/base.in index ad8b6ced..939b2799 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -5,4 +5,7 @@ Django # Web application framework openedx-atlas -pycasbin # Authorization library for implementing access control models +attrs # Classes without boilerplate +pycasbin # Authorization library for implementing access control models +casbin-django-orm-adapter # Adapter for Django ORM for Casbin +redis-watcher # Watcher for Redis for Casbin diff --git a/requirements/base.txt b/requirements/base.txt index 0d23b15c..48aa2b73 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -2,17 +2,29 @@ # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # -# pip-compile --output-file=requirements/base.txt requirements/base.in +# pip-compile --cert=None --client-cert=None --index-url=None --output-file=requirements/base.txt --pip-args=None requirements/base.in # asgiref==3.9.1 # via django -django==4.2.23 +attrs==25.3.0 + # via -r requirements/base.in +casbin-django-orm-adapter==1.7.0 + # via -r requirements/base.in +django==4.2.24 # via # -c https:/raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/base.in + # casbin-django-orm-adapter openedx-atlas==0.7.0 # via -r requirements/base.in pycasbin==2.2.0 + # via + # -r requirements/base.in + # 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 pycasbin diff --git a/requirements/ci.txt b/requirements/ci.txt index 87f162de..fc973e79 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # -# pip-compile --output-file=requirements/ci.txt requirements/ci.in +# pip-compile --cert=None --client-cert=None --index-url=None --output-file=requirements/ci.txt --pip-args=None requirements/ci.in # cachetools==6.2.0 # via tox @@ -28,7 +28,7 @@ pluggy==1.6.0 # via tox pyproject-api==1.9.1 # via tox -tox==4.28.4 +tox==4.30.2 # via -r requirements/ci.in virtualenv==20.34.0 # via tox diff --git a/requirements/dev.txt b/requirements/dev.txt index 58806021..4a6c599e 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # -# pip-compile --output-file=requirements/dev.txt requirements/dev.in +# pip-compile --cert=None --client-cert=None --index-url=None --output-file=requirements/dev.txt --pip-args=None requirements/dev.in # asgiref==3.9.1 # via @@ -13,6 +13,8 @@ astroid==3.3.11 # -r requirements/quality.txt # pylint # pylint-celery +attrs==25.3.0 + # via -r requirements/quality.txt build==1.3.0 # via # -r requirements/pip-tools.txt @@ -21,12 +23,14 @@ cachetools==6.2.0 # via # -r requirements/ci.txt # tox +casbin-django-orm-adapter==1.7.0 + # via -r requirements/quality.txt chardet==5.2.0 # via # -r requirements/ci.txt # diff-cover # tox -click==8.2.1 +click==8.3.0 # via # -r requirements/pip-tools.txt # -r requirements/quality.txt @@ -46,7 +50,7 @@ colorama==0.4.6 # via # -r requirements/ci.txt # tox -coverage[toml]==7.10.5 +coverage[toml]==7.10.6 # via # -r requirements/quality.txt # pytest-cov @@ -62,10 +66,11 @@ distlib==0.4.0 # via # -r requirements/ci.txt # virtualenv -django==4.2.23 +django==4.2.24 # via # -c https:/raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/quality.txt + # casbin-django-orm-adapter # edx-i18n-tools edx-i18n-tools==1.9.0 # via -r requirements/dev.in @@ -136,7 +141,10 @@ pluggy==1.6.0 polib==1.2.0 # via edx-i18n-tools pycasbin==2.2.0 - # via -r requirements/quality.txt + # via + # -r requirements/quality.txt + # casbin-django-orm-adapter + # redis-watcher pycodestyle==2.14.0 # via -r requirements/quality.txt pydocstyle==6.3.0 @@ -175,12 +183,12 @@ pyproject-hooks==1.2.0 # -r requirements/pip-tools.txt # build # pip-tools -pytest==8.4.1 +pytest==8.4.2 # via # -r requirements/quality.txt # pytest-cov # pytest-django -pytest-cov==6.2.1 +pytest-cov==7.0.0 # via -r requirements/quality.txt pytest-django==4.11.1 # via -r requirements/quality.txt @@ -193,6 +201,12 @@ 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 @@ -221,7 +235,7 @@ tomlkit==0.13.3 # via # -r requirements/quality.txt # pylint -tox==4.28.4 +tox==4.30.2 # via -r requirements/ci.txt virtualenv==20.34.0 # via diff --git a/requirements/doc.txt b/requirements/doc.txt index ff1044df..4a267b09 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # -# pip-compile --output-file=requirements/doc.txt requirements/doc.in +# pip-compile --cert=None --client-cert=None --index-url=None --output-file=requirements/doc.txt --pip-args=None requirements/doc.in # accessible-pygments==0.0.5 # via pydata-sphinx-theme @@ -12,6 +12,8 @@ asgiref==3.9.1 # via # -r requirements/test.txt # django +attrs==25.3.0 + # via -r requirements/test.txt babel==2.17.0 # via # pydata-sphinx-theme @@ -22,30 +24,33 @@ beautifulsoup4==4.13.5 # via pydata-sphinx-theme build==1.3.0 # via -r requirements/doc.in +casbin-django-orm-adapter==1.7.0 + # via -r requirements/test.txt certifi==2025.8.3 # via requests -cffi==1.17.1 +cffi==2.0.0 # via cryptography charset-normalizer==3.4.3 # via requests -click==8.2.1 +click==8.3.0 # via # -r requirements/test.txt # code-annotations code-annotations==2.3.0 # via -r requirements/test.txt -coverage[toml]==7.10.5 +coverage[toml]==7.10.6 # via # -r requirements/test.txt # pytest-cov -cryptography==45.0.6 +cryptography==46.0.1 # via secretstorage ddt==1.7.2 # via -r requirements/test.txt -django==4.2.23 +django==4.2.24 # via # -c https:/raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/test.txt + # casbin-django-orm-adapter doc8==2.0.0 # via -r requirements/doc.in docutils==0.21.2 @@ -92,7 +97,7 @@ markupsafe==3.0.2 # jinja2 mdurl==0.1.2 # via markdown-it-py -more-itertools==10.7.0 +more-itertools==10.8.0 # via # jaraco-classes # jaraco-functools @@ -114,8 +119,11 @@ pluggy==1.6.0 # pytest # pytest-cov pycasbin==2.2.0 - # via -r requirements/test.txt -pycparser==2.22 + # via + # -r requirements/test.txt + # casbin-django-orm-adapter + # redis-watcher +pycparser==2.23 # via cffi pydata-sphinx-theme==0.15.4 # via sphinx-book-theme @@ -131,12 +139,12 @@ pygments==2.19.2 # sphinx pyproject-hooks==1.2.0 # via build -pytest==8.4.1 +pytest==8.4.2 # via # -r requirements/test.txt # pytest-cov # pytest-django -pytest-cov==6.2.1 +pytest-cov==7.0.0 # via -r requirements/test.txt pytest-django==4.11.1 # via -r requirements/test.txt @@ -150,6 +158,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 @@ -166,7 +180,7 @@ rich==14.1.0 # via twine roman-numerals-py==3.1.0 # via sphinx -secretstorage==3.3.3 +secretstorage==3.4.0 # via keyring simpleeval==1.0.3 # via @@ -208,7 +222,7 @@ text-unidecode==1.3 # via # -r requirements/test.txt # python-slugify -twine==6.1.0 +twine==6.2.0 # via -r requirements/doc.in typing-extensions==4.15.0 # via diff --git a/requirements/pip-tools.txt b/requirements/pip-tools.txt index 236747f1..b34c27e0 100644 --- a/requirements/pip-tools.txt +++ b/requirements/pip-tools.txt @@ -2,11 +2,11 @@ # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # -# pip-compile --output-file=requirements/pip-tools.txt requirements/pip-tools.in +# pip-compile --cert=None --client-cert=None --index-url=None --output-file=requirements/pip-tools.txt --pip-args=None requirements/pip-tools.in # build==1.3.0 # via pip-tools -click==8.2.1 +click==8.3.0 # via pip-tools packaging==25.0 # via build diff --git a/requirements/pip.txt b/requirements/pip.txt index fa355ff0..204fe225 100644 --- a/requirements/pip.txt +++ b/requirements/pip.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # -# pip-compile --allow-unsafe --output-file=requirements/pip.txt requirements/pip.in +# pip-compile --allow-unsafe --cert=None --client-cert=None --index-url=None --output-file=requirements/pip.txt --pip-args=None requirements/pip.in # wheel==0.45.1 # via -r requirements/pip.in diff --git a/requirements/quality.txt b/requirements/quality.txt index 1f0574f3..046dd275 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # -# pip-compile --output-file=requirements/quality.txt requirements/quality.in +# pip-compile --cert=None --client-cert=None --index-url=None --output-file=requirements/quality.txt --pip-args=None requirements/quality.in # asgiref==3.9.1 # via @@ -12,7 +12,11 @@ astroid==3.3.11 # via # pylint # pylint-celery -click==8.2.1 +attrs==25.3.0 + # via -r requirements/test.txt +casbin-django-orm-adapter==1.7.0 + # via -r requirements/test.txt +click==8.3.0 # via # -r requirements/test.txt # click-log @@ -24,7 +28,7 @@ code-annotations==2.3.0 # via # -r requirements/test.txt # edx-lint -coverage[toml]==7.10.5 +coverage[toml]==7.10.6 # via # -r requirements/test.txt # pytest-cov @@ -32,10 +36,11 @@ ddt==1.7.2 # via -r requirements/test.txt dill==0.4.0 # via pylint -django==4.2.23 +django==4.2.24 # via # -c https:/raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/test.txt + # casbin-django-orm-adapter edx-lint==5.6.0 # via -r requirements/quality.in iniconfig==2.1.0 @@ -70,7 +75,10 @@ pluggy==1.6.0 # pytest # pytest-cov pycasbin==2.2.0 - # via -r requirements/test.txt + # via + # -r requirements/test.txt + # casbin-django-orm-adapter + # redis-watcher pycodestyle==2.14.0 # via -r requirements/quality.in pydocstyle==6.3.0 @@ -93,12 +101,12 @@ pylint-plugin-utils==0.9.0 # via # pylint-celery # pylint-django -pytest==8.4.1 +pytest==8.4.2 # via # -r requirements/test.txt # pytest-cov # pytest-django -pytest-cov==6.2.1 +pytest-cov==7.0.0 # via -r requirements/test.txt pytest-django==4.11.1 # via -r requirements/test.txt @@ -110,6 +118,12 @@ 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 diff --git a/requirements/test.txt b/requirements/test.txt index d0f0f363..aed9c827 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -2,23 +2,28 @@ # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # -# pip-compile --output-file=requirements/test.txt requirements/test.in +# pip-compile --cert=None --client-cert=None --index-url=None --output-file=requirements/test.txt --pip-args=None requirements/test.in # asgiref==3.9.1 # via # -r requirements/base.txt # django -click==8.2.1 +attrs==25.3.0 + # via -r requirements/base.txt +casbin-django-orm-adapter==1.7.0 + # via -r requirements/base.txt +click==8.3.0 # via code-annotations code-annotations==2.3.0 # via -r requirements/test.in -coverage[toml]==7.10.5 +coverage[toml]==7.10.6 # via pytest-cov ddt==1.7.2 # via -r requirements/test.in # via # -c https:/raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/base.txt + # casbin-django-orm-adapter iniconfig==2.1.0 # via pytest jinja2==3.1.6 @@ -34,14 +39,17 @@ pluggy==1.6.0 # pytest # pytest-cov pycasbin==2.2.0 - # via -r requirements/base.txt + # via + # -r requirements/base.txt + # casbin-django-orm-adapter + # redis-watcher pygments==2.19.2 # via pytest -pytest==8.4.1 +pytest==8.4.2 # via # pytest-cov # pytest-django -pytest-cov==6.2.1 +pytest-cov==7.0.0 # via -r requirements/test.in pytest-django==4.11.1 # via -r requirements/test.in @@ -49,6 +57,12 @@ 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 diff --git a/setup.py b/setup.py index 217bb067..91aa3307 100755 --- a/setup.py +++ b/setup.py @@ -132,7 +132,7 @@ def is_requirement(line): setup( name="openedx-authz", version=VERSION, - description="""One-line description for README and other doc files.""", + description="""Open edX AuthZ provides the architecture and foundations of the authorization framework.""", long_description=README + "\n\n" + CHANGELOG, author="Open edX Project", author_email="oscm@openedx.org", diff --git a/tox.ini b/tox.ini index 731e15d4..0c2ec5cb 100644 --- a/tox.ini +++ b/tox.ini @@ -30,7 +30,7 @@ ignore = D101,D200,D203,D212,D215,D404,D405,D406,D407,D408,D409,D410,D411,D412,D match-dir = (?!migrations) [pytest] -DJANGO_SETTINGS_MODULE = test_settings +DJANGO_SETTINGS_MODULE = openedx_authz.settings.test addopts = --cov openedx_authz --cov tests --cov-report term-missing --cov-report xml norecursedirs = .* docs requirements site-packages @@ -45,7 +45,7 @@ commands = [testenv:docs] setenv = - DJANGO_SETTINGS_MODULE = test_settings + DJANGO_SETTINGS_MODULE = openedx_authz.settings.test PYTHONPATH = {toxinidir} # Adding the option here instead of as a default in the docs Makefile because that Makefile is generated by shpinx. SPHINXOPTS = -W @@ -77,12 +77,12 @@ commands = rm tests/__init__.py pycodestyle openedx_authz tests manage.py setup.py pydocstyle openedx_authz tests manage.py setup.py - isort --check-only --diff tests test_utils openedx_authz manage.py setup.py test_settings.py + isort --check-only --diff tests test_utils openedx_authz manage.py setup.py make selfcheck [testenv:pii_check] setenv = - DJANGO_SETTINGS_MODULE = test_settings + DJANGO_SETTINGS_MODULE = openedx_authz.settings.test deps = -r{toxinidir}/requirements/test.txt commands =