Skip to content

Commit c914c28

Browse files
authored
[FC-0099] feat: add casbin-based authorization engine (#55)
1 parent 76882c7 commit c914c28

28 files changed

Lines changed: 683 additions & 57 deletions

.annotation_safe_list.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,5 @@ waffle.Sample:
3939
".. no_pii:": "This model has no PII"
4040
waffle.Switch:
4141
".. no_pii:": "This model has no PII"
42+
casbin_adapter.CasbinRule:
43+
".. no_pii:": "This model stores authorization policy rules and contains no PII"

.coveragerc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ branch = True
33
data_file = .coverage
44
source=openedx_authz
55
omit =
6-
test_settings.py
76
*/migrations/*
87
*admin.py
98
*/static/*
109
*/templates/*
1110
*/tests/*
11+
*/settings/*

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,6 @@ docs/openedx_authz.*.rst
6363
# Private requirements
6464
requirements/private.in
6565
requirements/private.txt
66+
67+
# Sqlite Database
68+
*.db

docs/conf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def get_version(*file_paths):
4141

4242
VERSION = get_version("../openedx_authz", "__init__.py")
4343
# Configure Django for autodoc usage
44-
os.environ["DJANGO_SETTINGS_MODULE"] = "test_settings"
44+
os.environ["DJANGO_SETTINGS_MODULE"] = "openedx_authz.settings.test"
4545
django_setup()
4646

4747
# If extensions (or modules to document with autodoc) are in another directory,
@@ -407,7 +407,7 @@ def get_version(*file_paths):
407407
documentation_title,
408408
author,
409409
project_title,
410-
"One-line description for README and other doc files.",
410+
"Open edX AuthZ provides the architecture and foundations of the authorization framework.",
411411
"Miscellaneous",
412412
),
413413
]

docs/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
openedx-authz
77
=============
88

9-
One-line description for README and other doc files.
9+
Open edX AuthZ provides the architecture and foundations of the authorization framework.
1010

1111
Contents:
1212

manage.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
PWD = os.path.abspath(os.path.dirname(__file__))
1010

1111
if __name__ == "__main__":
12-
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_settings")
12+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "openedx_authz.settings.test")
1313
sys.path.append(PWD)
1414
try:
1515
from django.core.management import execute_from_command_line

openedx_authz/apps.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,31 @@ class OpenedxAuthzConfig(AppConfig):
1111
"""
1212

1313
name = "openedx_authz"
14-
plugin_app = {}
14+
verbose_name = "Open edX AuthZ"
15+
default_auto_field = "django.db.models.BigAutoField"
16+
plugin_app = {
17+
"url_config": {
18+
"lms.djangoapp": {
19+
"namespace": "openedx-authz",
20+
"regex": r"^openedx-authz/",
21+
"relative_path": "urls",
22+
},
23+
"cms.djangoapp": {
24+
"namespace": "openedx-authz",
25+
"regex": r"^openedx-authz/",
26+
"relative_path": "urls",
27+
},
28+
},
29+
"settings_config": {
30+
"lms.djangoapp": {
31+
"test": {"relative_path": "settings.test"},
32+
"common": {"relative_path": "settings.common"},
33+
"production": {"relative_path": "settings.production"},
34+
},
35+
"cms.djangoapp": {
36+
"test": {"relative_path": "settings.test"},
37+
"common": {"relative_path": "settings.common"},
38+
"production": {"relative_path": "settings.production"},
39+
},
40+
},
41+
}

openedx_authz/engine/__init__.py

Whitespace-only changes.

openedx_authz/engine/adapter.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
"""
2+
Extended Casbin Adapter with Filtering Support.
3+
4+
This module provides an enhanced adapter implementation for Casbin that extends
5+
the base Django adapter with filtering capabilities. The ExtendedAdapter allows
6+
for efficient loading of policy rules from the database with support for
7+
filtering based on policy attributes.
8+
9+
The adapter combines functionality from both the base Adapter (for Django ORM
10+
integration) and FilteredAdapter (for selective policy loading) to provide
11+
optimized policy management for authorization systems.
12+
"""
13+
14+
from enum import Enum
15+
16+
from casbin import persist
17+
from casbin.model import Model
18+
from casbin.persist import FilteredAdapter
19+
from casbin_adapter.adapter import Adapter
20+
from casbin_adapter.models import CasbinRule
21+
from django.db.models import QuerySet
22+
23+
from openedx_authz.engine.filter import Filter
24+
25+
26+
class PolicyAttribute(Enum):
27+
"""
28+
Enumeration of Casbin policy attributes.
29+
30+
These attributes map to the columns of the CasbinRule table, but their meaning
31+
depends on the policy type (ptype). Check the ``openedx_authz.engine.Filter`` class
32+
for more details.
33+
"""
34+
35+
PTYPE = "ptype"
36+
"""ptype (str): Type of policy"""
37+
38+
V0 = "v0"
39+
"""v0 (str): First policy value."""
40+
41+
V1 = "v1"
42+
"""v1 (str): Second policy value."""
43+
44+
V2 = "v2"
45+
"""v2 (str): Third policy value."""
46+
47+
V3 = "v3"
48+
"""v3 (str): Fourth policy value."""
49+
50+
V4 = "v4"
51+
"""v4 (str): Fifth policy value."""
52+
53+
V5 = "v5"
54+
"""v5 (str): Sixth policy value."""
55+
56+
57+
class ExtendedAdapter(Adapter, FilteredAdapter):
58+
"""
59+
Extended Casbin adapter with filtering capabilities.
60+
61+
This adapter extends the base Django ORM Casbin adapter to support filtered
62+
policy loading, allowing for more efficient policy management by loading
63+
only relevant policy rules based on specified filter criteria.
64+
65+
Inherits from:
66+
Adapter: Base Django adapter for Casbin policy persistence.
67+
FilteredAdapter: Interface for filtered policy loading.
68+
"""
69+
70+
def is_filtered(self) -> bool:
71+
"""
72+
Check if the adapter supports filtering.
73+
74+
Returns:
75+
bool: True if the adapter supports filtered policy loading, False otherwise.
76+
"""
77+
return True
78+
79+
def load_filtered_policy(self, model: Model, filter: Filter) -> None: # pylint: disable=redefined-builtin
80+
"""
81+
Load policy rules from storage with filtering applied.
82+
83+
This method loads policy rules from the database and applies the specified
84+
filter to load only relevant rules. The filtered rules are then loaded
85+
into the provided Casbin model.
86+
87+
IMPORTANT: This method is used internally by the ``enforcer.load_filtered_policy()``
88+
method. Do not call this method directly. If you need to load policy rules, use
89+
the ``enforcer.load_filtered_policy()`` method.
90+
91+
Args:
92+
model (Model): The Casbin model to load policy rules into.
93+
filter (Filter): Filter object containing criteria for policy selection.
94+
Should have attributes like ptype, v0, v1, etc. with lists
95+
of values to filter by.
96+
"""
97+
queryset = CasbinRule.objects.using(self.db_alias)
98+
filtered_queryset = self.filter_query(queryset, filter)
99+
for line in filtered_queryset:
100+
persist.load_policy_line(str(line), model)
101+
102+
def filter_query(self, queryset: QuerySet, filter: Filter) -> QuerySet: # pylint: disable=redefined-builtin
103+
"""
104+
Apply filter criteria to the policy queryset.
105+
106+
This method takes a Django queryset of CasbinRule objects and applies
107+
filtering based on the provided filter object's attributes. It supports
108+
filtering by policy type (ptype) and policy values (v0-v5).
109+
110+
Args:
111+
queryset (QuerySet): Django queryset of CasbinRule objects to filter.
112+
filter (Filter): Filter object with attributes (ptype, v0, v1, v2, v3, v4, v5)
113+
containing lists of values to filter by. Empty lists are ignored.
114+
115+
Returns:
116+
QuerySet: Filtered and ordered queryset of CasbinRule objects.
117+
"""
118+
for attr in PolicyAttribute:
119+
filter_values = getattr(filter, attr.value)
120+
if len(filter_values) > 0:
121+
filter_kwargs = {f"{attr.value}__in": filter_values}
122+
queryset = queryset.filter(**filter_kwargs)
123+
return queryset.order_by("id")

openedx_authz/engine/enforcer.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""
2+
Core authorization enforcer for Open edX AuthZ system.
3+
4+
Provides a Casbin FastEnforcer instance with extended adapter for database policy
5+
storage and Redis watcher for distributed policy synchronization.
6+
7+
Components:
8+
- Enforcer: Main FastEnforcer instance for policy evaluation
9+
- Adapter: ExtendedAdapter for filtered database policy loading
10+
- Watcher: Redis-based watcher for real-time policy updates
11+
12+
Usage:
13+
from openedx_authz.engine.enforcer import enforcer
14+
allowed = enforcer.enforce(user, resource, action)
15+
16+
Requires `CASBIN_MODEL` setting and Redis configuration for watcher functionality.
17+
"""
18+
19+
import logging
20+
21+
from casbin import FastEnforcer
22+
from django.conf import settings
23+
24+
from openedx_authz.engine.adapter import ExtendedAdapter
25+
from openedx_authz.engine.watcher import Watcher
26+
27+
logger = logging.getLogger(__name__)
28+
29+
adapter = ExtendedAdapter()
30+
enforcer = FastEnforcer(settings.CASBIN_MODEL, adapter, enable_log=True)
31+
enforcer.enable_auto_save(True)
32+
33+
if Watcher:
34+
try:
35+
enforcer.set_watcher(Watcher)
36+
logger.info("Watcher successfully set on Casbin enforcer")
37+
except Exception as e: # pylint: disable=broad-exception-caught
38+
logger.error(f"Failed to set watcher on Casbin enforcer: {e}")

0 commit comments

Comments
 (0)