Skip to content

Commit

Permalink
Initial commit for default DRF FilterBackend on full Open edX API
Browse files Browse the repository at this point in the history
Attempting to handle multi-tenancy protections for full API.
https://appsembler.atlassian.net/browse/ENG-176
  • Loading branch information
bryanlandia committed Jul 6, 2023
1 parent bdf22f6 commit e3af1c8
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 0 deletions.
Empty file.
110 changes: 110 additions & 0 deletions openedx/core/djangoapps/appsembler/openedx_api/filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"""
Default filter backend for full DRF API.
Set via REST_FRAMEWORK settings
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': [
'openedx.core.djangoapps.appsembler.openedx_api.filters.AppsemblerMultiTenantFilterBackend'
]
}
Implement django_filters Filterset to provide a final filter by
allowed course Organization to ensure multitenant-safe API results.
..
The APIs we want to support are (draft):
see https://courses.edx.org/api-docs/
Certificates (GeneratedCertificate.course_id__org)
Cohorts (CourseUserGroup.course_id -> no FK to courseoverview)
Completion (BlockCompletion.course_id__org)
? Course Experience ()
? Course Home ()
Courses (CourseOverview.org)
Courseware (CourseOverview.org)
Discussion (not sure how to handle... discussions aren't Django objs, just gets course via lms.djangoapps.courseware.courses.get_course_with_access)
Enrollment (CourseEnrollment.course__org)
Grades: not sure yet how these are serialized...
...or if they use PersistentGrades.
User (User.organizations__contains)
...
"""

from django.contrib.auth.models import User

from django_filters import rest_framework as filters
from django_filters import rest_framework

from completion.models import BlockCompletion
from lms.djangoapps.content.course_overviews.models import CourseOverview
from lms.djangoapps.certificates.models import GeneratedCertificate
from organizations.models import Organization
from student.models import CourseEnrollment


COURSE_PREFIX = 'course-v1:'


# TODO: Need to figure out how to limit this FilterBackend (or make essentially no-op) for some parts of API


class AppsemblerMultiTenantFilterBackend(filters.DjangoFilterBackend):

filterset_base = AllowedCourseOrgFilterSet


class AllowedCourseOrgFilterSet(filters.FilterSet):
"""Filter by allowed organization, with dynamic queryset filter.
"""
MODEL_COURSE_ORG_LOOKUPS = {
User: 'organizations__contains',
CourseEnrollment: 'course__org__',
CourseOverview: 'org',
CourseUserGroup: 'course_id__startswith',
GeneratedCertificate: 'course_id__startswith',
BlockCompletion: 'context_key__startswith',
...
}

# have no ForeignKey to CourseOverview or other path to Organization,
# just an OpaqueKeyField subclass so we have to do a string comparison
OPAQUE_KEY_FIELD_LOOKUP_MODELS = (
BlockCompletion,
CourseUserGroup,
GeneratedCertificate,
)

# org field is just a string of the name of the org.
STRING_ORG_NAME_LOOKUP_MODELS = (
CourseEnrollment,
CourseOverview,
)

# TODO: Allow a superuser to bypass filter

allowed_org = filters.BooleanFilter(method="filter_allowed_org")

def filter_allowed_org(self, queryset, name, value):
import pdb; pdb.set_trace()
requesting_user = self.request.user

try:
user_allowed_org = self.request.user.organizations.first()
except Organization.DoesNotExist:
raise # TODO: do something else

try:
lookup = MODEL_COURSE_ORG_LOOKUPS[self.queryset.model]
except KeyError:
raise # TODO: do something else

if model in OPAQUE_KEY_FIELD_LOOKUP_MODELS:
return queryset.filter(**{lookup: "{}{}+".format(COURSE_PREFIX, user_allowed_org))})
elif model in STRING_ORG_NAME_LOOKUP_MODELS:
return queryset.filter(**{lookup: user_allowed_org.short_name})
else:
return queryset.filter(**{lookup: user_allowed_org})

# don't declare an explicit model via Meta
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,12 @@ def plugin_settings(settings):

# Off by default. See the `site_configuration.tahoe_organization_helpers.py` module.
settings.FEATURES['TAHOE_SITE_CONFIG_CLIENT_ORGANIZATIONS_SUPPORT'] = False

# Use default DRF API FilterBackend to handle multi-tenancy protections for Open edX API
settings.REST_FRAMEWORK.update(
{
'DEFAULT_FILTER_BACKENDS': [
'openedx.core.djangoapps.appsembler.openedx_api.filters.AppsemblerMultiTenantFilterBackend'
]
}
)

0 comments on commit e3af1c8

Please sign in to comment.