Skip to content

Commit 5b31809

Browse files
committed
Merge branch 'release/24.07.0'
2 parents b09206e + 8f273d2 commit 5b31809

25 files changed

+1217
-492
lines changed

CHANGELOG

+5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
We follow the CalVer (https://calver.org/) versioning scheme: YY.MINOR.MICRO.
44

5+
24.07.0 (2024-09-19)
6+
====================
7+
8+
- Preprints Affiliation Project BE Release
9+
510
24.06.0 (2024-09-12)
611
====================
712

api/base/permissions.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
from framework.auth import oauth_scopes
99
from framework.auth.cas import CasResponse
1010

11-
from osf.models import ApiOAuth2Application, ApiOAuth2PersonalToken
11+
from osf.models import ApiOAuth2Application, ApiOAuth2PersonalToken, Preprint
12+
from osf.utils import permissions as osf_permissions
1213
from website.util.sanitize import is_iterable_but_not_string
14+
from api.base.utils import get_user_auth
1315

1416

1517
# Implementation built on django-oauth-toolkit, but with more granular control over read+write permissions
@@ -160,3 +162,17 @@ def has_object_permission(self, request, view, obj):
160162
obj = self.get_object(request, view, obj)
161163
return super().has_object_permission(request, view, obj)
162164
return Perm
165+
166+
167+
class WriteOrPublicForRelationshipInstitutions(permissions.BasePermission):
168+
def has_object_permission(self, request, view, obj):
169+
assert isinstance(obj, dict)
170+
auth = get_user_auth(request)
171+
resource = obj['self']
172+
173+
if request.method in permissions.SAFE_METHODS:
174+
return resource.is_public or resource.can_view(auth)
175+
else:
176+
if isinstance(resource, Preprint):
177+
return resource.can_edit(auth=auth)
178+
return resource.has_permission(auth.user, osf_permissions.WRITE)

api/draft_registrations/serializers.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from api.nodes.serializers import (
99
DraftRegistrationLegacySerializer,
1010
DraftRegistrationDetailLegacySerializer,
11-
update_institutions,
1211
get_license_details,
1312
NodeSerializer,
1413
NodeLicenseSerializer,
@@ -18,6 +17,7 @@
1817
NodeContributorDetailSerializer,
1918
RegistrationSchemaRelationshipField,
2019
)
20+
from api.institutions.utils import update_institutions
2121
from api.taxonomies.serializers import TaxonomizableSerializerMixin
2222
from osf.exceptions import DraftRegistrationStateError
2323
from osf.models import Node

api/institutions/serializers.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ def create(self, validated_data):
129129
if not node.has_permission(user, osf_permissions.WRITE):
130130
raise exceptions.PermissionDenied(detail='Write permission on node {} required'.format(node_dict['_id']))
131131
if not node.is_affiliated_with_institution(inst):
132-
node.add_affiliated_institution(inst, user, save=True)
132+
node.add_affiliated_institution(inst, user)
133133
changes_flag = True
134134

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

180180
if not changes_flag:
@@ -292,3 +292,9 @@ def get_absolute_url(self, obj):
292292
'version': 'v2',
293293
},
294294
)
295+
296+
297+
class InstitutionRelated(JSONAPIRelationshipSerializer):
298+
id = ser.CharField(source='_id', required=False, allow_null=True)
299+
class Meta:
300+
type_ = 'institutions'

