Skip to content
Draft
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
52 changes: 51 additions & 1 deletion contentcuration/contentcuration/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1895,6 +1895,40 @@ class Meta:
def __str__(self):
return self.name

@classmethod
def filter_edit_queryset(cls, queryset, user):
Comment thread
yasinelmi marked this conversation as resolved.
if user.is_anonymous:
return queryset.none()

if user.is_admin:
return queryset

return queryset.filter(

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.

praise: Checking status=ORGANIZATION_ROLE_STATUS_ACTIVE alongside the role — and keeping all three conditions in one filter() so they match a single user_roles row rather than across rows — is the easy thing to get wrong here. test_filter_edit_queryset__pending_admin_cannot_edit locks it in.

user_roles__user=user,
user_roles__role=ORGANIZATION_ADMIN,
user_roles__status=ORGANIZATION_ROLE_STATUS_ACTIVE,
deleted=False,
).distinct()

@classmethod
def filter_view_queryset(cls, queryset, user):
if user.is_anonymous:
return queryset.none()

if user.is_admin:
return queryset

return queryset.filter(
user_roles__user=user,
user_roles__role__in=[
ORGANIZATION_ADMIN,
ORGANIZATION_EDITOR,
ORGANIZATION_VIEWER,
],
user_roles__status=ORGANIZATION_ROLE_STATUS_ACTIVE,
deleted=False,
).distinct()


