diff --git a/addons/osfstorage/models.py b/addons/osfstorage/models.py index 86402523377..9554036dfa1 100644 --- a/addons/osfstorage/models.py +++ b/addons/osfstorage/models.py @@ -16,7 +16,7 @@ from osf.utils import permissions from website.files import exceptions from website.files import utils as files_utils -from website.util import api_url_for +# from website.util import api_url_for from website import settings as website_settings from addons.osfstorage.settings import DEFAULT_REGION_ID from website.util import api_v2_url @@ -608,12 +608,13 @@ def serialize_waterbutler_settings(self): return dict(Region.objects.get(id=self.region_id).waterbutler_settings, **{ 'nid': self.owner._id, 'rootId': self.root_node._id, - 'baseUrl': api_url_for( - 'osfstorage_get_metadata', - guid=self.owner._id, - _absolute=True, - _internal=True - ), + 'baseUrl': f'http://localhost:5000/api/v1/{self.owner._id}/osfstorage/' + # 'baseUrl': api_url_for( + # 'osfstorage_get_metadata', + # guid=self.owner._id, + # _absolute=True, + # _internal=True + # ), }) def serialize_waterbutler_credentials(self): @@ -628,12 +629,13 @@ def create_waterbutler_log(self, auth, action, metadata): } if (metadata['kind'] != 'folder'): - url = self.owner.web_url_for( - 'addon_view_or_download_file', - guid=self.owner._id, - path=metadata['path'], - provider='osfstorage' - ) + url = 'url' + # url = self.owner.web_url_for( + # 'addon_view_or_download_file', + # guid=self.owner._id, + # path=metadata['path'], + # provider='osfstorage' + # ) params['urls'] = {'view': url, 'download': url + '?action=download'} self.owner.add_log( diff --git a/api/files/urls.py b/api/files/urls.py index b883801568b..4710677dd56 100644 --- a/api/files/urls.py +++ b/api/files/urls.py @@ -1,10 +1,18 @@ -from django.urls import re_path +from django.urls import path, re_path from api.files import views app_name = 'osf' urlpatterns = [ + +] + +urlpatterns = [ + path('auth/', views.WaterbutlerAuthView.as_view(), name='WaterbutlerAuthView'), + path('project//waterbutler/logs/', views.WaterbutlerLogView.as_view(), name='waterbutler-logs-project'), + path('project//node//waterbutler/logs/', views.WaterbutlerLogView.as_view(), name='waterbutler-logs-node'), + re_path(r'^(?P\w+)/$', views.FileDetail.as_view(), name=views.FileDetail.view_name), re_path(r'^(?P\w+)/cedar_metadata_records/$', views.FileCedarMetadataRecordsList.as_view(), name=views.FileCedarMetadataRecordsList.view_name), re_path(r'^(?P\w+)/versions/$', views.FileVersionsList.as_view(), name=views.FileVersionsList.view_name), diff --git a/api/files/views.py b/api/files/views.py index d96289d4010..22df13c04ab 100644 --- a/api/files/views.py +++ b/api/files/views.py @@ -198,3 +198,475 @@ def get_default_queryset(self): def get_queryset(self): return self.get_queryset_from_request() + +from website import settings +from rest_framework import status +from rest_framework.response import Response +from rest_framework.views import APIView +from framework.auth import Auth +from osf import features +from waffle import flag_is_active +from api.base import permissions as base_permissions +from addons.base import views as request_helpers + +def _decrypt_and_decode_jwt_payload(payload): + try: + payload_encrypted = payload.encode('utf-8') + payload_decrypted = jwe.decrypt(payload_encrypted, request_helpers.WATERBUTLER_JWE_KEY) + from website import settings + return jwt.decode( + payload_decrypted, + settings.WATERBUTLER_JWT_SECRET, + options={'require_exp': True}, + algorithms=[settings.WATERBUTLER_JWT_ALGORITHM], + )['data'] + except (jwt.InvalidTokenError, KeyError) as err: + raise err + +class WaterbutlerAuthView(APIView): + """ + Authenticate a request and construct a JWT payload for Waterbutler callbacks. + """ + + def get(self, request, *args, **kwargs): + + print('*' * 100) + auth = Auth(user=request.user) + # Decode incoming WB payload + waterbutler_data = _decrypt_and_decode_jwt_payload(request.GET['payload']) + # Resolve target resource + resource = request_helpers._get_authenticated_resource( + waterbutler_data['nid'], + ) + # action = waterbutler_data['action'] + # Validate permissions + + # request_helpers._check_resource_permissions( + # resource=resource, + # auth=auth, + # action=action, + # ) + provider_name = waterbutler_data['provider'] + file_version = None + file_node = None + # osfstorage / legacy flow + if ( + provider_name == 'osfstorage' + or not flag_is_active(request, features.ENABLE_GV) + ): + file_version, file_node = ( + request_helpers._get_osfstorage_file_version_and_node( + file_path=waterbutler_data.get('path'), + file_version_id=waterbutler_data.get('version'), + ) + ) + ( + waterbutler_settings, + waterbutler_credentials, + ) = request_helpers._get_waterbutler_configs( + resource=resource, + provider_name=provider_name, + file_version=file_version, + ) + # GV provider flow + else: + result = request_helpers.get_waterbutler_config( + gv_addon_pk=( + f"{waterbutler_data['nid']}:" + f"{waterbutler_data['provider']}" + ), + requested_resource=resource, + requesting_user=auth.user, + addon_type='configured-storage-addons', + ) + if not result: + return Response( + { + 'detail': ( + 'Requested Provider is not configured ' + 'for given node' + ), + }, + status=status.HTTP_404_NOT_FOUND, + ) + waterbutler_settings = result.get_attribute('config') + waterbutler_credentials = result.get_attribute('credentials') + # Metrics/logging + # request_helpers._enqueue_metrics( + # file_version=file_version, + # file_node=file_node, + # action=action, + # auth=auth, + # from_mfr=request_helpers._download_is_from_mfr(waterbutler_data), + # ) + # Construct WB response payload + payload = _construct_payload( + auth=auth, + resource=resource, + credentials=waterbutler_credentials, + waterbutler_settings=waterbutler_settings, + ) + + return Response(payload, status=status.HTTP_200_OK) + +from osf.models import Registration +from django.utils import timezone +import datetime +import jwe +import jwt + +def _construct_payload(auth, resource, credentials, waterbutler_settings): + + if isinstance(resource, Registration): + callback_url = resource.callbacks_url + else: + callback_url = f'http://localhost:8000/v2/files/project/{resource._primary_key}/waterbutler/logs/' + # callback_url = resource.api_url_for( + # 'create_waterbutler_log', + # _absolute=True, + # _internal=True + # ) + + # Construct the data dictionary for JWT encoding + from website import settings + jwt_data = { + 'exp': timezone.now() + datetime.timedelta(seconds=settings.WATERBUTLER_JWT_EXPIRATION), + 'data': { + 'auth': request_helpers.make_auth(auth.user), + 'credentials': credentials, + 'settings': waterbutler_settings, + 'callback_url': callback_url, + }, + } + + # JWT encode the data + encoded_jwt = jwt.encode( + jwt_data, + settings.WATERBUTLER_JWT_SECRET, + algorithm=settings.WATERBUTLER_JWT_ALGORITHM, + ) + + # Encrypt the encoded JWT with JWE + decoded_encrypted_jwt = jwe.encrypt( + encoded_jwt.encode(), + request_helpers.WATERBUTLER_JWE_KEY, + ).decode() + + return {'payload': decoded_encrypted_jwt} + +import os +from django.db import transaction + +from rest_framework.views import APIView + +from framework.exceptions import HTTPError +import copy +from api.base.parsers import HMACSignedParser +from website.project.decorators import _inject_nodes +class WaterbutlerLogView(APIView): + + parser_classes = [HMACSignedParser] + + authentication_classes = [] + permission_classes = [] + + def put(self, request, *args, **kwargs): + payload = copy.deepcopy(request.data) + + try: + if request_helpers.Preprint.load(kwargs.get('pid')): + _inject_nodes(kwargs) + + _inject_nodes(kwargs) + + if getattr(kwargs['node'], 'is_collection', True): + raise HTTPError( + 404, + ) + + result = self._create_waterbutler_log( + payload=payload, + **kwargs, + ) + + return Response(result, status=status.HTTP_200_OK) + + except HTTPError as exc: + return Response( + {'detail': str(exc)}, + status=getattr(exc, 'code', 400), + ) + + def _create_waterbutler_log(self, payload, **kwargs): + + with transaction.atomic(): + + try: + auth_data = payload['auth'] + + # Don't log download actions + if payload['action'] in request_helpers.DOWNLOAD_ACTIONS: + guid_id = payload['metadata'].get('nid') + + node, _ = Guid.load_referent(guid_id) + + return {'status': 'success'} + + user = request_helpers.OSFUser.load(auth_data['id']) + + if user is None: + raise HTTPError(status.HTTP_400_BAD_REQUEST) + + action = request_helpers.LOG_ACTION_MAP[payload['action']] + + except KeyError: + raise HTTPError(status.HTTP_400_BAD_REQUEST) + + auth = Auth(user=user) + + node = ( + kwargs.get('node') + or kwargs.get('project') + or request_helpers.Preprint.load(kwargs.get('nid')) + or request_helpers.Preprint.load(kwargs.get('pid')) + ) + + # + # MOVE / COPY FLOW + # + if action in ( + request_helpers.NodeLog.FILE_MOVED, + request_helpers.NodeLog.FILE_COPIED, + ): + + for bundle in ('source', 'destination'): + for key in ( + 'provider', + 'materialized', + 'name', + 'nid', + ): + if key not in payload[bundle]: + raise HTTPError( + status.HTTP_400_BAD_REQUEST, + ) + + dest = payload['destination'] + src = payload['source'] + + # + # Detect rename + # + if src is not None and dest is not None: + + dest_path = dest['materialized'] + src_path = src['materialized'] + + if ( + dest_path.endswith('/') + and src_path.endswith('/') + ): + dest_path = os.path.dirname(dest_path) + src_path = os.path.dirname(src_path) + + if ( + os.path.split(dest_path)[0] + == os.path.split(src_path)[0] + and dest['provider'] == src['provider'] + and dest['nid'] == src['nid'] + and dest['name'] != src['name'] + ): + action = request_helpers.LOG_ACTION_MAP['rename'] + + destination_node = node + source_node = ( + request_helpers.AbstractNode.load(src['nid']) + or request_helpers.Preprint.load(src['nid']) + ) + + # + # Resolve addons + # + source = None + if hasattr(source_node, 'get_addon'): + source = source_node.get_addon( + payload['source']['provider'], + ) + + destination = None + if hasattr(node, 'get_addon'): + destination = node.get_addon( + payload['destination']['provider'], + ) + + # + # Enrich source payload + # + payload['source'].update({ + 'materialized': payload['source'][ + 'materialized' + ].lstrip('/'), + + 'addon': ( + source.config.full_name + if source else 'osfstorage' + ), + + 'url': source_node.web_url_for( + 'addon_view_or_download_file', + path=payload['source']['path'].lstrip('/'), + provider=payload['source']['provider'], + ), + + 'node': { + '_id': source_node._id, + 'url': source_node.url, + 'title': source_node.title, + }, + }) + + # + # Enrich destination payload + # + payload['destination'].update({ + 'materialized': payload['destination'][ + 'materialized' + ].lstrip('/'), + + 'addon': ( + destination.config.full_name + if destination else 'osfstorage' + ), + + 'url': destination_node.web_url_for( + 'addon_view_or_download_file', + path=payload['destination'][ + 'path' + ].lstrip('/'), + provider=payload['destination'][ + 'provider' + ], + ), + + 'node': { + '_id': destination_node._id, + 'url': destination_node.url, + 'title': destination_node.title, + }, + }) + + # + # Create log + # + if not payload.get('errors'): + destination_node.add_log( + action=action, + auth=auth, + params=payload, + ) + + # + # Notifications + # + if ( + payload.get('email') + or payload.get('errors') + ): + + if payload.get('email'): + notification_type = ( + request_helpers.NotificationTypeEnum + .USER_FILE_OPERATION_SUCCESS + .instance + ) + + if payload.get('errors'): + notification_type = ( + request_helpers.NotificationTypeEnum + .USER_FILE_OPERATION_FAILED + .instance + ) + + notification_type.emit( + user=user, + subscribed_object=node, + event_context={ + 'action': payload['action'], + 'source_node': source_node._id, + 'source_node_title': source_node.title, + 'destination_node': + destination_node._id, + 'destination_node_title': + destination_node.title, + 'destination_node_parent_node_title': ( + destination_node + .parent_node.title + if destination_node.parent_node + else None + ), + 'source_path': payload['source'][ + 'materialized' + ], + 'source_addon': + payload['source']['addon'], + 'destination_addon': + payload['destination']['addon'], + 'osf_support_email': + settings.OSF_SUPPORT_EMAIL, + 'logo': + settings.OSF_LOGO, + 'OSF_LOGO_LIST': + settings.OSF_LOGO_LIST, + 'OSF_LOGO': + settings.OSF_LOGO, + 'domain': + settings.DOMAIN, + }, + ) + + # + # Operation failed + # + if payload.get('errors'): + return {'status': 'success'} + + # + # NORMAL FLOW + # + else: + node.create_waterbutler_log( + auth=auth, + action=action, + payload=payload, + ) + + # + # Update storage metrics + # + metadata = ( + payload.get('metadata') + or payload.get('destination') + ) + + target_node = request_helpers.AbstractNode.load( + metadata.get('nid'), + ) + + if ( + target_node + and payload['action'] != 'download_file' + ): + request_helpers.update_storage_usage_with_size(payload) + + # + # Fire signals + # + with transaction.atomic(): + request_helpers.file_signals.file_updated.send( + target=node, + user=user, + event_type=action, + payload=payload, + ) + + return {'status': 'success'} diff --git a/notifications/file_event_notifications.py b/notifications/file_event_notifications.py index da2ab78431d..30e9c182c88 100644 --- a/notifications/file_event_notifications.py +++ b/notifications/file_event_notifications.py @@ -156,7 +156,7 @@ def url(self): if self._url is None: self._url = furl( self.node.absolute_url, - path=self.node.web_url_for('collect_file_trees').split('/') + path='localhost:5000/project//files/'.split('/') ) return self._url.url