api/institutions/utils.py

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from rest_framework import exceptions
2+
3+
from api.base.serializers import relationship_diff
4+
from osf.models import Institution
5+
from osf.utils import permissions as osf_permissions
6+
7+
8+
def get_institutions_to_add_remove(institutions, new_institutions):
9+
diff = relationship_diff(
10+
current_items={inst._id: inst for inst in institutions.all()},
11+
new_items={inst['_id']: inst for inst in new_institutions},
12+
)
13+
14+
insts_to_add = []
15+
for inst_id in diff['add']:
16+
inst = Institution.load(inst_id)
17+
if not inst:
18+
raise exceptions.NotFound(detail=f'Institution with id "{inst_id}" was not found')
19+
insts_to_add.append(inst)
20+
21+
return insts_to_add, diff['remove'].values()
22+
23+
24+
def update_institutions(resource, new_institutions, user, post=False):
25+
add, remove = get_institutions_to_add_remove(
26+
institutions=resource.affiliated_institutions,
27+
new_institutions=new_institutions,
28+
)
29+
30+
if not post:
31+
for inst in remove:
32+
if not user.is_affiliated_with_institution(inst) and not resource.has_permission(user, osf_permissions.ADMIN):
33+
raise exceptions.PermissionDenied(detail=f'User needs to be affiliated with {inst.name}')
34+
resource.remove_affiliated_institution(inst, user)
35+
36+
for inst in add:
37+
if not user.is_affiliated_with_institution(inst):
38+
raise exceptions.PermissionDenied(detail=f'User needs to be affiliated with {inst.name}')
39+
resource.add_affiliated_institution(inst, user)
40+
41+
42+
def update_institutions_if_user_associated(resource, desired_institutions_data, user):
43+
"""Update institutions only if the user is associated with the institutions. Otherwise, raise an exception."""
44+
45+
desired_institutions = Institution.objects.filter(_id__in=[item['_id'] for item in desired_institutions_data])
46+
47+
# If a user wants to affiliate with a resource check that they have it.
48+
for inst in desired_institutions:
49+
if user.is_affiliated_with_institution(inst):
50+
resource.add_affiliated_institution(inst, user)
51+
else:
52+
raise exceptions.PermissionDenied(detail=f'User needs to be affiliated with {inst.name}')
53+
54+
# If a user doesn't include an affiliation they have, then remove it.
55+
resource_institutions = resource.affiliated_institutions.all()
56+
for inst in user.get_affiliated_institutions():
57+
if inst in resource_institutions and inst not in desired_institutions:
58+
resource.remove_affiliated_institution(inst, user)

api/nodes/permissions.py

-12
Original file line numberDiff line numberDiff line change
@@ -294,18 +294,6 @@ def has_object_permission(self, request, view, obj):
294294
return True
295295

296296

297-
class WriteOrPublicForRelationshipInstitutions(permissions.BasePermission):
298-
def has_object_permission(self, request, view, obj):
299-
assert isinstance(obj, dict)
300-
auth = get_user_auth(request)
301-
node = obj['self']
302-
303-
if request.method in permissions.SAFE_METHODS:
304-
return node.is_public or node.can_view(auth)
305-
else:
306-
return node.has_permission(auth.user, osf_permissions.WRITE)
307-
308-
309297
class ReadOnlyIfRegistration(permissions.BasePermission):
310298
"""Makes PUT and POST forbidden for registrations."""
311299

api/nodes/serializers.py

