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

[ENG-5028] [ENG-5920] Preprints Affiliation Project PR (BE) #10745

Merged
merged 2 commits into from
Sep 18, 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
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
Loading