Skip to content

Commit

Permalink
[ENG-5028] Squash all commits for project feature branch (#10743)
Browse files Browse the repository at this point in the history
[ENG-5829] Add Institutional Affiliation mix-in to Preprints #10649
[ENG-5844] Add preprint institution affiliation endpoint #10655
[ENG-5845] Preprint Institutions Relationship #10659
[ENG-5846][ENG-5847] Add affiliated_institutions to preprint list and detail endpoint #10684
[Bug-fix] Fix Self Link for Preprint Affiliated Institutions #10698
[ENG-5908] Add Test Confirming Preprint Affiliated Institution Data is Sent to SHARE #10700
[ENG-5966] 2.0.1 BE: Update permission to support edit as a WRITE contributor #10741
  • Loading branch information
cslzchen authored Sep 6, 2024
1 parent 63365d8 commit dadfbae
Show file tree
Hide file tree
Showing 23 changed files with 1,207 additions and 491 deletions.
18 changes: 17 additions & 1 deletion api/base/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
from framework.auth import oauth_scopes
from framework.auth.cas import CasResponse

from osf.models import ApiOAuth2Application, ApiOAuth2PersonalToken
from osf.models import ApiOAuth2Application, ApiOAuth2PersonalToken, Preprint
from osf.utils import permissions as osf_permissions
from website.util.sanitize import is_iterable_but_not_string
from api.base.utils import get_user_auth


# Implementation built on django-oauth-toolkit, but with more granular control over read+write permissions
Expand Down Expand Up @@ -160,3 +162,17 @@ def has_object_permission(self, request, view, obj):
obj = self.get_object(request, view, obj)
return super().has_object_permission(request, view, obj)
return Perm


class WriteOrPublicForRelationshipInstitutions(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
assert isinstance(obj, dict)
auth = get_user_auth(request)
resource = obj['self']

if request.method in permissions.SAFE_METHODS:
return resource.is_public or resource.can_view(auth)
else:
if isinstance(resource, Preprint):
return resource.can_edit(auth=auth)
return resource.has_permission(auth.user, osf_permissions.WRITE)
2 changes: 1 addition & 1 deletion api/draft_registrations/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from api.nodes.serializers import (
DraftRegistrationLegacySerializer,
DraftRegistrationDetailLegacySerializer,
update_institutions,
get_license_details,
NodeSerializer,
NodeLicenseSerializer,
Expand All @@ -18,6 +17,7 @@
NodeContributorDetailSerializer,
RegistrationSchemaRelationshipField,
)
from api.institutions.utils import update_institutions
from api.taxonomies.serializers import TaxonomizableSerializerMixin
from osf.exceptions import DraftRegistrationStateError
from osf.models import Node
Expand Down
10 changes: 8 additions & 2 deletions api/institutions/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ def create(self, validated_data):
if not node.has_permission(user, osf_permissions.WRITE):
raise exceptions.PermissionDenied(detail='Write permission on node {} required'.format(node_dict['_id']))
if not node.is_affiliated_with_institution(inst):
node.add_affiliated_institution(inst, user, save=True)
node.add_affiliated_institution(inst, user)
changes_flag = True

if not changes_flag:
Expand Down Expand Up @@ -174,7 +174,7 @@ def create(self, validated_data):
if not registration.has_permission(user, osf_permissions.WRITE):
raise exceptions.PermissionDenied(detail='Write permission on registration {} required'.format(registration_dict['_id']))
if not registration.is_affiliated_with_institution(inst):
registration.add_affiliated_institution(inst, user, save=True)
registration.add_affiliated_institution(inst, user)
changes_flag = True

if not changes_flag:
Expand Down Expand Up @@ -292,3 +292,9 @@ def get_absolute_url(self, obj):
'version': 'v2',
},
)


class InstitutionRelated(JSONAPIRelationshipSerializer):
id = ser.CharField(source='_id', required=False, allow_null=True)
class Meta:
type_ = 'institutions'
58 changes: 58 additions & 0 deletions api/institutions/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from rest_framework import exceptions

