From c47481a2ecd546ce0b357d67482f261e0d68910d Mon Sep 17 00:00:00 2001 From: Matt Frazier Date: Mon, 8 Jul 2024 13:21:17 -0400 Subject: [PATCH 01/11] Update CHANGELOG, bump version --- CHANGELOG | 11 ++++++++++- package.json | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 1db53b2d592..cb59936325a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,7 +2,16 @@ We follow the CalVer (https://calver.org/) versioning scheme: YY.MINOR.MICRO. -24.01.0 (2024-04-30) +24.03.0 (2024-07-08) +==================== +- Allow AbstractProviders to specify Discover page presence +- Update get_auth +- Expand Addons Service support functionality, waffled +- Set default Resource Type General for Registrations +- Bug fix: Archiver Unicode edge cases +- Bug fix: RelationshipField during project creation + +24.02.0 (2024-04-30) ==================== - Initial Addons Service work, Waffled - Improve default `description` for Google Dataset Discovery diff --git a/package.json b/package.json index d95254712c5..4a59f79fe4f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "OSF", - "version": "24.02.0", + "version": "24.03.0", "description": "Facilitating Open Science", "repository": "https://github.com/CenterForOpenScience/osf.io", "author": "Center for Open Science", From ed3fadd77e0f7f005ab45b6e0d968dfc4af16136 Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Mon, 8 Jul 2024 14:19:38 -0400 Subject: [PATCH 02/11] [ENG-5030] Preprints Phase 2 - BE (#10617) * Add original_publication_citation to preprints model and serializer * Fix an issue where original_publication_citation was not updated * Add unit tests * Respond to CR: * Rename original_publication_citation to custom_publication_citation * Redo migration * Redo migrations * Update preprints to route to EOW * Remvoe the use of preprints_dir * Add self link and view for preprint subjects relationship (#10619) * add self link and view for preprint subjects relationship * Fix permissions issue * Add required write scopes --------- Co-authored-by: Brian J. Geiger * Get root_folder for preprints (#10630) ## Purpose Make it so preprints api serialization has a root_folder ## Changes 1. Modify the view to get the root_folder in the case of preprints ## Side Effects Should only affect preprints ## Ticket https://openscience.atlassian.net/browse/ENG-5716 --------- Co-authored-by: Yuhuai Liu Co-authored-by: Brian J. Geiger Co-authored-by: Brian J. Geiger --- api/nodes/views.py | 9 ++++- api/preprints/serializers.py | 10 ++++++ api/preprints/urls.py | 1 + api/preprints/views.py | 33 ++++++++++++++++++- .../preprints/views/test_preprint_detail.py | 23 +++++++++++++ ...21_preprint_custom_publication_citation.py | 18 ++++++++++ osf/models/preprint.py | 1 + website/routes.py | 10 +----- website/views.py | 3 -- 9 files changed, 94 insertions(+), 14 deletions(-) create mode 100644 osf/migrations/0021_preprint_custom_publication_citation.py diff --git a/api/nodes/views.py b/api/nodes/views.py index 23777b677b9..dae613cf868 100644 --- a/api/nodes/views.py +++ b/api/nodes/views.py @@ -153,6 +153,7 @@ File, Folder, CedarMetadataRecord, + Preprint, ) from addons.osfstorage.models import Region from osf.utils.permissions import ADMIN, WRITE_NODE @@ -1481,12 +1482,18 @@ def __init__(self, node, provider_name, storage_addon=None): self.node_id = node._id self.pk = node._id self.id = node.id - self.root_folder = storage_addon.root_node if storage_addon else None + self.root_folder = self._get_root_folder(storage_addon) @property def target(self): return self.node + def _get_root_folder(self, storage_addon): + if isinstance(self.target, Preprint): + return self.target.root_folder + else: + return storage_addon.root_node if storage_addon else None + class NodeStorageProvidersList(JSONAPIBaseView, generics.ListAPIView, NodeMixin): """The documentation for this endpoint can be found [here](https://developer.osf.io/#operation/nodes_providers_list). """ diff --git a/api/preprints/serializers.py b/api/preprints/serializers.py index 6a00e581f4d..146490862af 100644 --- a/api/preprints/serializers.py +++ b/api/preprints/serializers.py @@ -111,6 +111,7 @@ class PreprintSerializer(TaxonomizableSerializerMixin, MetricsSerializerMixin, J date_modified = VersionedDateTimeField(source='modified', read_only=True) date_published = VersionedDateTimeField(read_only=True) original_publication_date = VersionedDateTimeField(required=False, allow_null=True) + custom_publication_citation = ser.CharField(required=False, allow_blank=True, allow_null=True) doi = ser.CharField(source='article_doi', required=False, allow_null=True) title = ser.CharField(required=True, max_length=512) description = ser.CharField(required=False, allow_blank=True, allow_null=True) @@ -222,6 +223,11 @@ def subjects_view_kwargs(self): # Overrides TaxonomizableSerializerMixin return {'preprint_id': '<_id>'} + @property + def subjects_self_view(self): + # Overrides TaxonomizableSerializerMixin + return 'preprints:preprint-relationships-subjects' + def get_preprint_url(self, obj): return absolute_reverse('preprints:preprint-detail', kwargs={'preprint_id': obj._id, 'version': self.context['request'].parser_context['kwargs']['version']}) @@ -326,6 +332,10 @@ def update(self, preprint, validated_data): preprint.original_publication_date = validated_data['original_publication_date'] or None save_preprint = True + if 'custom_publication_citation' in validated_data: + preprint.custom_publication_citation = validated_data['custom_publication_citation'] or None + save_preprint = True + if 'has_coi' in validated_data: try: preprint.update_has_coi(auth, validated_data['has_coi']) diff --git a/api/preprints/urls.py b/api/preprints/urls.py index 70c72d991f6..c8b48e6b1cd 100644 --- a/api/preprints/urls.py +++ b/api/preprints/urls.py @@ -16,6 +16,7 @@ re_path(r'^(?P\w+)/files/osfstorage/$', views.PreprintFilesList.as_view(), name=views.PreprintFilesList.view_name), re_path(r'^(?P\w+)/identifiers/$', views.PreprintIdentifierList.as_view(), name=views.PreprintIdentifierList.view_name), re_path(r'^(?P\w+)/relationships/node/$', views.PreprintNodeRelationship.as_view(), name=views.PreprintNodeRelationship.view_name), + re_path(r'^(?P\w+)/relationships/subjects/$', views.PreprintSubjectsRelationship.as_view(), name=views.PreprintSubjectsRelationship.view_name), re_path(r'^(?P\w+)/review_actions/$', views.PreprintActionList.as_view(), name=views.PreprintActionList.view_name), re_path(r'^(?P\w+)/requests/$', views.PreprintRequestListCreate.as_view(), name=views.PreprintRequestListCreate.view_name), re_path(r'^(?P\w+)/subjects/$', views.PreprintSubjectsList.as_view(), name=views.PreprintSubjectsList.view_name), diff --git a/api/preprints/views.py b/api/preprints/views.py index 08df330c7db..302b8623102 100644 --- a/api/preprints/views.py +++ b/api/preprints/views.py @@ -58,7 +58,7 @@ from api.requests.permissions import PreprintRequestPermission from api.requests.serializers import PreprintRequestSerializer, PreprintRequestCreateSerializer from api.requests.views import PreprintRequestMixin -from api.subjects.views import BaseResourceSubjectsList +from api.subjects.views import BaseResourceSubjectsList, SubjectRelationshipBaseView from api.base.metrics import PreprintMetricsViewMixin from osf.metrics import PreprintDownload, PreprintView @@ -456,6 +456,37 @@ class PreprintSubjectsList(BaseResourceSubjectsList, PreprintMixin): def get_resource(self): return self.get_preprint() + +class PreprintSubjectsRelationship(SubjectRelationshipBaseView, PreprintMixin): + """The documentation for this endpoint can be found [here](https://developer.osf.io/#operation/preprint_subjects_list). + """ + permission_classes = ( + drf_permissions.IsAuthenticatedOrReadOnly, + base_permissions.TokenHasScope, + ModeratorIfNeverPublicWithdrawn, + ContributorOrPublic, + PreprintPublishedOrWrite, + ) + + required_read_scopes = [CoreScopes.PREPRINTS_READ] + required_write_scopes = [CoreScopes.PREPRINTS_WRITE] + + view_category = 'preprints' + view_name = 'preprint-relationships-subjects' + + def get_resource(self, check_object_permissions=True): + return self.get_preprint(check_object_permissions=check_object_permissions) + + def get_object(self): + resource = self.get_resource(check_object_permissions=False) + obj = { + 'data': resource.subjects.all(), + 'self': resource, + } + self.check_object_permissions(self.request, resource) + return obj + + class PreprintActionList(JSONAPIBaseView, generics.ListCreateAPIView, ListFilterMixin, PreprintMixin): """Action List *Read-only* diff --git a/api_tests/preprints/views/test_preprint_detail.py b/api_tests/preprints/views/test_preprint_detail.py index aca17d6901e..332aea74a6c 100644 --- a/api_tests/preprints/views/test_preprint_detail.py +++ b/api_tests/preprints/views/test_preprint_detail.py @@ -305,6 +305,19 @@ def test_update_original_publication_date_to_none(self, app, preprint, url): preprint.reload() assert preprint.original_publication_date is None + def test_update_custom_publication_citation_to_none(self, app, preprint, url): + write_contrib = AuthUserFactory() + preprint.add_contributor(write_contrib, WRITE, save=True) + preprint.custom_publication_citation = 'fake citation' + preprint.save() + update_payload = build_preprint_update_payload( + preprint._id, attributes={'custom_publication_citation': None} + ) + res = app.patch_json_api(url, update_payload, auth=write_contrib.auth) + assert res.status_code == 200 + preprint.reload() + assert preprint.custom_publication_citation is None + @responses.activate @mock.patch('osf.models.preprint.update_or_enqueue_on_preprint_updated', mock.Mock()) def test_update_preprint_permission_write_contrib(self, app, preprint, url): @@ -536,6 +549,16 @@ def test_update_original_publication_date(self, app, user, preprint, url): preprint.reload() assert preprint.original_publication_date == date + def test_update_custom_publication_citation(self, app, user, preprint, url): + citation = 'fake citation' + update_payload = build_preprint_update_payload( + preprint._id, attributes={'custom_publication_citation': citation} + ) + res = app.patch_json_api(url, update_payload, auth=user.auth) + assert res.status_code == 200 + preprint.reload() + assert preprint.custom_publication_citation == citation + @responses.activate @mock.patch('osf.models.preprint.update_or_enqueue_on_preprint_updated', mock.Mock()) def test_update_article_doi(self, app, user, preprint, url): diff --git a/osf/migrations/0021_preprint_custom_publication_citation.py b/osf/migrations/0021_preprint_custom_publication_citation.py new file mode 100644 index 00000000000..678d3bd2192 --- /dev/null +++ b/osf/migrations/0021_preprint_custom_publication_citation.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.17 on 2024-05-15 15:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('osf', '0020_abstractprovider_advertise_on_discover_page'), + ] + + operations = [ + migrations.AddField( + model_name='preprint', + name='custom_publication_citation', + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/osf/models/preprint.py b/osf/models/preprint.py index 428b334c853..0a188e9f28b 100644 --- a/osf/models/preprint.py +++ b/osf/models/preprint.py @@ -152,6 +152,7 @@ class Preprint(DirtyFieldsMixin, GuidMixin, IdentifierMixin, ReviewableMixin, Ba is_published = models.BooleanField(default=False, db_index=True) date_published = NonNaiveDateTimeField(null=True, blank=True) original_publication_date = NonNaiveDateTimeField(null=True, blank=True) + custom_publication_citation = models.TextField(null=True, blank=True) license = models.ForeignKey('osf.NodeLicenseRecord', on_delete=models.SET_NULL, null=True, blank=True) diff --git a/website/routes.py b/website/routes.py index 787fe2e367b..eb3f93fd186 100644 --- a/website/routes.py +++ b/website/routes.py @@ -261,15 +261,7 @@ def ember_app(path=None): if request.path.strip('/').startswith(k): ember_app = EXTERNAL_EMBER_APPS[k] if k == 'preprints': - if request.path.rstrip('/').endswith('edit'): - # Route preprint edit pages to old preprint app - ember_app = EXTERNAL_EMBER_APPS.get('preprints', False) or ember_app - elif request.path.rstrip('/').endswith('submit'): - # Route preprint submit pages to old preprint app - ember_app = EXTERNAL_EMBER_APPS.get('preprints', False) or ember_app - else: - # Route other preprint pages to EOW - ember_app = EXTERNAL_EMBER_APPS.get('ember_osf_web', False) or ember_app + ember_app = EXTERNAL_EMBER_APPS.get('ember_osf_web', False) or ember_app break if not ember_app: diff --git a/website/views.py b/website/views.py index adee1d27887..90fb285d499 100644 --- a/website/views.py +++ b/website/views.py @@ -38,7 +38,6 @@ from api.waffle.utils import storage_i18n_flag_active logger = logging.getLogger(__name__) -preprints_dir = os.path.abspath(os.path.join(os.getcwd(), EXTERNAL_EMBER_APPS['preprints']['path'])) ember_osf_web_dir = os.path.abspath(os.path.join(os.getcwd(), EXTERNAL_EMBER_APPS['ember_osf_web']['path'])) @@ -332,8 +331,6 @@ def resolve_guid(guid, suffix=None): if isinstance(resource, Preprint): if resource.provider.domain_redirect_enabled: return redirect(resource.absolute_url, http_status.HTTP_301_MOVED_PERMANENTLY) - if clean_suffix.endswith('edit'): - return stream_emberapp(EXTERNAL_EMBER_APPS['preprints']['server'], preprints_dir) return use_ember_app() elif isinstance(resource, Registration) and (clean_suffix in ('', 'comments', 'links', 'components', 'resources',)) and waffle.flag_is_active(request, features.EMBER_REGISTRIES_DETAIL_PAGE): From bdbcc63db293f1f51b970c0582de54918f9eacc3 Mon Sep 17 00:00:00 2001 From: Matt Frazier Date: Mon, 8 Jul 2024 14:26:36 -0400 Subject: [PATCH 03/11] Update CHANGELOG, bump version --- CHANGELOG | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index cb59936325a..f7b89a25aeb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,10 @@ We follow the CalVer (https://calver.org/) versioning scheme: YY.MINOR.MICRO. +24.03.0 (2024-07-08) +==================== +- Preprints into Ember OSF Web, Phase 2 Backend Release + 24.03.0 (2024-07-08) ==================== - Allow AbstractProviders to specify Discover page presence diff --git a/package.json b/package.json index 4a59f79fe4f..e23665cd5f1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "OSF", - "version": "24.03.0", + "version": "24.04.0", "description": "Facilitating Open Science", "repository": "https://github.com/CenterForOpenScience/osf.io", "author": "Center for Open Science", From 9cec4c21e4b555881111f8f196b9bbf6323d6e29 Mon Sep 17 00:00:00 2001 From: Uditi Mehta <57388785+uditijmehta@users.noreply.github.com> Date: Tue, 9 Jul 2024 14:51:48 -0400 Subject: [PATCH 04/11] [ENG-3685] Add permissions for withdrawn registration files (#10650) * Prevent users from accessing files from withdrawn registrations via API and Waterbutler --------- Co-authored-by: Uditi Mehta Co-authored-by: Uditi Mehta --- addons/base/views.py | 3 + api/files/serializers.py | 1 - api/files/views.py | 3 + api_tests/files/views/test_file_detail.py | 76 +++++++++++++++++++++-- 4 files changed, 78 insertions(+), 5 deletions(-) diff --git a/addons/base/views.py b/addons/base/views.py index 3394e6dc22b..b78f264697f 100644 --- a/addons/base/views.py +++ b/addons/base/views.py @@ -305,6 +305,9 @@ def get_authenticated_resource(resource_id): if resource.deleted: raise HTTPError(http_status.HTTP_410_GONE, message='Resource has been deleted.') + if getattr(resource, 'is_retracted', False): + raise HTTPError(http_status.HTTP_410_GONE, message='Resource has been retracted.') + return resource diff --git a/api/files/serializers.py b/api/files/serializers.py index 9e92bca2037..7680cb560e1 100644 --- a/api/files/serializers.py +++ b/api/files/serializers.py @@ -441,7 +441,6 @@ def to_representation(self, value): guid = Guid.load(view.kwargs['file_id']) if guid: data['data']['id'] = guid._id - return data diff --git a/api/files/views.py b/api/files/views.py index 15999637336..2c4aae80976 100644 --- a/api/files/views.py +++ b/api/files/views.py @@ -57,6 +57,9 @@ def get_file(self, check_permissions=True): if obj.target.creator.is_disabled: raise Gone(detail='This user has been deactivated and their quickfiles are no longer available.') + if getattr(obj.target, 'is_retracted', False): + raise Gone(detail='The requested file is no longer available.') + if check_permissions: # May raise a permission denied self.check_object_permissions(self.request, obj) diff --git a/api_tests/files/views/test_file_detail.py b/api_tests/files/views/test_file_detail.py index 14b95016e36..0c6e7876fe4 100644 --- a/api_tests/files/views/test_file_detail.py +++ b/api_tests/files/views/test_file_detail.py @@ -31,6 +31,9 @@ SessionStore = import_module(django_conf_settings.SESSION_ENGINE).SessionStore +from addons.base.views import get_authenticated_resource +from framework.exceptions import HTTPError + # stolen from^W^Winspired by DRF # rest_framework.fields.DateTimeField.to_representation def _dt_to_iso8601(value): @@ -639,6 +642,10 @@ def file(self, root_node, user): }).save() return file + @pytest.fixture() + def file_url(self, file): + return '/{}files/{}/'.format(API_BASE, file._id) + def test_listing(self, app, user, file): file.create_version(user, { 'object': '0683m38e', @@ -705,6 +712,67 @@ def test_load_and_property(self, app, user, file): expect_errors=True, auth=user.auth, ).status_code == 405 + def test_retracted_registration_file(self, app, user, file_url, file): + resource = RegistrationFactory(is_public=True) + retraction = resource.retract_registration( + user=resource.creator, + justification='Justification for retraction', + save=True, + moderator_initiated=False + ) + + retraction.accept() + resource.save() + resource.refresh_from_db() + + file.target = resource + file.save() + + res = app.get(file_url, auth=user.auth, expect_errors=True) + assert res.status_code == 410 + + def test_retracted_file_returns_410(self, app, user, file_url, file): + resource = RegistrationFactory(is_public=True) + retraction = resource.retract_registration( + user=resource.creator, + justification='Justification for retraction', + save=True, + moderator_initiated=False + ) + + retraction.accept() + resource.save() + resource.refresh_from_db() + + file.target = resource + file.save() + + res = app.get(file_url, auth=user.auth, expect_errors=True) + assert res.status_code == 410 + + def test_get_authenticated_resource_retracted(self): + resource = RegistrationFactory(is_public=True) + + assert resource.is_retracted is False + + retraction = resource.retract_registration( + user=resource.creator, + justification='Justification for retraction', + save=True, + moderator_initiated=False + ) + + retraction.accept() + resource.save() + resource.refresh_from_db() + + assert resource.is_retracted is True + + with pytest.raises(HTTPError) as excinfo: + get_authenticated_resource(resource._id) + + assert excinfo.value.code == 410 + @pytest.mark.django_db class TestFileTagging: @@ -916,20 +984,20 @@ def test_withdrawn_preprint_files(self, app, file_url, preprint, user, other_use # Unauthenticated res = app.get(file_url, expect_errors=True) - assert res.status_code == 401 + assert res.status_code == 410 # Noncontrib res = app.get(file_url, auth=other_user.auth, expect_errors=True) - assert res.status_code == 403 + assert res.status_code == 410 # Write contributor preprint.add_contributor(other_user, WRITE, save=True) res = app.get(file_url, auth=other_user.auth, expect_errors=True) - assert res.status_code == 403 + assert res.status_code == 410 # Admin contrib res = app.get(file_url, auth=user.auth, expect_errors=True) - assert res.status_code == 403 + assert res.status_code == 410 @pytest.mark.django_db class TestShowAsUnviewed: From ed34ace57ed74ae906781215391e3ef02180bbf8 Mon Sep 17 00:00:00 2001 From: Matt Frazier Date: Tue, 9 Jul 2024 17:00:14 -0400 Subject: [PATCH 05/11] Allow DOI metadata updates to be queued --- website/identifiers/tasks.py | 11 +++++++---- website/settings/defaults.py | 3 ++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/website/identifiers/tasks.py b/website/identifiers/tasks.py index 2fd51428dc3..e0823cec5cd 100644 --- a/website/identifiers/tasks.py +++ b/website/identifiers/tasks.py @@ -5,12 +5,15 @@ from framework.celery_tasks.handlers import queued_task from framework import sentry - -@queued_task -@celery_app.task(ignore_results=True) -def update_doi_metadata_on_change(target_guid): +@celery_app.task(bind=True, max_retries=5, acks_late=True) +def task__update_doi_metadata_on_change(target_guid): sentry.log_message('Updating DOI for guid', extra_data={'guid': target_guid}, level=logging.INFO) Guid = apps.get_model('osf.Guid') target_object = Guid.load(target_guid).referent if target_object.get_identifier('doi'): target_object.request_identifier_update(category='doi') + +@queued_task +@celery_app.task(ignore_results=True) +def update_doi_metadata_on_change(target_guid): + task__update_doi_metadata_on_change(target_guid) diff --git a/website/settings/defaults.py b/website/settings/defaults.py index ce44070357b..79200467839 100644 --- a/website/settings/defaults.py +++ b/website/settings/defaults.py @@ -455,7 +455,7 @@ class CeleryConfig: 'website.mailchimp_utils', 'website.notifications.tasks', 'website.collections.tasks', - 'website.identifier.tasks', + 'website.identifiers.tasks', 'website.preprints.tasks', 'website.project.tasks', } @@ -516,6 +516,7 @@ class CeleryConfig: 'website.mailchimp_utils', 'website.notifications.tasks', 'website.archiver.tasks', + 'website.identifiers.tasks', 'website.search.search', 'website.project.tasks', 'scripts.populate_new_and_noteworthy_projects', From 799cb57b7da84ca9efd8680697cc90c6f41004ad Mon Sep 17 00:00:00 2001 From: Matt Frazier Date: Tue, 9 Jul 2024 18:03:45 -0400 Subject: [PATCH 06/11] Fix signature --- website/identifiers/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/identifiers/tasks.py b/website/identifiers/tasks.py index e0823cec5cd..f940956d54b 100644 --- a/website/identifiers/tasks.py +++ b/website/identifiers/tasks.py @@ -6,7 +6,7 @@ from framework import sentry @celery_app.task(bind=True, max_retries=5, acks_late=True) -def task__update_doi_metadata_on_change(target_guid): +def task__update_doi_metadata_on_change(self, target_guid): sentry.log_message('Updating DOI for guid', extra_data={'guid': target_guid}, level=logging.INFO) Guid = apps.get_model('osf.Guid') target_object = Guid.load(target_guid).referent From 30604a0d6b6fd3aa3f8dc5db274c520589ceea38 Mon Sep 17 00:00:00 2001 From: Matt Frazier Date: Thu, 11 Jul 2024 12:35:41 -0400 Subject: [PATCH 07/11] Check Registration READ perms on the Registration - Do not record download metrics for renders --- addons/base/views.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/addons/base/views.py b/addons/base/views.py index 3394e6dc22b..e3dc206164e 100644 --- a/addons/base/views.py +++ b/addons/base/views.py @@ -206,7 +206,7 @@ def check_resource_permissions(resource, auth, action): def _check_registration_permissions(registration, auth, permission, action): if permission == permissions.READ: - return registration.registered_from.can_view(auth) + return registration.can_view(auth) or registration.registered_from.can_view(auth) if action in ('copyfrom', 'upload'): return _check_hierarchical_write_permissions(resource=registration, auth=auth) return registration.can_edit(auth) @@ -241,6 +241,19 @@ def _check_hierarchical_write_permissions(resource, auth): permissions_resource = permissions_resource.parent_node return False +def _download_is_from_mfr(waterbutler_data): + metrics_data = waterbutler_data['metrics'] + uri = metrics_data['uri'] + is_render_uri = furl.furl(uri or '').query.params.get('mode') == 'render' + return ( + # This header is sent for download requests that + # originate from MFR, e.g. for the code pygments renderer + request.headers.get('X-Cos-Mfr-Render-Request', None) or + # Need to check the URI in order to account + # for renderers that send XHRs from the + # rendered content, e.g. PDFs + is_render_uri + ) def make_auth(user): if user is not None: @@ -412,7 +425,7 @@ def get_auth(auth, **kwargs): # Trigger any file-specific signals based on the action taken (e.g., file viewed, downloaded) if action == 'render': file_signals.file_viewed.send(auth=auth, fileversion=fileversion, file_node=file_node) - elif action == 'download': + elif action == 'download' and not _download_is_from_mfr(waterbutler_data): file_signals.file_downloaded.send(auth=auth, fileversion=fileversion, file_node=file_node) # Construct the response payload including the JWT From 34d56414859066b98c0998fbc00cd130bf5de4fd Mon Sep 17 00:00:00 2001 From: Fitz Elliott Date: Fri, 12 Jul 2024 13:05:54 -0400 Subject: [PATCH 08/11] Revert "[ENG-3685] Add permissions for withdrawn registration files (#10650)" (#10666) This reverts commit 9cec4c21e4b555881111f8f196b9bbf6323d6e29. --- addons/base/views.py | 3 - api/files/serializers.py | 1 + api/files/views.py | 3 - api_tests/files/views/test_file_detail.py | 76 ++--------------------- 4 files changed, 5 insertions(+), 78 deletions(-) diff --git a/addons/base/views.py b/addons/base/views.py index 834e6c32817..e3dc206164e 100644 --- a/addons/base/views.py +++ b/addons/base/views.py @@ -318,9 +318,6 @@ def get_authenticated_resource(resource_id): if resource.deleted: raise HTTPError(http_status.HTTP_410_GONE, message='Resource has been deleted.') - if getattr(resource, 'is_retracted', False): - raise HTTPError(http_status.HTTP_410_GONE, message='Resource has been retracted.') - return resource diff --git a/api/files/serializers.py b/api/files/serializers.py index 7680cb560e1..9e92bca2037 100644 --- a/api/files/serializers.py +++ b/api/files/serializers.py @@ -441,6 +441,7 @@ def to_representation(self, value): guid = Guid.load(view.kwargs['file_id']) if guid: data['data']['id'] = guid._id + return data diff --git a/api/files/views.py b/api/files/views.py index 2c4aae80976..15999637336 100644 --- a/api/files/views.py +++ b/api/files/views.py @@ -57,9 +57,6 @@ def get_file(self, check_permissions=True): if obj.target.creator.is_disabled: raise Gone(detail='This user has been deactivated and their quickfiles are no longer available.') - if getattr(obj.target, 'is_retracted', False): - raise Gone(detail='The requested file is no longer available.') - if check_permissions: # May raise a permission denied self.check_object_permissions(self.request, obj) diff --git a/api_tests/files/views/test_file_detail.py b/api_tests/files/views/test_file_detail.py index 0c6e7876fe4..14b95016e36 100644 --- a/api_tests/files/views/test_file_detail.py +++ b/api_tests/files/views/test_file_detail.py @@ -31,9 +31,6 @@ SessionStore = import_module(django_conf_settings.SESSION_ENGINE).SessionStore -from addons.base.views import get_authenticated_resource -from framework.exceptions import HTTPError - # stolen from^W^Winspired by DRF # rest_framework.fields.DateTimeField.to_representation def _dt_to_iso8601(value): @@ -642,10 +639,6 @@ def file(self, root_node, user): }).save() return file - @pytest.fixture() - def file_url(self, file): - return '/{}files/{}/'.format(API_BASE, file._id) - def test_listing(self, app, user, file): file.create_version(user, { 'object': '0683m38e', @@ -712,67 +705,6 @@ def test_load_and_property(self, app, user, file): expect_errors=True, auth=user.auth, ).status_code == 405 - def test_retracted_registration_file(self, app, user, file_url, file): - resource = RegistrationFactory(is_public=True) - retraction = resource.retract_registration( - user=resource.creator, - justification='Justification for retraction', - save=True, - moderator_initiated=False - ) - - retraction.accept() - resource.save() - resource.refresh_from_db() - - file.target = resource - file.save() - - res = app.get(file_url, auth=user.auth, expect_errors=True) - assert res.status_code == 410 - - def test_retracted_file_returns_410(self, app, user, file_url, file): - resource = RegistrationFactory(is_public=True) - retraction = resource.retract_registration( - user=resource.creator, - justification='Justification for retraction', - save=True, - moderator_initiated=False - ) - - retraction.accept() - resource.save() - resource.refresh_from_db() - - file.target = resource - file.save() - - res = app.get(file_url, auth=user.auth, expect_errors=True) - assert res.status_code == 410 - - def test_get_authenticated_resource_retracted(self): - resource = RegistrationFactory(is_public=True) - - assert resource.is_retracted is False - - retraction = resource.retract_registration( - user=resource.creator, - justification='Justification for retraction', - save=True, - moderator_initiated=False - ) - - retraction.accept() - resource.save() - resource.refresh_from_db() - - assert resource.is_retracted is True - - with pytest.raises(HTTPError) as excinfo: - get_authenticated_resource(resource._id) - - assert excinfo.value.code == 410 - @pytest.mark.django_db class TestFileTagging: @@ -984,20 +916,20 @@ def test_withdrawn_preprint_files(self, app, file_url, preprint, user, other_use # Unauthenticated res = app.get(file_url, expect_errors=True) - assert res.status_code == 410 + assert res.status_code == 401 # Noncontrib res = app.get(file_url, auth=other_user.auth, expect_errors=True) - assert res.status_code == 410 + assert res.status_code == 403 # Write contributor preprint.add_contributor(other_user, WRITE, save=True) res = app.get(file_url, auth=other_user.auth, expect_errors=True) - assert res.status_code == 410 + assert res.status_code == 403 # Admin contrib res = app.get(file_url, auth=user.auth, expect_errors=True) - assert res.status_code == 410 + assert res.status_code == 403 @pytest.mark.django_db class TestShowAsUnviewed: From 6c32a94a4bbd9e11bc74a6281804d1715c96f235 Mon Sep 17 00:00:00 2001 From: Jon Walz Date: Fri, 28 Jun 2024 10:18:16 -0400 Subject: [PATCH 09/11] [ENG-5727][ENG-5728] Get Addon information from GravyValet (#10652) * Get addon data for external providers from GravyValet (if flag is active) * Further improve `get_auth` --------- Co-authored-by: Jon Walz --- addons/base/views.py | 392 ++++++++++------------- osf/external/gravy_valet/auth_helpers.py | 5 +- osf/external/gravy_valet/translations.py | 15 +- osf/models/draft_node.py | 6 + osf/models/mixins.py | 20 ++ osf/models/node.py | 30 ++ osf/models/preprint.py | 4 + osf/models/user.py | 32 ++ tests/test_addons.py | 364 +++++++++------------ tests/test_preprints.py | 50 ++- tests/test_utils.py | 8 +- 11 files changed, 475 insertions(+), 451 deletions(-) diff --git a/addons/base/views.py b/addons/base/views.py index e3dc206164e..696b499505a 100644 --- a/addons/base/views.py +++ b/addons/base/views.py @@ -1,5 +1,4 @@ import datetime -from rest_framework import status as http_status import os import uuid import markupsafe @@ -15,6 +14,7 @@ from django.db import transaction from django.contrib.contenttypes.models import ContentType from elasticsearch import exceptions as es_exceptions +from rest_framework import status as http_status from api.caching.tasks import update_storage_usage_with_size @@ -32,7 +32,7 @@ from framework.exceptions import HTTPError from framework.flask import redirect from framework.sentry import log_exception -from framework.routing import json_renderer, proxy_url +from framework.routing import proxy_url from framework.transactions.handlers import no_auto_transaction from website import mails from website import settings @@ -45,7 +45,6 @@ BaseFileVersionsThrough, OSFUser, AbstractNode, - DraftNode, Preprint, Node, NodeLog, @@ -65,7 +64,6 @@ # import so that associated listener is instantiated and gets emails from website.notifications.events.files import FileEvent # noqa -from osf.utils.requests import requests_retry_session ERROR_MESSAGES = {'FILE_GONE': u"""