Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 2 additions & 22 deletions cms/djangoapps/contentstore/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.edxmako.services import MakoService
from common.djangoapps.student import auth
from common.djangoapps.student.auth import STUDIO_EDIT_ROLES, has_studio_read_access, has_studio_write_access
from common.djangoapps.student.auth import STUDIO_EDIT_ROLES, has_studio_write_access
from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole, GlobalStaff
from common.djangoapps.track import contexts
Expand All @@ -71,6 +71,7 @@
from common.djangoapps.xblock_django.user_service import DjangoXBlockUserService
from openedx.core import toggles as core_toggles
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.content.services import StudioPermissionsService
from openedx.core.djangoapps.content_libraries.api import get_container
from openedx.core.djangoapps.content_tagging.toggles import is_tagging_feature_disabled
from openedx.core.djangoapps.credit.api import get_credit_requirements, is_credit_course
Expand Down Expand Up @@ -2262,27 +2263,6 @@ def get_group_configurations_context(course, store):
return context


class StudioPermissionsService:
"""
Service that can provide information about a user's permissions.

Deprecated. To be replaced by a more general authorization service.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Hmm, this StudioPermissionsService is marked as Deprecated, and now this PR is un-deprecating it. I wonder who marked it as deprecated and what the replacement is."

(looks at git blame)

Oh.


Only used by LegacyLibraryContentBlock (and library_tools.py).
"""

def __init__(self, user):
self._user = user

def can_read(self, course_key):
""" Does the user have read access to the given course/library? """
return has_studio_read_access(self._user, course_key)

def can_write(self, course_key):
""" Does the user have read access to the given course/library? """
return has_studio_write_access(self._user, course_key)


def track_course_update_event(course_key, user, course_update_content=None):
"""
Track course update event
Expand Down
3 changes: 2 additions & 1 deletion cms/djangoapps/contentstore/views/preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from common.djangoapps.student.models import anonymous_id_for_user
from common.djangoapps.xblock_django.user_service import DjangoXBlockUserService
from lms.djangoapps.lms_xblock.field_data import LmsFieldData
from openedx.core.djangoapps.content.services import StudioPermissionsService
from openedx.core.djangoapps.discussions.services import DiscussionConfigService
from openedx.core.djangoapps.video_config.services import VideoConfigService
from openedx.core.lib.cache_utils import CacheService
Expand All @@ -44,7 +45,7 @@
from xmodule.util.sandboxing import SandboxService
from xmodule.x_module import AUTHOR_VIEW, PREVIEW_VIEWS, STUDENT_VIEW, XModuleMixin

from ..utils import StudioPermissionsService, get_visibility_partition_info
from ..utils import get_visibility_partition_info
from .access import get_user_role
from .session_kv_store import SessionKeyValueStore

Expand Down
26 changes: 26 additions & 0 deletions openedx/core/djangoapps/authz/tests/fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
""" Fixtures for AuthZ-aware tests """
import casbin
import pkg_resources
from openedx_authz.engine.enforcer import AuthzEnforcer
from openedx_authz.engine.utils import migrate_policy_between_enforcers


def seed_policies():
"""Seed the database with AuthZ policies."""
global_enforcer = AuthzEnforcer.get_enforcer()
global_enforcer.load_policy()

model_path = pkg_resources.resource_filename(
"openedx_authz.engine",
"config/model.conf",
)

policy_path = pkg_resources.resource_filename(
"openedx_authz.engine",
"config/authz.policy",
)

migrate_policy_between_enforcers(
source_enforcer=casbin.Enforcer(model_path, policy_path),
target_enforcer=global_enforcer,
)
27 changes: 2 additions & 25 deletions openedx/core/djangoapps/authz/tests/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@

from unittest.mock import patch

import casbin
import pkg_resources
from openedx_authz.api.users import assign_role_to_user_in_scope
from openedx_authz.constants.roles import COURSE_STAFF
from openedx_authz.engine.enforcer import AuthzEnforcer
from openedx_authz.engine.utils import migrate_policy_between_enforcers
from rest_framework.test import APIClient

from common.djangoapps.student.tests.factories import UserFactory
from openedx.core import toggles as core_toggles
from openedx.core.djangoapps.authz.tests.fixtures import seed_policies


class CourseAuthoringAuthzTestMixin:
Expand Down Expand Up @@ -44,7 +42,7 @@ def tearDownClass(cls):
def setUp(self):
super().setUp()

self._seed_policies()
seed_policies()

self.authorized_user = UserFactory(password=self.password)
self.unauthorized_user = UserFactory(password=self.password)
Expand Down Expand Up @@ -76,27 +74,6 @@ def add_user_to_role_in_course(self, user, role, course_key):
)
AuthzEnforcer.get_enforcer().load_policy()

@classmethod
def _seed_policies(cls):
"""Seed the database with AuthZ policies."""
global_enforcer = AuthzEnforcer.get_enforcer()
global_enforcer.load_policy()

model_path = pkg_resources.resource_filename(
"openedx_authz.engine",
"config/model.conf",
)

policy_path = pkg_resources.resource_filename(
"openedx_authz.engine",
"config/authz.policy",
)

migrate_policy_between_enforcers(
source_enforcer=casbin.Enforcer(model_path, policy_path),
target_enforcer=global_enforcer,
)


class CourseAuthzTestMixin(CourseAuthoringAuthzTestMixin):
"""
Expand Down
184 changes: 184 additions & 0 deletions openedx/core/djangoapps/content/services.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
"""
Services for learning content
"""
from __future__ import annotations

