Skip to content

Commit ec9785f

Browse files
committed
fixup! feat: add course authoring migration and rollback scripts
1 parent e627dd7 commit ec9785f

6 files changed

Lines changed: 67 additions & 42 deletions

File tree

openedx_authz/admin.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from django.contrib import admin
66

77
from openedx_authz.models import ExtendedCasbinRule
8+
from openedx_authz.models.scopes import CourseScope
9+
from openedx_authz.models.subjects import UserSubject
810

911

1012
class CasbinRuleForm(forms.ModelForm):
@@ -48,3 +50,18 @@ class CasbinRuleAdmin(admin.ModelAdmin):
4850
# TODO: In a future, possibly we should only show an inline for the rules that
4951
# have an extended rule, and show the subject and scope information in detail.
5052
inlines = [ExtendedCasbinRuleInline]
53+
54+
55+
@admin.register(ExtendedCasbinRule)
56+
class ExtendedCasbinRuleAdmin(admin.ModelAdmin):
57+
pass
58+
59+
60+
@admin.register(UserSubject)
61+
class UserSubjectAdmin(admin.ModelAdmin):
62+
pass
63+
64+
65+
@admin.register(CourseScope)
66+
class CourseScopeAdmin(admin.ModelAdmin):
67+
list_display = ("id", "course_overview")

openedx_authz/engine/utils.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from casbin import Enforcer
1010

