-
Notifications
You must be signed in to change notification settings - Fork 5
[DO NOT MERGE] feat: basic casbin tests #37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d3c3487
4ba8fa9
a2feec6
97ef2a7
a64f254
24b5da4
a7e5e48
6659457
212d6ea
0bb8fcb
be6add0
e484a05
26fdd1c
a735270
1d9e857
9743a3a
f2d57ec
127d932
868bff4
62b30ca
7bffc13
851a940
c5b97a0
0dce5ad
0c2fbf3
907027b
e9165ed
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| """ | ||
| Admin for openedx_authz. | ||
| """ | ||
|
|
||
| from django.contrib import admin | ||
| from .models import Library | ||
|
|
||
| admin.site.register(Library) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"], | ||
| }, | ||
| ), | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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)) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| """ | ||
| Settings package for openedx_authz plugin. | ||
| """ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This request middleware loads the entire policy into memory using This also takes me to this question here: https://openedx.atlassian.net/wiki/spaces/OEPM/pages/5210112002/WIP+Open+edX+AuthZ+Framework+Long-Term+Vision?focusedCommentId=5220433941. Django ORM adapter which is used by this library doesn't support loading filtered policies, which again is not a viable option for us loading an entire database into memory. What I think we could do is implementing our own extended adapter which filters policies, using as guide what's already implemented in the adapter itself and also in other python adapters which do support filtering policies like https://github.com/officialpycasbin/sqlalchemy-adapter We could do something like (pseudocode, haven't tested it):
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tested this in the django shell, it seems to work: The filter will probably have to be more complex and the query used should also consider other use cases like loading for a single org etc which I haven't tested. At least I think this could work. There is still a concern about the performance implications of these queries, though.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for your comment! Yes, based on what you mentioned, we shouldn’t use the default Middleware. I also tested your implementation, but I wonder if we should do something additional. You’re calling I’m also noticing that if we don’t use the middleware, the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Based on your code and the SQLAlchemy adapter, I created this # openedx_authz.engine.adapter
from casbin import persist
from casbin.persist import FilteredAdapter
from casbin_adapter.adapter import Adapter
from casbin_adapter.models import CasbinRule
class ExtendedAdapter(Adapter, FilteredAdapter):
"""
Extended adapter for the casbin model.
"""
_filtered = False
def load_filtered_policy(self, model, filter) -> None: # pylint: disable=redefined-builtin
"""loads all policy rules from the storage."""
queryset = CasbinRule.objects.using(self.db_alias).all()
filtered_queryset = self.filter_query(queryset, filter)
for line in filtered_queryset:
persist.load_policy_line(str(line), model)
self._filtered = True
def filter_query(self, queryset, filter): # pylint: disable=redefined-builtin
"""filters the queryset based on the attributes of the filter."""
for attr in ("ptype", "v0", "v1", "v2", "v3", "v4", "v5"):
filter_values = getattr(filter, attr)
if len(filter_values) > 0:
filter_kwargs = {f"{attr}__in": filter_values}
queryset = queryset.filter(**filter_kwargs)
return queryset.order_by("id")# openedx_authz.engine.filter
class Filter:
"""
Filter class for the casbin model.
"""
ptype = []
v0 = []
v1 = []
v2 = []
v3 = []
v4 = []
v5 = []I did some tests, and it seems to work: In [1]: from openedx_authz.engine.filter import Filter
In [2]: from casbin_adapter.enforcer import enforcer as e
In [3]: f = Filter()
In [4]: f.v0 = ["alice"]
In [5]: e.load_filtered_policy(f)
2025-09-10 22:52:36,830 INFO 147 [casbin.policy] [user None] [ip None] policy.py:73 - Policy:
2025-09-10 22:52:36,830 INFO 147 [casbin.policy] [user None] [ip None] policy.py:79 - p : sub, obj, act : [['alice', 'data1', 'read']]
2025-09-10 22:52:36,831 INFO 147 [casbin.policy] [user None] [ip None] policy.py:79 - g : _, _ : [['alice', 'data2_admin']]
2025-09-10 22:52:36,831 INFO 147 [casbin.policy] [user None] [ip None] assertion.py:48 - Role links for: g
2025-09-10 22:52:36,831 INFO 147 [casbin.role] [user None] [ip None] role_manager.py:218 - alice < data2_admin
In [6]: e.get_policy()
Out[6]: [['alice', 'data1', 'read']]
In [7]: f.v0 = ["bob"]
In [8]: e.load_filtered_policy(f)
2025-09-10 22:52:48,271 INFO 147 [casbin.policy] [user None] [ip None] policy.py:73 - Policy:
2025-09-10 22:52:48,272 INFO 147 [casbin.policy] [user None] [ip None] policy.py:79 - p : sub, obj, act : [['bob', 'data2', 'write']]
2025-09-10 22:52:48,272 INFO 147 [casbin.policy] [user None] [ip None] policy.py:79 - g : _, _ : []
2025-09-10 22:52:48,272 INFO 147 [casbin.policy] [user None] [ip None] assertion.py:48 - Role links for: g
2025-09-10 22:52:48,272 INFO 147 [casbin.role] [user None] [ip None] role_manager.py:218 -
In [9]: e.get_policy()
Out[9]: [['bob', 'data2', 'write']]
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the new Casbin config using only the Django ORM Adapter: 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, "model.conf")
watcher_options = WatcherOptions()
watcher_options.host = "redis"
watcher_options.port = 6379
watcher_options.optional_update_callback = callback_function
watcher = new_watcher(watcher_options)
settings.CASBIN_WATCHER = watcher
settings.CASBIN_ADAPTER = "openedx_authz.engine.adapter.ExtendedAdapter"
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great! Where can we get the redis configurations though? I see it's in the |
||
| settings.MIDDLEWARE = settings.MIDDLEWARE + [middleware_class] | ||
|
|
||
| # Add authorization configuration | ||
| settings.CASBIN_MODEL = os.path.join(ROOT_DIRECTORY, "model.conf") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be a file part of a tutor plugin instead so it can be loaded and accessed as a volume and properly maintained by operators.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree! |
||
| settings.DAUTHZ = { | ||
| "DEFAULT": { | ||
| "MODEL": { | ||
| "CONFIG_TYPE": "file", | ||
| "CONFIG_FILE_PATH": settings.CASBIN_MODEL, | ||
| "CONFIG_TEXT": "", | ||
| }, | ||
| "ADAPTER": { | ||
| "NAME": "casbin_adapter.adapter.Adapter", | ||
| }, | ||
| "LOG": { | ||
| "ENABLED": True, | ||
| }, | ||
| }, | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| """ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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, | ||
| }, | ||
| }, | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a few comments about this approach:
Enforcer(or another most distinguished name) that behaves like this enforcer but sets up the watcher as part of its initialization (ducktyping)? We could also implement more customizations if needed.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
load_filtered_policy.CASBIN_WATCHERsetting. It could be useful to avoid the issue you mentioned. I’ll be running some tests with that configuration.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See #37 (comment)