from api.base.serializers import relationship_diff
from osf.models import Institution
from osf.utils import permissions as osf_permissions


def get_institutions_to_add_remove(institutions, new_institutions):
diff = relationship_diff(
current_items={inst._id: inst for inst in institutions.all()},
new_items={inst['_id']: inst for inst in new_institutions},
)

insts_to_add = []
for inst_id in diff['add']:
inst = Institution.load(inst_id)
if not inst:
raise exceptions.NotFound(detail=f'Institution with id "{inst_id}" was not found')
insts_to_add.append(inst)

return insts_to_add, diff['remove'].values()


def update_institutions(resource, new_institutions, user, post=False):
add, remove = get_institutions_to_add_remove(
institutions=resource.affiliated_institutions,
new_institutions=new_institutions,
)

if not post:
for inst in remove:
if not user.is_affiliated_with_institution(inst) and not resource.has_permission(user, osf_permissions.ADMIN):
raise exceptions.PermissionDenied(detail=f'User needs to be affiliated with {inst.name}')
resource.remove_affiliated_institution(inst, user)

for inst in add:
if not user.is_affiliated_with_institution(inst):
raise exceptions.PermissionDenied(detail=f'User needs to be affiliated with {inst.name}')
resource.add_affiliated_institution(inst, user)


def update_institutions_if_user_associated(resource, desired_institutions_data, user):
"""Update institutions only if the user is associated with the institutions. Otherwise, raise an exception."""

desired_institutions = Institution.objects.filter(_id__in=[item['_id'] for item in desired_institutions_data])

# If a user wants to affiliate with a resource check that they have it.
for inst in desired_institutions:
if user.is_affiliated_with_institution(inst):
resource.add_affiliated_institution(inst, user)
else:
raise exceptions.PermissionDenied(detail=f'User needs to be affiliated with {inst.name}')

# If a user doesn't include an affiliation they have, then remove it.
resource_institutions = resource.affiliated_institutions.all()
for inst in user.get_affiliated_institutions():
if inst in resource_institutions and inst not in desired_institutions:
resource.remove_affiliated_institution(inst, user)
12 changes: 0 additions & 12 deletions api/nodes/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,18 +294,6 @@ def has_object_permission(self, request, view, obj):
return True


