Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: prevent cross-org ObjectTags from being created #633

Merged
merged 3 commits into from
Feb 15, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ class ObjectTagTaxonomyOrgFilterBackend(BaseFilterBackend):

def filter_queryset(self, request, queryset, _):
if oel_tagging.is_taxonomy_admin(request.user):
return queryset
return queryset.prefetch_related('taxonomy__taxonomyorg_set')

user_admin_orgs = get_admin_orgs(request.user)
user_orgs = get_user_orgs(request.user)
Expand All @@ -87,4 +87,4 @@ def filter_queryset(self, request, queryset, _):
)
)
)
)
).prefetch_related('taxonomy__taxonomyorg_set')
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ def _setUp_users(self):
email="[email protected]",
is_staff=True,
)
self.superuser = User.objects.create(
username="superuser",
email="[email protected]",
is_superuser=True,
)

self.staffA = User.objects.create(
username="staffA",
Expand Down Expand Up @@ -1652,14 +1657,15 @@ def test_tag_library_invalid(self, user_attr, taxonomy_attr):
assert response.status_code == status.HTTP_400_BAD_REQUEST

@ddt.data(
("staff", status.HTTP_200_OK),
("superuser", status.HTTP_200_OK),
("staff", status.HTTP_403_FORBIDDEN),
("staffA", status.HTTP_403_FORBIDDEN),
("staffB", status.HTTP_403_FORBIDDEN),
)
@ddt.unpack
def test_tag_cross_org(self, user_attr, expected_status):
"""
Tests that only global admins can add a taxonomy from orgA to an object from orgB
Tests that only superusers may add a taxonomy from orgA to an object from orgB
"""
user = getattr(self, user_attr)
self.client.force_authenticate(user=user)
Expand All @@ -1671,14 +1677,15 @@ def test_tag_cross_org(self, user_attr, expected_status):
assert response.status_code == expected_status

@ddt.data(
("staff", status.HTTP_200_OK),
("superuser", status.HTTP_200_OK),
("staff", status.HTTP_403_FORBIDDEN),
("staffA", status.HTTP_403_FORBIDDEN),
("staffB", status.HTTP_403_FORBIDDEN),
)
@ddt.unpack
def test_tag_no_org(self, user_attr, expected_status):
"""
Tests that only global admins can add a no-org taxonomy to an object
Tests that only superusers may add a no-org taxonomy to an object
"""
user = getattr(self, user_attr)
self.client.force_authenticate(user=user)
Expand Down Expand Up @@ -1771,15 +1778,15 @@ def test_get_tags(self):
assert response3.data[str(self.courseA)]["taxonomies"] == expected_tags

@ddt.data(
('staff', 'courseA', 7),
('staff', 'libraryA', 7),
("content_creatorA", 'courseA', 13, False),
("content_creatorA", 'libraryA', 13, False),
("library_staffA", 'libraryA', 13, False), # Library users can only view objecttags, not change them?
("library_userA", 'libraryA', 13, False),
("instructorA", 'courseA', 13),
("course_instructorA", 'courseA', 13),
("course_staffA", 'courseA', 13),
('staff', 'courseA', 8),
('staff', 'libraryA', 8),
("content_creatorA", 'courseA', 11, False),
("content_creatorA", 'libraryA', 11, False),
("library_staffA", 'libraryA', 11, False), # Library users can only view objecttags, not change them?
("library_userA", 'libraryA', 11, False),
("instructorA", 'courseA', 11),
("course_instructorA", 'courseA', 11),
("course_staffA", 'courseA', 11),
)
@ddt.unpack
def test_object_tags_query_count(
Expand Down Expand Up @@ -2322,7 +2329,7 @@ class TestTaxonomyTagsViewSet(TestTaxonomyObjectsMixin, APITestCase):
"""
@ddt.data(
('staff', 11),
("content_creatorA", 13), # FIXME too many queries?
("content_creatorA", 13),
("library_staffA", 13),
("library_userA", 13),
("instructorA", 13),
Expand Down
34 changes: 33 additions & 1 deletion openedx/core/djangoapps/content_tagging/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,35 @@ def can_view_object_tag_objectid(user: UserType, object_id: str) -> bool:
return bool(object_org) and (is_org_admin(user, object_org) or is_org_user(user, object_org))


@rules.predicate
def can_change_object_tag(
user: UserType, perm_obj: oel_tagging.ObjectTagPermissionItem | None = None
) -> bool:
"""
Returns True if the given user may change object tags with the given taxonomy + object_id.

Adds additional checks to ensure the taxonomy is available for use with the object_id's org.
"""
if oel_tagging.can_change_object_tag(user, perm_obj):
if perm_obj and perm_obj.taxonomy and perm_obj.object_id:
# can_change_object_tag_objectid already checked that object_id is valid and has an org,
# so these statements will not fail. But we need to assert to keep the type checker happy.
try:
context_key = get_context_key_from_key_string(perm_obj.object_id)
assert context_key.org

Choose a reason for hiding this comment

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

Similar to the question I had on openedx#34200 (comment) , if we should handle the AssertionError and return False or does the @rules.predicate or elsewhere handle it?

Copy link
Member Author

Choose a reason for hiding this comment

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

Agreed, I'll handle it and return False.

except (ValueError, AssertionError):
return False # pragma: no cover

is_all_org, taxonomy_orgs = TaxonomyOrg.get_organizations(perm_obj.taxonomy)
if not is_all_org:
# Ensure the object_id's org is among the allowed taxonomy orgs
object_org = rules_cache.get_orgs([context_key.org])
return bool(object_org) and object_org[0] in taxonomy_orgs

return True
return False


@rules.predicate
def can_change_taxonomy_tag(user: UserType, tag: oel_tagging.Tag | None = None) -> bool:
"""
Expand Down Expand Up @@ -304,7 +333,10 @@ def can_change_taxonomy_tag(user: UserType, tag: oel_tagging.Tag | None = None)
rules.set_perm("oel_tagging.view_tag", rules.always_allow)

# ObjectTag
rules.set_perm("oel_tagging.can_tag_object", oel_tagging.can_change_object_tag)
rules.set_perm("oel_tagging.add_objecttag", can_change_object_tag)
rules.set_perm("oel_tagging.change_objecttag", can_change_object_tag)
rules.set_perm("oel_tagging.delete_objecttag", can_change_object_tag)
rules.set_perm("oel_tagging.can_tag_object", can_change_object_tag)

# This perms are used in the tagging rest api from openedx_tagging that is exposed in the CMS. They are overridden here
# to include Organization and objects permissions.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,7 @@ def test_object_tag_no_orgs(self, perm, tag_attr):
"""Only superusers can create/edit an ObjectTag with a no-org Taxonomy"""
object_tag = getattr(self, tag_attr)
assert self.superuser.has_perm(perm, object_tag)
assert self.staff.has_perm(perm, object_tag)
assert not self.staff.has_perm(perm, object_tag)
assert not self.user_both_orgs.has_perm(perm, object_tag)
assert not self.user_org2.has_perm(perm, object_tag)
assert not self.learner.has_perm(perm, object_tag)
Expand Down
Loading