import warnings
from collections.abc import Callable
from typing import Literal

from opaque_keys.edx.keys import LearningContextKey
from opaque_keys.edx.locator import CourseLocator, LibraryLocatorV2
from openedx_authz.api import is_user_allowed
from openedx_authz.constants.permissions import (
COURSES_CREATE_FILES,
COURSES_EDIT_COURSE_CONTENT,
COURSES_EDIT_FILES,
COURSES_MANAGE_COURSE_UPDATES,
COURSES_VIEW_COURSE,
COURSES_VIEW_FILES,
EDIT_LIBRARY_CONTENT,
REUSE_LIBRARY_CONTENT,
VIEW_LIBRARY,
)
from openedx_authz.data import PermissionData

from common.djangoapps.student.auth import has_studio_read_access, has_studio_write_access
from common.djangoapps.student.roles import enable_authz_course_authoring
from openedx.core.djangoapps.content_tagging.rules import UserType

COMMON_PERM_KEY = Literal[
"read_content",
"edit_content",
"read_files",
"edit_files",
"create_files",
"reuse_content",
"manage_updates",
]
CONTEXT_TYPES = Literal["course", "library"]


# Maps conceptual permissions to how they're applied to a particular learning context.
# Note, for instance, that courses need a special file permission that libraries don't need,
# since library files are constrained to the blocks whereas course files are course-wide.
#
# This can be expanded with more permissions from the AuthZ constants if they're needed later
# on following the patterns in this file.
PERMISSION_MAP: dict[COMMON_PERM_KEY, dict[CONTEXT_TYPES, PermissionData]] = {
"read_content": {
"course": COURSES_VIEW_COURSE,
"library": VIEW_LIBRARY,
},
"edit_content": {
"course": COURSES_EDIT_COURSE_CONTENT,
"library": EDIT_LIBRARY_CONTENT,
},
"read_files": {
"course": COURSES_VIEW_FILES,
"library": VIEW_LIBRARY,
},
"create_files": {
"course": COURSES_CREATE_FILES,
"library": EDIT_LIBRARY_CONTENT,
},
"edit_files": {
"course": COURSES_EDIT_FILES,
"library": EDIT_LIBRARY_CONTENT,
},
"reuse_content": {
"library": REUSE_LIBRARY_CONTENT,
},
"manage_updates": {
"course": COURSES_MANAGE_COURSE_UPDATES,
}
}