11+
from openedx_authz.api.data import CourseOverviewData
1112
from openedx_authz.api.users import (
1213
assign_role_to_user_in_scope,
1314
batch_assign_role_to_users_in_scope,
@@ -193,7 +194,9 @@ def migrate_legacy_course_roles_to_authz(CourseAccessRole, delete_after_migratio
193194
param CourseAccessRole: The CourseAccessRole model to use.
194195
"""
195196

196-
legacy_permissions = CourseAccessRole.objects.select_related("user").all()
197+
legacy_permissions = (
198+
CourseAccessRole.objects.filter(course_id__startswith="course-v1:").select_related("user").all()
199+
)
197200

198201
# List to keep track of any permissions that could not be migrated
199202
permissions_with_errors = []
@@ -224,11 +227,20 @@ def migrate_legacy_course_roles_to_authz(CourseAccessRole, delete_after_migratio
224227
f"to Role: {role.external_key} in Scope: {permission.course_id}"
225228
)
226229

227-
assign_role_to_user_in_scope(
230+
is_user_added = assign_role_to_user_in_scope(
228231
user_external_key=permission.user.username,
229232
role_external_key=role.external_key,
230233
scope_external_key=str(permission.course_id),
231234
)
235+
236+
if not is_user_added:
237+
logger.error(
238+
f"Failed to migrate permission for User: {permission.user.username} "
239+
f"to Role: {role.external_key} in Scope: {permission.course_id}"
240+
)
241+
permissions_with_errors.append(permission)
242+
continue
243+
232244
permissions_with_no_errors.append(permission)
233245

234246
if delete_after_migration:
@@ -264,6 +276,10 @@ def migrate_authz_to_legacy_course_roles(CourseAccessRole, UserSubject, delete_a
264276
role_assignments = get_user_role_assignments(user_external_key=user_external_key)
265277

266278
for assignment in role_assignments:
279+
if not isinstance(assignment.scope, CourseOverviewData):
280+
logger.error(f"Skipping role assignment for User: {user_external_key} due to missing course scope.")
281+
continue
282+
267283
scope = assignment.scope.external_key
268284

269285
course_overview = assignment.scope.get_object()

openedx_authz/management/commands/authz_migrate_course_authoring.py

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,31 +34,26 @@ def handle(self, *args, **options):
3434
self.stdout.write(self.style.WARNING("Starting legacy → Authz migration..."))
3535

3636
try:
37+
if delete_after_migration:
38+
confirm = input(
39+
"Are you sure you want to delete successfully migrated legacy roles? Type 'yes' to continue: "
40+
)
41+
42+
if confirm != "yes":
43+
self.stdout.write(self.style.WARNING("Deletion aborted."))
44+
return
3745
with transaction.atomic():
3846
errors = migrate_legacy_course_roles_to_authz(
3947
CourseAccessRole=CourseAccessRole,
40-
delete_after_migration=False, # control deletion here instead
48+
delete_after_migration=delete_after_migration,
4149
)
4250

4351
if errors:
4452
self.stdout.write(self.style.ERROR(f"Migration completed with {len(errors)} errors."))
4553
else:
4654
self.stdout.write(self.style.SUCCESS("Migration completed successfully with no errors."))
4755

48-
# Handle deletion separately for safety
4956
if delete_after_migration:
50-
confirm = input(
51-
"Are you sure you want to delete successfully migrated legacy roles? Type 'yes' to continue: "
52-
)
53-
54-
if confirm != "yes":
55-
self.stdout.write(self.style.WARNING("Deletion aborted."))
56-
return
57-
58-
migrated_ids = [p.id for p in CourseAccessRole.objects.all() if p not in errors]
59-
60-
CourseAccessRole.objects.filter(id__in=migrated_ids).delete()
61-
6257
self.stdout.write(self.style.SUCCESS("Legacy roles deleted successfully."))
6358

6459
except Exception as exc:

openedx_authz/management/commands/authz_rollback_course_authoring.py

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -36,36 +36,28 @@ def handle(self, *args, **options):
3636
self.stdout.write(self.style.WARNING("Starting Authz → Legacy rollback migration..."))
3737

3838
try:
39+
if delete_after_migration:
40+
confirm = input(
41+
"Are you sure you want to remove the new Authz role "
42+
"assignments after rollback? Type 'yes' to continue: "
43+
)
44+
45+
if confirm != "yes":
46+
self.stdout.write(self.style.WARNING("Deletion aborted."))
47+
return
3948
with transaction.atomic():
4049
errors = migrate_authz_to_legacy_course_roles(
4150
CourseAccessRole=CourseAccessRole,
4251
UserSubject=UserSubject,
43-
delete_after_migration=False, # control deletion here
52+
delete_after_migration=delete_after_migration, # control deletion here
4453
)
4554

4655
if errors:
4756
self.stdout.write(self.style.ERROR(f"Rollback completed with {len(errors)} errors."))
4857
else:
4958
self.stdout.write(self.style.SUCCESS("Rollback completed successfully with no errors."))
5059

51-
# Handle deletion separately for safety
5260
if delete_after_migration:
53-
confirm = input(
54-
"Are you sure you want to remove the new Authz role "
55-
"assignments after rollback? Type 'yes' to continue: "
56-
)
57-
58-
if confirm != "yes":
59-
self.stdout.write(self.style.WARNING("Deletion aborted."))
60-
return
61-
62-
# Re-run with deletion enabled
63-
migrate_authz_to_legacy_course_roles(
64-
CourseAccessRole=CourseAccessRole,
65-
UserSubject=UserSubject,
66-
delete_after_migration=True,
67-
)
68-
6961
self.stdout.write(self.style.SUCCESS("Authz role assignments removed successfully."))
7062

7163
except Exception as exc:

openedx_authz/models/core.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ class Scope(BaseRegistryModel):
114114
class Meta:
115115
abstract = False
116116

117+
def __str__(self):
118+
return f"Scope (namespace={self.NAMESPACE})"
119+
117120

118121
class Subject(BaseRegistryModel):
119122
"""Model representing a subject in the authorization system.
@@ -132,6 +135,9 @@ class Subject(BaseRegistryModel):
132135
class Meta:
133136
abstract = False
134137

138+
def __str__(self):
139+
return f"Subject (namespace={self.NAMESPACE})"
140+
135141

136142
class ExtendedCasbinRule(models.Model):
137143
"""Extended model for Casbin rules to store additional metadata.
@@ -182,6 +188,9 @@ class Meta:
182188
verbose_name = "Extended Casbin Rule"
183189
verbose_name_plural = "Extended Casbin Rules"
184190

191+
def __str__(self):
192+
return f"ExtendedCasbinRule for {self.casbin_rule}"
193+
185194
@classmethod
186195
def create_based_on_policy(
187196
cls,

openedx_authz/tests/test_migrations.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -721,7 +721,7 @@ def test_authz_migrate_course_authoring_command(self, mock_migrate):
721721
mock_migrate.assert_called_once()
722722
args, kwargs = mock_migrate.call_args
723723

724-
self.assertEqual(kwargs["delete_after_migration"], False)
724+
self.assertEqual(kwargs["delete_after_migration"], True)
725725

726726
@patch("openedx_authz.management.commands.authz_rollback_course_authoring.CourseAccessRole", CourseAccessRole)
727727
@patch("openedx_authz.management.commands.authz_rollback_course_authoring.migrate_authz_to_legacy_course_roles")
@@ -747,12 +747,8 @@ def test_authz_rollback_course_authoring_command(self, mock_rollback):
747747
with patch("builtins.input", return_value="yes"):
748748
call_command("authz_rollback_course_authoring", "--delete")
749749

750-
# First call (normal)
751-
# Second call (with deletion=True)
752-
self.assertEqual(mock_rollback.call_count, 2)
750+
self.assertEqual(mock_rollback.call_count, 1)
753751

754-
first_call_kwargs = mock_rollback.call_args_list[0][1]
755-
second_call_kwargs = mock_rollback.call_args_list[1][1]
752+
call_kwargs = mock_rollback.call_args_list[0][1]
756753

757-
self.assertEqual(first_call_kwargs["delete_after_migration"], False)
758-
self.assertEqual(second_call_kwargs["delete_after_migration"], True)
754+
self.assertEqual(call_kwargs["delete_after_migration"], True)

0 commit comments

Comments
 (0)