Skip to content
Open
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
10 changes: 7 additions & 3 deletions common/djangoapps/student/roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -532,9 +532,13 @@ def _authz_get_orgs_for_user(self, user) -> list[str]:
Returns a list of org short names for the user with given role.
AuthZ compatibility layer
"""
# TODO: This will be implemented on Milestone 1
# of the Authz for Course Authoring project
return []
role = get_authz_role_from_legacy_role(self._role_name)
assignments = authz_api.get_user_role_assignments_filtered(
Copy link
Copy Markdown
Member

@mariajgrimaldi mariajgrimaldi Mar 31, 2026

Choose a reason for hiding this comment

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

out of scope question: Is this filtering only for courses, or could it be used for libraries as well? I understand that roles are not shared between namespaces, but it worries me that we're relying on this implicit condition to not mix namespace scopes.

EDIT: although roles are specified per namespace, so it should be enough using a course role here to only get course scopes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'll resolve my comment.

Copy link
Copy Markdown
Member

@mariajgrimaldi mariajgrimaldi Apr 1, 2026

Choose a reason for hiding this comment

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

(non-blocking) I'm going to resurface my question because of this discussion we're having here: https://github.com/openedx/openedx-platform/pull/38199/changes#r3015158428. Is this approach also applicable here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, here since we are passing a role (for example, course_auditor), it will only return course assignments (both from CourseOverviewData and OrgCourseOverviewGlobData). It works as you mentioned. There cannot be a course_auditor with assignments in libraries, only course assignments can exist. If the role is for libraries, then it will only return library assignments.

user_external_key=user.username,
role_external_key=role,
)
orgs = {assignment.scope.org for assignment in assignments if assignment.scope.org is not None}
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.

I have one doubt about the desired functionality for this function.

My first impression was that this function should return the list of org-wide assignations.

However on further inspection, I see that the old code doesn't make a distinction between CourseAccessRoles with or without course_id, and according to the docstring at CourseAccessRole definiton, org-wide assignations are the ones that have course_id as None.

So just to confirm that we are all on the same understanding, this function (legacy version or new one), returns all Orgs to which the user has at least access to one course, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, both _legacy_get_orgs_for_user and _authz_get_orgs_for_user return all orgs where the user has at least one assignment with that role.

return list(orgs)

def _legacy_get_orgs_for_user(self, user) -> list[str]:
"""
Expand Down
66 changes: 54 additions & 12 deletions common/djangoapps/student/tests/test_roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,44 @@
Tests of student.roles
"""

from unittest.mock import patch

import ddt
from unittest.mock import patch
from django.contrib.auth.models import Permission
from django.test import TestCase
from edx_toggles.toggles.testutils import override_waffle_flag
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locator import LibraryLocator

from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from openedx_authz.api.data import ContentLibraryData, RoleAssignmentData, RoleData, UserData
from openedx_authz.api.data import ContentLibraryData, CourseOverviewData, RoleAssignmentData, RoleData, UserData
from openedx_authz.constants.roles import COURSE_ADMIN, COURSE_STAFF
from openedx_authz.engine.enforcer import AuthzEnforcer

from common.djangoapps.student.admin import CourseAccessRoleHistoryAdmin
from common.djangoapps.student.models import CourseAccessRoleHistory, User
from common.djangoapps.student.role_helpers import get_course_roles, has_staff_roles
from common.djangoapps.student.roles import (
ROLE_CACHE_UNGROUPED_ROLES__KEY,
AuthzCompatCourseAccessRole,
CourseAccessRole,
CourseBetaTesterRole,
CourseDataResearcherRole,
CourseFinanceAdminRole,
CourseInstructorRole,
CourseRole,
CourseLimitedStaffRole,
CourseStaffRole,
CourseFinanceAdminRole,
CourseRole,
CourseSalesAdminRole,
LibraryUserRole,
CourseDataResearcherRole,
CourseStaffRole,
GlobalStaff,
LibraryUserRole,
OrgContentCreatorRole,
OrgInstructorRole,
OrgStaffRole,
RoleCache,
get_authz_compat_course_access_roles_for_user,
get_role_cache_key_for_course,
ROLE_CACHE_UNGROUPED_ROLES__KEY
)
from common.djangoapps.student.role_helpers import get_course_roles, has_staff_roles
from common.djangoapps.student.tests.factories import AnonymousUserFactory, InstructorFactory, StaffFactory, UserFactory
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from openedx.core.toggles import AUTHZ_COURSE_AUTHORING_FLAG


Expand Down Expand Up @@ -239,9 +239,51 @@ def test_get_orgs_for_user(self):
role_second_org.add_users(self.student)
assert len(role.get_orgs_for_user(self.student)) == 2

@override_waffle_flag(AUTHZ_COURSE_AUTHORING_FLAG, active=True)
def test_get_orgs_for_user_authz(self):
"""
Test get_orgs_for_user using AuthZ compatibility layer
"""
role = CourseStaffRole(self.course_key)

other_org = "MIT"
other_course_key = CourseKey.from_string(f"course-v1:{other_org}+Javascript+2026_T1")
another_course_key = CourseKey.from_string(f"course-v1:{other_org}+Python+2026_T1")

staff_authz_role = RoleData(external_key=COURSE_STAFF)
instructor_authz_role = RoleData(external_key=COURSE_ADMIN)

assignments = [
RoleAssignmentData(
subject=UserData(external_key=self.student.username),
roles=[staff_authz_role],
scope=CourseOverviewData(external_key=str(self.course_key)),
),
RoleAssignmentData(
subject=UserData(external_key=self.student.username),
roles=[staff_authz_role],
scope=CourseOverviewData(external_key=str(other_course_key)),
),
RoleAssignmentData(
subject=UserData(external_key=self.student.username),
roles=[staff_authz_role],
scope=CourseOverviewData(external_key=str(another_course_key)),
),
# Non-matching role should be ignored
RoleAssignmentData(
subject=UserData(external_key=self.student.username),
roles=[instructor_authz_role],
scope=CourseOverviewData(external_key=str(self.course_key)),
),
]

with patch("openedx_authz.api.users.get_user_role_assignments_filtered", return_value=assignments):
result = role.get_orgs_for_user(self.student)
self.assertCountEqual(result, [self.course_key.org, other_org])

def test_get_authz_compat_course_access_roles_for_user(self):
"""
Thest that get_authz_compat_course_access_roles_for_user doesn't crash when the user
Test that get_authz_compat_course_access_roles_for_user doesn't crash when the user
has Libraries V2 or other non-course roles in their assignments.
"""
lib_assignment = RoleAssignmentData(
Expand Down
3 changes: 2 additions & 1 deletion requirements/edx/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,7 @@ edx-opaque-keys[django]==3.1.0
edx-organizations==7.3.0
# via
# -r requirements/edx/kernel.in
# openedx-authz
# openedx-core
edx-proctoring==5.2.0
# via -r requirements/edx/kernel.in
Expand Down Expand Up @@ -823,7 +824,7 @@ openedx-atlas==0.7.0
# enterprise-integrated-channels
# openedx-authz
# openedx-forum
openedx-authz==1.0.0
openedx-authz==1.2.0
# via -r requirements/edx/kernel.in
openedx-calc==5.0.0
# via
Expand Down
3 changes: 2 additions & 1 deletion requirements/edx/development.txt
Original file line number Diff line number Diff line change
Expand Up @@ -806,6 +806,7 @@ edx-organizations==7.3.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# openedx-authz
# openedx-core
edx-proctoring==5.2.0
# via
Expand Down Expand Up @@ -1373,7 +1374,7 @@ openedx-atlas==0.7.0
# enterprise-integrated-channels
# openedx-authz
# openedx-forum
openedx-authz==1.0.0
openedx-authz==1.2.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
Expand Down
3 changes: 2 additions & 1 deletion requirements/edx/doc.txt
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,7 @@ edx-opaque-keys[django]==3.1.0
edx-organizations==7.3.0
# via
# -r requirements/edx/base.txt
# openedx-authz
# openedx-core
edx-proctoring==5.2.0
# via -r requirements/edx/base.txt
Expand Down Expand Up @@ -1001,7 +1002,7 @@ openedx-atlas==0.7.0
# enterprise-integrated-channels
# openedx-authz
# openedx-forum
openedx-authz==1.0.0
openedx-authz==1.2.0
# via -r requirements/edx/base.txt
openedx-calc==5.0.0
# via
Expand Down
3 changes: 2 additions & 1 deletion requirements/edx/testing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,7 @@ edx-opaque-keys[django]==3.1.0
edx-organizations==7.3.0
# via
# -r requirements/edx/base.txt
# openedx-authz
# openedx-core
edx-proctoring==5.2.0
# via -r requirements/edx/base.txt
Expand Down Expand Up @@ -1050,7 +1051,7 @@ openedx-atlas==0.7.0
# enterprise-integrated-channels
# openedx-authz
# openedx-forum
openedx-authz==1.0.0
openedx-authz==1.2.0
# via -r requirements/edx/base.txt
openedx-calc==5.0.0
# via
Expand Down
Loading