Spike - RBAC AuthZ - Automatic Role Migration
Related ticket: https://github.com/openedx/openedx-platform/issues/38145
1. Overview
This Spike explores the implementation of an automatic role migration mechanism when the feature flag authz.enable_course_authoring changes.
The goal is to ensure that permission data remains consistent between the Legacy system (CourseAccessRole) and the new AuthZ system, without requiring manual administrator intervention.
The proposed solution uses:
Django Signals to detect changes in Waffle flags.
Celery Tasks to execute migrations asynchronously.
A tracking model (
MigrationTask) for visibility and control.
2. Context
Currently, there are Django management commands that can be executed manually to perform the migration process at the course or organization level:
authz_migrate_course_authoring(forward)authz_rollback_course_authoring(rollback)
According to ADRs (#10 and #11):
Migration must occur automatically when the flag state changes (enabled/disabled)
The system must respect the scope:
Course
Organization
3. Problem
Currently:
Migration is manual.
There is no status tracking.
There is no concurrency protection.
There is no user feedback.
This can lead to:
Permission inconsistencies.
Race conditions.
Lack of observability.
4. Objectives
Define how to automatically trigger the migration.
Ensure that only one migration per scope runs at a time.
Design a status tracking mechanism.
Define how to inform the user about the result.
Validate that the process is safe, scalable, and decoupled.
5. Proposed Architecture
5.1 MigrationTask Model
A model is introduced for tracking:
class MigrationTask(models.Model):
task_id = models.CharField(max_length=255, unique=True)
migration_type = models.CharField(max_length=20) # forward / rollback
scope_type = models.CharField(max_length=20) # course / org
scope_key = models.CharField(max_length=255)
status = models.CharField(max_length=20) # pending, running, completed, failed
error_message = models.TextField(null=True, blank=True)
metadata = models.JSONField(default=dict)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)5.2 Trigger de Migración (Signals)
It is proposed to use Django signals (pre_save) on:
WaffleFlagCourseOverrideModelWaffleFlagOrgOverrideModel
Flow:
Detect change in
authz.enable_course_authoringDetect transition:
False → True→ Forward migrationTrue → False→ Rollback
Determine scope:
Course
Organization
Trigger Celery task
5.3 Celery Tasks
Main task:
def migrate_course_authoring_async(migration_type, scope_type, scope_key)Responsibilities:
Implement locking using Django cache/Redis.
Create a
MigrationTaskrecord and update its status during execution.Execute migration using existing functions in engine/utils.py
Handle errors.
5.4 Locking Strategy
To avoid race conditions:
lock_key = f"authz_migration:{scope_type}:{scope_key}"Implemented using
cache.add()Default TTL: 1 hour
Behavior:
If the lock exists → the new migration is ignored.
Prevents concurrent executions.
6. Execution Flow
User changes the flag.
Signal detects the change.
Celery task is triggered.
Task:
Acquires lock
Executes migration
Updates status
User checks status via Django Admin using the
MigrationTaskmodel
7. Handling Critical Cases
7.1 Race Conditions
Problem: Rapid flag changes (enable/disable)
Solution:
Lock per scope.
Async execution.
Ignore concurrent executions.
7.2 Migration Failures
Use of atomic transactions.
Do not delete data if migration fails.
Log the error in
MigrationTask.
Open Questions
Will migration run globally (
Flag)?Automatic migration will not run for global flags. The main reason is that, depending on the instance, it could impact hundreds or thousands of courses, which introduces a high performance risk.
Should the TTL be configurable?
Should we create a REST API/command to check migration status?