def perm_for_key(perm_label: COMMON_PERM_KEY, context_key: LearningContextKey) -> PermissionData:
"""Returns the appropriate permission data for a context key."""
if perm_label not in PERMISSION_MAP:
raise ValueError(f"Unsupported permission category, {perm_label}.")
match context_key:
case LibraryLocatorV2():
try:
return PERMISSION_MAP[perm_label]["library"]
except KeyError as err:
raise TypeError(f"Permission category not applicable to libraries: {perm_label}") from err
case CourseLocator():
try:
return PERMISSION_MAP[perm_label]["course"]
except KeyError as err:
raise TypeError(f"Permission category not applicable to courses: {perm_label}") from err
raise TypeError(f"{context_key} is not a recognized LearningContextKey.")


def _course_fallback_check(fallback_function: Callable[[UserType, CourseLocator], bool]):
"""
AuthZ isn't universal yet. When dealing with courses, fall back to the old permissions
system if authz is disabled for a course. Remove this decorator when the old system is
fully removed and AuthZ is the only way we do checks.
"""
def wrapper(function):
def wrapped(self: StudioPermissionsService, context_key: LearningContextKey):
if isinstance(context_key, CourseLocator):
if enable_authz_course_authoring(context_key):
return function(self, context_key)
return fallback_function(self._user, context_key) # pylint: disable=protected-access
return function(self, context_key)
return wrapped
return wrapper


class StudioPermissionsService:
"""
Service that can provide information about a user's permissions.
"""

def __init__(self, user) -> None:
self._user = user

def _check_permission(self, perm_label: COMMON_PERM_KEY, context_key: LearningContextKey) -> bool:
""" Verify that a user has a specific permission for this context. """
return self._user.is_active and is_user_allowed(
self._user,
perm_for_key(perm_label, context_key).identifier,
str(context_key),
)

@_course_fallback_check(has_studio_read_access)
def can_read_content(self, context_key: LearningContextKey) -> bool:
""" Can the user read the content of this course/library? """
return self._check_permission("read_content", context_key)

@_course_fallback_check(has_studio_write_access)
def can_edit_content(self, context_key: LearningContextKey) -> bool:
""" Can the user edit the content of this course/library? """
return self._check_permission("edit_content", context_key)

@_course_fallback_check(has_studio_read_access)
def can_read_files(self, context_key: LearningContextKey) -> bool:
""" Can the user read files for this course/library? """
return self._check_permission("read_files", context_key)

@_course_fallback_check(has_studio_write_access)
def can_create_files(self, context_key: LearningContextKey) -> bool:
""" Can the user create files for this course/library? """
return self._check_permission("create_files", context_key)

@_course_fallback_check(has_studio_write_access)
def can_edit_files(self, context_key: LearningContextKey) -> bool:
""" Can the user edit files for this course/library? """
return self._check_permission("edit_files", context_key)

def can_read(self, context_key: LearningContextKey) -> bool:
""" Does the user have read access to the given course/library? """
warnings.warn(
"can_read is deprecated. "
"Use a more specific permission, like can_read_content or can_read_files instead. "
"See https://github.com/openedx/openedx-platform/issues/37409.",
DeprecationWarning,
stacklevel=2,
)
return has_studio_read_access(self._user, context_key)

def can_write(self, context_key: LearningContextKey) -> bool:
""" Does the user have write access to the given course/library? """
warnings.warn(
"can_write is deprecated. "
"Use a more specific permission, like can_edit_content, can_create_files, "
"or can_edit_files instead. "
"See https://github.com/openedx/openedx-platform/issues/37409.",
DeprecationWarning,
stacklevel=2,
)
return has_studio_write_access(self._user, context_key)

def can_reuse_content(self, context_key: LibraryLocatorV2) -> bool:
""" Does the user have the ability to reuse content from this library? """
return self._check_permission("reuse_content", context_key)

@_course_fallback_check(has_studio_write_access)
def can_manage_updates(self, context_key: CourseLocator) -> bool:
""" Does the user have the ability to manage updates for this course? """
return self._check_permission("manage_updates", context_key)
Empty file.
Loading
Loading