class WriteOrPublicForRelationshipInstitutions(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
assert isinstance(obj, dict)
auth = get_user_auth(request)
node = obj['self']

if request.method in permissions.SAFE_METHODS:
return node.is_public or node.can_view(auth)
else:
return node.has_permission(auth.user, osf_permissions.WRITE)


class ReadOnlyIfRegistration(permissions.BasePermission):
"""Makes PUT and POST forbidden for registrations."""

Expand Down
51 changes: 5 additions & 46 deletions api/nodes/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@
)
from api.base.serializers import (
VersionedDateTimeField, HideIfRegistration, IDField,
JSONAPIRelationshipSerializer,
JSONAPISerializer, LinksField,
NodeFileHyperLinkField, RelationshipField,
ShowIfVersion, TargetTypeField, TypeField,
WaterbutlerLink, relationship_diff, BaseAPISerializer,
WaterbutlerLink, BaseAPISerializer,
HideIfWikiDisabled, ShowIfAdminScopeOrAnonymous,
ValuesListField, TargetField,
)
Expand All @@ -21,6 +20,7 @@
get_user_auth, is_truthy,
)
from api.base.versioning import get_kebab_snake_case_field
from api.institutions.utils import update_institutions
from api.taxonomies.serializers import TaxonomizableSerializerMixin
from django.apps import apps
from django.conf import settings
Expand All @@ -34,7 +34,7 @@
from addons.osfstorage.models import Region
from osf.exceptions import NodeStateError
from osf.models import (
Comment, DraftRegistration, ExternalAccount, Institution,
Comment, DraftRegistration, ExternalAccount,
RegistrationSchema, AbstractNode, PrivateLink, Preprint,
RegistrationProvider, OSFGroup, NodeLicense, DraftNode,
Registration, Node,
Expand All @@ -52,44 +52,6 @@ def to_internal_value(self, data):
return self.get_object(data)


def get_institutions_to_add_remove(institutions, new_institutions):
diff = relationship_diff(
current_items={inst._id: inst for inst in institutions.all()},
new_items={inst['_id']: inst for inst in new_institutions},
)

insts_to_add = []
for inst_id in diff['add']:
inst = Institution.load(inst_id)
if not inst:
raise exceptions.NotFound(detail=f'Institution with id "{inst_id}" was not found')
insts_to_add.append(inst)

return insts_to_add, diff['remove'].values()


def update_institutions(node, new_institutions, user, post=False):
add, remove = get_institutions_to_add_remove(
institutions=node.affiliated_institutions,
new_institutions=new_institutions,
)

if not post:
for inst in remove:
if not user.is_affiliated_with_institution(inst) and not node.has_permission(user, osf_permissions.ADMIN):
raise exceptions.PermissionDenied(
detail=f'User needs to be affiliated with {inst.name}',
)
node.remove_affiliated_institution(inst, user)

for inst in add:
if not user.is_affiliated_with_institution(inst):
raise exceptions.PermissionDenied(
detail=f'User needs to be affiliated with {inst.name}',
)
node.add_affiliated_institution(inst, user)


class RegionRelationshipField(RelationshipField):

def to_internal_value(self, data):
Expand Down Expand Up @@ -1479,13 +1441,10 @@ def get_storage_addons_url(self, obj):
},
)

class InstitutionRelated(JSONAPIRelationshipSerializer):
id = ser.CharField(source='_id', required=False, allow_null=True)
class Meta:
type_ = 'institutions'


class NodeInstitutionsRelationshipSerializer(BaseAPISerializer):
from api.institutions.serializers import InstitutionRelated # Avoid circular import

data = ser.ListField(child=InstitutionRelated())
links = LinksField({
'self': 'get_self_url',
Expand Down
2 changes: 1 addition & 1 deletion api/nodes/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
WaterButlerMixin,
)
from api.base.waffle_decorators import require_flag
from api.base.permissions import WriteOrPublicForRelationshipInstitutions
from api.cedar_metadata_records.serializers import CedarMetadataRecordsListSerializer
from api.cedar_metadata_records.utils import can_view_record
from api.citations.utils import render_citation
Expand Down Expand Up @@ -87,7 +88,6 @@
NodeGroupDetailPermissions,
IsContributorOrGroupMember,
AdminDeletePermissions,
WriteOrPublicForRelationshipInstitutions,
ExcludeWithdrawals,
NodeLinksShowIfVersion,
ReadOnlyIfWithdrawn,
Expand Down
27 changes: 27 additions & 0 deletions api/preprints/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,30 @@ def has_object_permission(self, request, view, obj):
raise exceptions.PermissionDenied(detail='Withdrawn preprints may not be edited')
return True
raise exceptions.NotFound


class PreprintInstitutionPermissionList(permissions.BasePermission):
"""
Custom permission class for checking access to a list of institutions
associated with a preprint.
Permissions:
- Allows safe methods (GET, HEAD, OPTIONS) for public preprints.
- For private preprints, checks if the user has read permissions.
Methods:
- has_object_permission: Raises MethodNotAllowed for non-safe methods and
checks if the user has the necessary permissions to access private preprints.
"""
def has_object_permission(self, request, view, obj):
if request.method not in permissions.SAFE_METHODS:
raise exceptions.MethodNotAllowed(method=request.method)

if obj.is_public:
return True

auth = get_user_auth(request)
if not auth.user:
return False
else:
return obj.has_permission(auth.user, osf_permissions.READ)
Loading

0 comments on commit dadfbae

Please sign in to comment.