Skip to content
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
2 changes: 1 addition & 1 deletion api/base/generic_bulk_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ def bulk_destroy(self, request, *args, **kwargs):
data = request.data

num_items = len(data)
bulk_limit = BULK_SETTINGS['DEFAULT_BULK_LIMIT']
bulk_limit = getattr(self, 'bulk_limit', BULK_SETTINGS['DEFAULT_BULK_LIMIT'])

if num_items > bulk_limit:
raise JSONAPIException(
Expand Down
11 changes: 10 additions & 1 deletion api/base/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1391,7 +1391,16 @@ def update(self, instance, validated_data):
# overrides ListSerializer
def run_validation(self, data):
meta = getattr(self, 'Meta', None)
bulk_limit = getattr(meta, 'bulk_limit', BULK_SETTINGS['DEFAULT_BULK_LIMIT'])

if meta and hasattr(meta, 'bulk_limit'):
bulk_limit = meta.bulk_limit
else:
child_meta = getattr(getattr(self, 'child', None), 'Meta', None)
bulk_limit = getattr(
child_meta,
'bulk_limit',
BULK_SETTINGS['DEFAULT_BULK_LIMIT'],
)

num_items = len(data)

Expand Down
2 changes: 2 additions & 0 deletions api/base/settings/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@

BULK_SETTINGS = {
'DEFAULT_BULK_LIMIT': 100,
'NODE_CONTRIBUTORS_BULK_LIMIT': 50,
}

MAX_PAGE_SIZE = 100
Expand Down Expand Up @@ -189,6 +190,7 @@
'user': '10000/day',
'non-cookie-auth': '100/hour',
'add-contributor': '10/second',
'add-contributor-unregistered': '30/hour',
'create-guid': '1000/hour',
'root-anon-throttle': '1000/hour',
'test-user': '2/hour',
Expand Down
1 change: 1 addition & 0 deletions api/base/settings/local-ci.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
'user': '1000000/second',
'non-cookie-auth': '1000000/second',
'add-contributor': '1000000/second',
'add-contributor-unregistered': '1000000/second',
'create-guid': '1000000/second',
'root-anon-throttle': '1000000/second',
'test-user': '2/hour',
Expand Down
1 change: 1 addition & 0 deletions api/base/settings/local-dist.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
'user': '1000000/second',
'non-cookie-auth': '1000000/second',
'add-contributor': '1000000/second',
'add-contributor-unregistered': '1000000/second',
'create-guid': '1000000/second',
'root-anon-throttle': '1000000/second',
'test-user': '2/hour',
Expand Down
21 changes: 21 additions & 0 deletions api/base/throttling.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,27 @@ def allow_request(self, request, view):
return super().allow_request(request, view)


class AddContributorUnregisteredThrottle(BaseThrottle, UserRateThrottle):

scope = 'add-contributor-unregistered'

def allow_request(self, request, view):
"""
Allow all add contributor requests that do not contain unregistered contributors.
"""
if request.method == 'POST':
data = request.data
items = data if isinstance(data, list) else [data]
contains_unregistered_contributor = any(
isinstance(item, dict) and item.get('email') and not item.get('id')
for item in items
)
if not contains_unregistered_contributor:
return True

return super().allow_request(request, view)


class CreateGuidThrottle(BaseThrottle, UserRateThrottle):

scope = 'create-guid'
Expand Down
7 changes: 7 additions & 0 deletions api/nodes/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
from website.project import new_private_link
from website.project.model import NodeUpdateError
from osf.utils import permissions as osf_permissions
from api.base.settings import BULK_SETTINGS


class RegistrationProviderRelationshipField(RelationshipField):
Expand Down Expand Up @@ -1222,6 +1223,8 @@ def get_unregistered_contributor(self, obj):


class NodeContributorsBulkCreateListSerializer(JSONAPIListSerializer):
class Meta:
bulk_limit = BULK_SETTINGS['NODE_CONTRIBUTORS_BULK_LIMIT']

email_preferences = ['false']

Expand Down Expand Up @@ -1313,6 +1316,7 @@ class NodeContributorsCreateSerializer(NodeContributorsSerializer):

class Meta(NodeContributorsSerializer.Meta):
list_serializer_class = NodeContributorsBulkCreateListSerializer
bulk_limit = BULK_SETTINGS['NODE_CONTRIBUTORS_BULK_LIMIT']

def validate_data(self, resource, user_id=None, full_name=None, email=None, index=None, child_nodes=None):
if not user_id and not full_name:
Expand Down Expand Up @@ -1363,6 +1367,9 @@ class NodeContributorDetailSerializer(NodeContributorsSerializer):
id = IDField(required=True, source='_id')
index = ser.IntegerField(required=False, read_only=False, source='_order')

class Meta(NodeContributorsSerializer.Meta):
bulk_limit = BULK_SETTINGS['NODE_CONTRIBUTORS_BULK_LIMIT']

def update(self, instance, validated_data):
return update_contributors_permissions_and_bibliographic_status(
self,
Expand Down
6 changes: 5 additions & 1 deletion api/nodes/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import dataclasses
import waffle

from api.base.settings import BULK_SETTINGS
from api.collections.serializers import CollectionSerializer
from osf import features
from packaging.version import Version
Expand Down Expand Up @@ -45,6 +46,7 @@
UserRateThrottle,
NonCookieAuthThrottle,
AddContributorThrottle,
AddContributorUnregisteredThrottle,
BurstRateThrottle,
FilesRateThrottle,
FilesBurstRateThrottle,
Expand Down Expand Up @@ -432,14 +434,16 @@ class NodeContributorsList(BaseContributorList, bulk_views.BulkUpdateJSONAPIView
required_write_scopes = [CoreScopes.NODE_CONTRIBUTORS_WRITE]
model_class = OSFUser

throttle_classes = (AddContributorThrottle, UserRateThrottle, NonCookieAuthThrottle, BurstRateThrottle)
throttle_classes = (AddContributorThrottle, UserRateThrottle, NonCookieAuthThrottle, BurstRateThrottle, AddContributorUnregisteredThrottle)

pagination_class = NodeContributorPagination
serializer_class = NodeContributorsSerializer
view_category = 'nodes'
view_name = 'node-contributors'
ordering = ('_order',) # default ordering

bulk_limit = BULK_SETTINGS['NODE_CONTRIBUTORS_BULK_LIMIT']

def get_resource(self):
return self.get_node()

Expand Down
8 changes: 4 additions & 4 deletions api_tests/nodes/views/test_node_contributors_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -1589,7 +1589,7 @@ def test_node_contributor_bulk_create_payload_errors(
bulk=True,
)
assert (
res.json['errors'][0]['detail'] == 'Bulk operation limit is 100, got 101.'
res.json['errors'][0]['detail'] == 'Bulk operation limit is 50, got 101.'
)
assert res.json['errors'][0]['source']['pointer'] == '/data'

Expand Down Expand Up @@ -1923,7 +1923,7 @@ def test_bulk_update_contributors_errors(
bulk=True,
)
assert (
res.json['errors'][0]['detail'] == 'Bulk operation limit is 100, got 101.'
res.json['errors'][0]['detail'] == 'Bulk operation limit is 50, got 101.'
)
assert res.json['errors'][0]['source']['pointer'] == '/data'

Expand Down Expand Up @@ -2353,7 +2353,7 @@ def test_bulk_partial_update_errors(
bulk=True,
)
assert (
res.json['errors'][0]['detail'] == 'Bulk operation limit is 100, got 101.'
res.json['errors'][0]['detail'] == 'Bulk operation limit is 50, got 101.'
)
assert res.json['errors'][0]['source']['pointer'] == '/data'

Expand Down Expand Up @@ -2765,7 +2765,7 @@ def test_bulk_delete_contributors_errors(
)
assert res.status_code == 400
assert (
res.json['errors'][0]['detail'] == 'Bulk operation limit is 100, got 101.'
res.json['errors'][0]['detail'] == 'Bulk operation limit is 50, got 101.'
)
assert res.json['errors'][0]['source']['pointer'] == '/data'

Expand Down
8 changes: 4 additions & 4 deletions api_tests/preprints/views/test_preprint_contributors_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -1755,7 +1755,7 @@ def test_preprint_contributor_bulk_create_payload_errors(
node_contrib_create_list = {'data': [payload_one] * 101}
res = app.post_json_api(url_published, node_contrib_create_list,
auth=user.auth, expect_errors=True, bulk=True)
assert res.json['errors'][0]['detail'] == 'Bulk operation limit is 100, got 101.'
assert res.json['errors'][0]['detail'] == 'Bulk operation limit is 50, got 101.'
assert res.json['errors'][0]['source']['pointer'] == '/data'

# test_preprint_contributor_ugly_payload
Expand Down Expand Up @@ -2092,7 +2092,7 @@ def test_bulk_update_contributors_errors(
res = app.put_json_api(
url_published, contrib_update_list,
auth=user.auth, expect_errors=True, bulk=True)
assert res.json['errors'][0]['detail'] == 'Bulk operation limit is 100, got 101.'
assert res.json['errors'][0]['detail'] == 'Bulk operation limit is 50, got 101.'
assert res.json['errors'][0]['source']['pointer'] == '/data'

# test_bulk_update_contributors_invalid_permissions
Expand Down Expand Up @@ -2514,7 +2514,7 @@ def test_bulk_partial_update_errors(
res = app.patch_json_api(
url_published, contrib_update_list,
auth=user.auth, expect_errors=True, bulk=True)
assert res.json['errors'][0]['detail'] == 'Bulk operation limit is 100, got 101.'
assert res.json['errors'][0]['detail'] == 'Bulk operation limit is 50, got 101.'
assert res.json['errors'][0]['source']['pointer'] == '/data'

# test_bulk_partial_update_invalid_permissions
Expand Down Expand Up @@ -2879,7 +2879,7 @@ def test_bulk_delete_contributors_errors(
url_published, new_payload, auth=user.auth,
expect_errors=True, bulk=True)
assert res.status_code == 400
assert res.json['errors'][0]['detail'] == 'Bulk operation limit is 100, got 101.'
assert res.json['errors'][0]['detail'] == 'Bulk operation limit is 50, got 101.'
assert res.json['errors'][0]['source']['pointer'] == '/data'

# test_bulk_delete_contributors_no_payload
Expand Down