class OrganizationRole(models.Model):
"""
Expand Down Expand Up @@ -3807,7 +3841,14 @@ def filter_edit_queryset(cls, queryset, user):
return queryset

return queryset.filter(
Comment thread
yasinelmi marked this conversation as resolved.
Q(email__iexact=user.email) | Q(sender=user) | Q(channel__editors=user)
Q(email__iexact=user.email)
| Q(sender=user)
| Q(channel__editors=user)
| Q(
organization__user_roles__user=user,
organization__user_roles__role=ORGANIZATION_ADMIN,
organization__user_roles__status=ORGANIZATION_ROLE_STATUS_ACTIVE,
)
).distinct()

@classmethod
Expand All @@ -3822,6 +3863,15 @@ def filter_view_queryset(cls, queryset, user):
| Q(sender=user)
| Q(channel__editors=user)
| Q(channel__viewers=user)
| Q(
organization__user_roles__user=user,
organization__user_roles__role__in=[
ORGANIZATION_ADMIN,
ORGANIZATION_EDITOR,
ORGANIZATION_VIEWER,
],
organization__user_roles__status=ORGANIZATION_ROLE_STATUS_ACTIVE,
)
).distinct()


Expand Down
170 changes: 170 additions & 0 deletions contentcuration/contentcuration/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@
from contentcuration.constants import channel_history
from contentcuration.constants import community_library_submission
from contentcuration.constants import user_history
from contentcuration.constants.organization_roles import ORGANIZATION_ADMIN
from contentcuration.constants.organization_roles import ORGANIZATION_EDITOR
from contentcuration.constants.organization_roles import (
ORGANIZATION_ROLE_STATUS_ACTIVE,
)
from contentcuration.constants.organization_roles import (
ORGANIZATION_ROLE_STATUS_PENDING,
)
from contentcuration.constants.organization_roles import ORGANIZATION_VIEWER
from contentcuration.models import AssessmentItem
from contentcuration.models import AuditedSpecialPermissionsLicense
from contentcuration.models import Change
Expand All @@ -34,6 +43,8 @@
from contentcuration.models import Language
from contentcuration.models import License
from contentcuration.models import object_storage_name
from contentcuration.models import Organization
from contentcuration.models import OrganizationRole
from contentcuration.models import RecommendationsEvent
from contentcuration.models import RecommendationsInteractionEvent
from contentcuration.models import User
Expand Down Expand Up @@ -309,6 +320,165 @@ def create_change(server_rev, applied):
self.assertEqual(channel.get_server_rev(), 2)


class OrganizationTestCase(PermissionQuerysetTestCase):
@property
def base_queryset(self):
return Organization.objects.all()

def test_filter_edit_queryset__admin_role(self):
organization = testdata.organization()
user = testdata.user()

queryset = Organization.filter_edit_queryset(self.base_queryset, user=user)
self.assertQuerysetDoesNotContain(queryset, pk=organization.id)

OrganizationRole.objects.create(
user=user,
organization=organization,
role=ORGANIZATION_ADMIN,
status=ORGANIZATION_ROLE_STATUS_ACTIVE,
)
queryset = Organization.filter_edit_queryset(self.base_queryset, user=user)
self.assertQuerysetContains(queryset, pk=organization.id)

def test_filter_edit_queryset__editor_role_cannot_edit(self):
organization = testdata.organization()
user = testdata.user()
OrganizationRole.objects.create(
user=user,
organization=organization,
role=ORGANIZATION_EDITOR,
status=ORGANIZATION_ROLE_STATUS_ACTIVE,
)

queryset = Organization.filter_edit_queryset(self.base_queryset, user=user)
self.assertQuerysetDoesNotContain(queryset, pk=organization.id)

def test_filter_edit_queryset__pending_admin_cannot_edit(self):
organization = testdata.organization()
user = testdata.user()
OrganizationRole.objects.create(
user=user,
organization=organization,
role=ORGANIZATION_ADMIN,
status=ORGANIZATION_ROLE_STATUS_PENDING,
)

queryset = Organization.filter_edit_queryset(self.base_queryset, user=user)
self.assertQuerysetDoesNotContain(queryset, pk=organization.id)

def test_filter_edit_queryset__anonymous(self):
organization = testdata.organization()

queryset = Organization.filter_edit_queryset(
self.base_queryset, user=self.anonymous_user
)
self.assertQuerysetDoesNotContain(queryset, pk=organization.id)

def test_filter_view_queryset__viewer_role(self):
organization = testdata.organization()
user = testdata.user()

queryset = Organization.filter_view_queryset(self.base_queryset, user=user)
self.assertQuerysetDoesNotContain(queryset, pk=organization.id)

OrganizationRole.objects.create(
user=user,
organization=organization,
role=ORGANIZATION_VIEWER,
status=ORGANIZATION_ROLE_STATUS_ACTIVE,
)
queryset = Organization.filter_view_queryset(self.base_queryset, user=user)
self.assertQuerysetContains(queryset, pk=organization.id)

def test_filter_view_queryset__anonymous(self):
organization = testdata.organization()

queryset = Organization.filter_view_queryset(
self.base_queryset, user=self.anonymous_user
)
self.assertQuerysetDoesNotContain(queryset, pk=organization.id)


class InvitationOrganizationTestCase(PermissionQuerysetTestCase):
@property
def base_queryset(self):
return Invitation.objects.all()

def _make_org_invitation(self):
organization = testdata.organization()
invitee = testdata.user(email="org-invitee@le.com")
invitation = Invitation.objects.create(
email=invitee.email, organization=organization
)
return organization, invitation

def test_filter_edit_queryset__organization_admin(self):
organization, invitation = self._make_org_invitation()
user = testdata.user()

queryset = Invitation.filter_edit_queryset(self.base_queryset, user=user)
self.assertQuerysetDoesNotContain(queryset, pk=invitation.id)

OrganizationRole.objects.create(
user=user,
organization=organization,
role=ORGANIZATION_ADMIN,
status=ORGANIZATION_ROLE_STATUS_ACTIVE,
)
queryset = Invitation.filter_edit_queryset(self.base_queryset, user=user)
self.assertQuerysetContains(queryset, pk=invitation.id)

def test_filter_edit_queryset__organization_editor_cannot_edit(self):
organization, invitation = self._make_org_invitation()
user = testdata.user()
OrganizationRole.objects.create(
user=user,
organization=organization,
role=ORGANIZATION_EDITOR,
status=ORGANIZATION_ROLE_STATUS_ACTIVE,
)

queryset = Invitation.filter_edit_queryset(self.base_queryset, user=user)
self.assertQuerysetDoesNotContain(queryset, pk=invitation.id)

def test_filter_view_queryset__organization_editor(self):
organization, invitation = self._make_org_invitation()
user = testdata.user()

queryset = Invitation.filter_view_queryset(self.base_queryset, user=user)
self.assertQuerysetDoesNotContain(queryset, pk=invitation.id)

OrganizationRole.objects.create(
user=user,
organization=organization,
role=ORGANIZATION_EDITOR,
status=ORGANIZATION_ROLE_STATUS_ACTIVE,
)
queryset = Invitation.filter_view_queryset(self.base_queryset, user=user)
self.assertQuerysetContains(queryset, pk=invitation.id)

def test_filter_view_queryset__organization_viewer(self):
organization, invitation = self._make_org_invitation()
user = testdata.user()
OrganizationRole.objects.create(
user=user,
organization=organization,
role=ORGANIZATION_VIEWER,
status=ORGANIZATION_ROLE_STATUS_ACTIVE,
)

queryset = Invitation.filter_view_queryset(self.base_queryset, user=user)
self.assertQuerysetContains(queryset, pk=invitation.id)

def test_filter_view_queryset__unrelated_user(self):
organization, invitation = self._make_org_invitation()
user = testdata.user()

queryset = Invitation.filter_view_queryset(self.base_queryset, user=user)
self.assertQuerysetDoesNotContain(queryset, pk=invitation.id)


class ContentNodeTestCase(PermissionQuerysetTestCase):
@property
def base_queryset(self):
Expand Down
16 changes: 16 additions & 0 deletions contentcuration/contentcuration/tests/testdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
from contentcuration.constants import (
community_library_submission as community_library_submission_constants,
)
from contentcuration.constants.organization_roles import ORGANIZATION_ADMIN
from contentcuration.constants.organization_roles import (
ORGANIZATION_ROLE_STATUS_ACTIVE,
)
from contentcuration.tests.utils import mixer


Expand Down Expand Up @@ -253,6 +257,18 @@ def channel(name="testchannel"):
return channel


def organization(name="Test Organization"):
return cc.Organization.objects.create(name=name)


def organization_role(
user, organization, role=ORGANIZATION_ADMIN, status=ORGANIZATION_ROLE_STATUS_ACTIVE
):
return cc.OrganizationRole.objects.create(
user=user, organization=organization, role=role, status=status
)


def random_string(chars=10):
"""
Generate a random string
Expand Down
14 changes: 14 additions & 0 deletions contentcuration/contentcuration/tests/viewsets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,33 @@ def generate_copy_event(*args, **kwargs):


def generate_create_event(*args, **kwargs):
# organization_id isn't a parameter of the production event builders (no
# production code emits it - real clients send it as a plain dict key,
# not through this Python function), but tests need to be able to attach
# it to simulate what a real client payload would contain, since
# handle_changes() reads it directly off the raw incoming dict.
organization_id = kwargs.pop("organization_id", None)
event = base_generate_create_event(*args, **kwargs)
if organization_id:
event["organization_id"] = organization_id
event["rev"] = random.randint(1, 10000000)
return event


def generate_delete_event(*args, **kwargs):
organization_id = kwargs.pop("organization_id", None)
event = base_generate_delete_event(*args, **kwargs)
if organization_id:
event["organization_id"] = organization_id
event["rev"] = random.randint(1, 10000000)
return event


def generate_update_event(*args, **kwargs):
organization_id = kwargs.pop("organization_id", None)
event = base_generate_update_event(*args, **kwargs)
if organization_id:
event["organization_id"] = organization_id
event["rev"] = random.randint(1, 10000000)
return event

Expand Down
Loading
Loading