+5-46
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,10 @@
77
)
88
from api.base.serializers import (
99
VersionedDateTimeField, HideIfRegistration, IDField,
10-
JSONAPIRelationshipSerializer,
1110
JSONAPISerializer, LinksField,
1211
NodeFileHyperLinkField, RelationshipField,
1312
ShowIfVersion, TargetTypeField, TypeField,
14-
WaterbutlerLink, relationship_diff, BaseAPISerializer,
13+
WaterbutlerLink, BaseAPISerializer,
1514
HideIfWikiDisabled, ShowIfAdminScopeOrAnonymous,
1615
ValuesListField, TargetField,
1716
)
@@ -21,6 +20,7 @@
2120
get_user_auth, is_truthy,
2221
)
2322
from api.base.versioning import get_kebab_snake_case_field
23+
from api.institutions.utils import update_institutions
2424
from api.taxonomies.serializers import TaxonomizableSerializerMixin
2525
from django.apps import apps
2626
from django.conf import settings
@@ -34,7 +34,7 @@
3434
from addons.osfstorage.models import Region
3535
from osf.exceptions import NodeStateError
3636
from osf.models import (
37-
Comment, DraftRegistration, ExternalAccount, Institution,
37+
Comment, DraftRegistration, ExternalAccount,
3838
RegistrationSchema, AbstractNode, PrivateLink, Preprint,
3939
RegistrationProvider, OSFGroup, NodeLicense, DraftNode,
4040
Registration, Node,
@@ -52,44 +52,6 @@ def to_internal_value(self, data):
5252
return self.get_object(data)
5353

5454

55-
def get_institutions_to_add_remove(institutions, new_institutions):
56-
diff = relationship_diff(
57-
current_items={inst._id: inst for inst in institutions.all()},
58-
new_items={inst['_id']: inst for inst in new_institutions},
59-
)
60-
61-
insts_to_add = []
62-
for inst_id in diff['add']:
63-
inst = Institution.load(inst_id)
64-
if not inst:
65-
raise exceptions.NotFound(detail=f'Institution with id "{inst_id}" was not found')
66-
insts_to_add.append(inst)
67-
68-
return insts_to_add, diff['remove'].values()
69-
70-
71-
def update_institutions(node, new_institutions, user, post=False):
72-
add, remove = get_institutions_to_add_remove(
73-
institutions=node.affiliated_institutions,
74-
new_institutions=new_institutions,
75-
)
76-
77-
if not post:
78-
for inst in remove:
79-
if not user.is_affiliated_with_institution(inst) and not node.has_permission(user, osf_permissions.ADMIN):
80-
raise exceptions.PermissionDenied(
81-
detail=f'User needs to be affiliated with {inst.name}',
82-
)
83-
node.remove_affiliated_institution(inst, user)
84-
85-
for inst in add:
86-
if not user.is_affiliated_with_institution(inst):
87-
raise exceptions.PermissionDenied(
88-
detail=f'User needs to be affiliated with {inst.name}',
89-
)
90-
node.add_affiliated_institution(inst, user)
91-
92-
9355
class RegionRelationshipField(RelationshipField):
9456

9557
def to_internal_value(self, data):
@@ -1479,13 +1441,10 @@ def get_storage_addons_url(self, obj):
14791441
},
14801442
)
14811443

1482-
class InstitutionRelated(JSONAPIRelationshipSerializer):
1483-
id = ser.CharField(source='_id', required=False, allow_null=True)
1484-
class Meta:
1485-
type_ = 'institutions'
1486-
14871444

14881445
class NodeInstitutionsRelationshipSerializer(BaseAPISerializer):
1446+
from api.institutions.serializers import InstitutionRelated # Avoid circular import
1447+
14891448
data = ser.ListField(child=InstitutionRelated())
14901449
links = LinksField({
14911450
'self': 'get_self_url',

api/nodes/views.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
WaterButlerMixin,
5858
)
5959
from api.base.waffle_decorators import require_flag
60+
from api.base.permissions import WriteOrPublicForRelationshipInstitutions
6061
from api.cedar_metadata_records.serializers import CedarMetadataRecordsListSerializer
6162
from api.cedar_metadata_records.utils import can_view_record
6263
from api.citations.utils import render_citation
@@ -87,7 +88,6 @@
8788
NodeGroupDetailPermissions,
8889
IsContributorOrGroupMember,
8990
AdminDeletePermissions,
90-
WriteOrPublicForRelationshipInstitutions,
9191
ExcludeWithdrawals,
9292
NodeLinksShowIfVersion,
9393
ReadOnlyIfWithdrawn,

api/preprints/permissions.py

+27
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,30 @@ def has_object_permission(self, request, view, obj):
137137
raise exceptions.PermissionDenied(detail='Withdrawn preprints may not be edited')
138138
return True
139139
raise exceptions.NotFound
140+
141+
142+
class PreprintInstitutionPermissionList(permissions.BasePermission):
143+
"""
144+
Custom permission class for checking access to a list of institutions
145+
associated with a preprint.
146+
147+
Permissions:
148+
- Allows safe methods (GET, HEAD, OPTIONS) for public preprints.
149+
- For private preprints, checks if the user has read permissions.
150+
151+
Methods:
152+
- has_object_permission: Raises MethodNotAllowed for non-safe methods and
153+
checks if the user has the necessary permissions to access private preprints.
154+
"""
155+
def has_object_permission(self, request, view, obj):
156+
if request.method not in permissions.SAFE_METHODS:
157+
raise exceptions.MethodNotAllowed(method=request.method)
158+
159+
if obj.is_public:
160+
return True
161+
162+
auth = get_user_auth(request)
163+
if not auth.user:
164+
return False
165+
else:
166+
return obj.has_permission(auth.user, osf_permissions.READ)

0 commit comments

Comments
 (0)