Skip to content
12 changes: 8 additions & 4 deletions cms/djangoapps/contentstore/tests/test_course_listing.py
Original file line number Diff line number Diff line change
Expand Up @@ -936,7 +936,9 @@ def test_get_course_keys_from_scopes_with_platform_scope(self):
"is_enabled",
side_effect=self._mock_authz_toggle(enabled_keys),
):
course_keys = _get_course_keys_from_scopes([PlatformCourseOverviewGlobData(external_key="course-v1:*")])
course_keys = _get_course_keys_from_scopes([
PlatformCourseOverviewGlobData(external_key=PlatformCourseOverviewGlobData.build_external_key())
])

assert course_keys == set(authz_keys) | set(legacy_keys)

Expand All @@ -953,7 +955,9 @@ def test_get_course_keys_from_scopes_with_platform_scope_global_flag_enabled(sel
"is_enabled",
side_effect=self._mock_authz_toggle(enabled_keys, global_enabled=True),
):
course_keys = _get_course_keys_from_scopes([PlatformCourseOverviewGlobData(external_key="course-v1:*")])
course_keys = _get_course_keys_from_scopes([
PlatformCourseOverviewGlobData(external_key=PlatformCourseOverviewGlobData.build_external_key())
])

assert course_keys == set(CourseOverview.get_all_courses().values_list("id", flat=True))

Expand All @@ -972,8 +976,8 @@ def test_get_course_keys_from_scopes_platform_scope_short_circuits(self):
):
course_keys = _get_course_keys_from_scopes(
[
OrgCourseOverviewGlobData(external_key="course-v1:Org1+*"),
PlatformCourseOverviewGlobData(external_key="course-v1:*"),
OrgCourseOverviewGlobData(external_key=OrgCourseOverviewGlobData.build_external_key("Org1")),
PlatformCourseOverviewGlobData(external_key=PlatformCourseOverviewGlobData.build_external_key()),
]
)

Expand Down
6 changes: 6 additions & 0 deletions common/djangoapps/student/roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from openedx_authz.api import users as authz_api
from openedx_authz.api.data import CourseOverviewData, OrgCourseOverviewGlobData, RoleAssignmentData
from openedx_authz.constants import roles as authz_roles
from organizations.api import get_organizations

from common.djangoapps.student.models import CourseAccessRole
from common.djangoapps.student.signals.signals import emit_course_access_role_added, emit_course_access_role_removed
Expand Down Expand Up @@ -632,6 +633,11 @@ def _authz_get_orgs_for_user(self, user) -> list[str]:
user_external_key=user.username,
role_external_key=role,
)
# A platform-wide grant (course-v1:*, lib:*) covers every org, not just the ones
# with a concrete assignment. Platform-glob scopes have no .org attribute at all
# (unlike org-glob/course/library scopes, where it's a real field that can be None).
if any(assignment.scope.IS_PLATFORM_GLOB for assignment in assignments):
return [org["short_name"] for org in get_organizations()]

@mariajgrimaldi mariajgrimaldi Aug 19, 2026

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.

Sorry for being late to this review. Does this return the same structure as in L641? Can we make sure of this with a test - like a test case that for a user returns a subset and for another all orgs? Not sure if we're doing that already. Thanks

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.

No worries about the timing! Yes — both branches return the same shape, a plain list[str] of org short names (L640 is a list comprehension over get_organizations(), L641 is list(a set) of assignment.scope.org values). Added test_get_orgs_for_user_authz_platform_glob_vs_org_scoped: it registers a third org that's never assigned to anyone, then asserts an org-scoped grant returns only its own org while a platform-wide grant returns all three, against that same shared pool of orgs — so the two branches are actually distinguished instead of just coincidentally returning the same numbers (which is what my original platform_glob test alone couldn't rule out, since it only ever registered exactly the orgs it expected back). Verified locally against a real devstack. Fixed in fbb2726.

orgs = {assignment.scope.org for assignment in assignments if assignment.scope.org is not None}
return list(orgs)

Expand Down
58 changes: 58 additions & 0 deletions common/djangoapps/student/tests/test_roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@
ContentLibraryData,
CourseOverviewData,
OrgCourseOverviewGlobData,
PlatformCourseOverviewGlobData,
RoleAssignmentData,
RoleData,
ScopeData,
UserData,
)
from openedx_authz.api.users import assign_role_to_user_in_scope
from openedx_authz.constants.roles import COURSE_ADMIN, COURSE_STAFF
from openedx_authz.engine.enforcer import AuthzEnforcer
from organizations.api import add_organization

from common.djangoapps.student.admin import CourseAccessRoleHistoryAdmin
from common.djangoapps.student.models import CourseAccessRoleHistory, User
Expand Down Expand Up @@ -313,6 +316,61 @@ def test_get_orgs_for_user_authz(self):
result = role.get_orgs_for_user(self.student)
self.assertCountEqual(result, [self.course_key.org, other_org]) # noqa: PT009

@override_waffle_flag(AUTHZ_COURSE_AUTHORING_FLAG, active=True)
def test_get_orgs_for_user_authz_platform_glob(self):
"""
A platform-wide glob assignment (course-v1:*) has no `.org` attribute, unlike
course/org-glob scopes. get_orgs_for_user must special-case it and return every
registered org instead of crashing with an AttributeError.
"""
role = CourseStaffRole(self.course_key)

for org in self.orgs:
add_organization({"name": org, "short_name": org, "description": ""})

assign_role_to_user_in_scope(
self.student.username,
COURSE_STAFF.external_key,
PlatformCourseOverviewGlobData.build_external_key(),
)
AuthzEnforcer.get_enforcer().load_policy()

result = role.get_orgs_for_user(self.student)
self.assertCountEqual(result, self.orgs) # noqa: PT009
assert role.has_org_for_user(self.student)
assert role.has_org_for_user(self.student, org=self.orgs[0])

@override_waffle_flag(AUTHZ_COURSE_AUTHORING_FLAG, active=True)
def test_get_orgs_for_user_authz_platform_glob_vs_org_scoped(self):
"""
Side-by-side check that the platform-glob branch (return every registered org)
and the regular branch (return only the orgs with a concrete assignment) produce
the same list[str] shape, over the same pool of registered orgs: an org-scoped
grant returns a subset, a platform-wide grant returns all of them.
"""
role = CourseStaffRole(self.course_key)
third_org = "Universal"
all_orgs = [*self.orgs, third_org]

for org in all_orgs:
add_organization({"name": org, "short_name": org, "description": ""})

subset_user = UserFactory()
assign_role_to_user_in_scope(
subset_user.username,
COURSE_STAFF.external_key,
OrgCourseOverviewGlobData.build_external_key(self.orgs[0]),
)
assign_role_to_user_in_scope(
self.student.username,
COURSE_STAFF.external_key,
PlatformCourseOverviewGlobData.build_external_key(),
)
AuthzEnforcer.get_enforcer().load_policy()

self.assertCountEqual(role.get_orgs_for_user(subset_user), [self.orgs[0]]) # noqa: PT009
self.assertCountEqual(role.get_orgs_for_user(self.student), all_orgs) # noqa: PT009

def test_get_authz_compat_course_access_roles_for_user(self):
"""
Test that get_authz_compat_course_access_roles_for_user doesn't crash when the user
Expand Down
Loading