-
Notifications
You must be signed in to change notification settings - Fork 336
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
[Need QA Prep] Feature/The Amicable Node-Preprint Divorce [PLAT-555][PLAT-480][PLAT-559][PLAT-561] #8422
[Need QA Prep] Feature/The Amicable Node-Preprint Divorce [PLAT-555][PLAT-480][PLAT-559][PLAT-561] #8422
Changes from 119 commits
eec5f53
b9af62b
4887176
6fa68dd
a3c8ab6
61cffe0
e710244
2b936ca
3765e3d
2a07f1f
49ea52f
d8bda8f
54ac459
592d480
d7abc9b
8acd0e8
64b6590
4d9434f
21ad0dd
5a44492
29b1ed2
5e8b821
44e0b7c
e3d926c
86f3622
b3544bc
e1df58a
5e052f1
64502e9
69c64c1
9260275
bdf8597
961378c
f274c4e
3af2b2f
2cdd3f9
750f64e
dad2424
f7efea8
62cc3c5
c7aac6d
7ba9757
2514170
5fc9320
75ec7c0
5e991c9
a7df58a
a2acafc
c1c9139
cce8387
41f1992
5bc0715
f7fbd07
03e494d
359bea7
683bebe
5dd0fcc
7d6500d
9d08390
a7564b2
299c6a6
0011ae1
cf523c9
477c4e0
4d745fa
0153f4a
5d376e2
335482a
73a12d7
bc42ab1
30b1cd7
21af518
aefdc8b
782cd09
2596109
1f52890
786efab
a631f84
9a4f380
487cc06
2cb6eb1
1c9bca8
8de7274
10f8ee9
b4dd2ba
1d5e556
7f3625c
559f330
74d1f43
b26a418
707338a
81df85a
dfd860f
7404167
0a0133a
6084402
f9130da
0b20771
3abb6d2
8e2f286
2401b34
55e4aa6
3b19ded
a80eb24
a271afb
c249c46
5c0ad7a
9ce1407
8fe6b06
ef37d25
6d057f2
9800776
b588d06
a07612a
4a05388
df7ce38
7fed2aa
ef620d7
99c8e17
5b9de71
e8334b9
9c75c2e
751e409
187544d
bf2a65b
3da67e3
be1730b
868e523
8dbcdf3
f273714
c5ae763
21a666f
6fb747b
ff4d533
ad917a3
981b0af
0c872a8
05f0403
8601f2c
77b5d51
44b3346
80eb0ba
7082dad
1fce58c
d84be3c
446ec83
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,7 @@ | |
import furl | ||
import jwe | ||
import jwt | ||
|
||
from django.db import transaction | ||
from django.contrib.contenttypes.models import ContentType | ||
|
||
|
@@ -34,7 +35,7 @@ | |
from addons.base import signals as file_signals | ||
from addons.base.utils import format_last_known_metadata | ||
from osf.models import (BaseFileNode, TrashedFileNode, | ||
OSFUser, AbstractNode, | ||
OSFUser, AbstractNode, Preprint, | ||
NodeLog, DraftRegistration, MetaSchema, | ||
Guid, FileVersionUserMetadata) | ||
from website.profile.utils import get_profile_image_url | ||
|
@@ -147,7 +148,6 @@ def get_addon_user_config(**kwargs): | |
'movefrom': 'write', | ||
} | ||
|
||
|
||
def check_access(node, auth, action, cas_resp): | ||
"""Verify that user can perform requested action on resource. Raise appropriate | ||
error code if action cannot proceed. | ||
|
@@ -157,12 +157,21 @@ def check_access(node, auth, action, cas_resp): | |
raise HTTPError(httplib.BAD_REQUEST) | ||
|
||
if cas_resp: | ||
if permission == 'read': | ||
if node.is_public: | ||
return True | ||
required_scope = oauth_scopes.CoreScopes.NODE_FILE_READ | ||
if isinstance(node, Preprint): | ||
if permission == 'read': | ||
if node.verified_publishable: | ||
return True | ||
required_scope = oauth_scopes.CoreScopes.PREPRINT_FILE_READ | ||
else: | ||
required_scope = oauth_scopes.CoreScopes.PREPRINT_FILE_WRITE | ||
else: | ||
required_scope = oauth_scopes.CoreScopes.NODE_FILE_WRITE | ||
if permission == 'read': | ||
if node.is_public: | ||
return True | ||
required_scope = oauth_scopes.CoreScopes.NODE_FILE_READ | ||
else: | ||
required_scope = oauth_scopes.CoreScopes.NODE_FILE_WRITE | ||
|
||
if not cas_resp.authenticated \ | ||
or required_scope not in oauth_scopes.normalize_scopes(cas_resp.attributes['accessTokenScope']): | ||
raise HTTPError(httplib.FORBIDDEN) | ||
|
@@ -172,8 +181,9 @@ def check_access(node, auth, action, cas_resp): | |
return True | ||
# The user may have admin privileges on a parent node, in which | ||
# case they should have read permissions | ||
if node.is_registration and node.registered_from.can_view(auth): | ||
return True | ||
if getattr(node, 'is_registration', False): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You shouldn't have to separate this into two lines, if |
||
if node.registered_from.can_view(auth): | ||
return True | ||
if permission == 'write' and node.can_edit(auth): | ||
return True | ||
|
||
|
@@ -188,33 +198,33 @@ def check_access(node, auth, action, cas_resp): | |
# `node.is_registration` == True. However, we have no way of telling if | ||
# `copyfrom` actions are originating from a node being registered. | ||
# TODO This is raise UNAUTHORIZED for registrations that have not been archived yet | ||
if action == 'copyfrom' or (action == 'copyto' and node.is_registration): | ||
parent = node.parent_node | ||
while parent: | ||
if parent.can_edit(auth): | ||
if isinstance(node, AbstractNode): | ||
if action == 'copyfrom' or (action == 'copyto' and node.is_registration): | ||
parent = node.parent_node | ||
while parent: | ||
if parent.can_edit(auth): | ||
return True | ||
parent = parent.parent_node | ||
|
||
# Users with the prereg admin permission should be allowed to download files | ||
# from prereg challenge draft registrations. | ||
try: | ||
prereg_schema = MetaSchema.objects.get(name='Prereg Challenge', schema_version=2) | ||
allowed_nodes = [node] + node.parents | ||
prereg_draft_registration = DraftRegistration.objects.filter( | ||
branched_from__in=allowed_nodes, | ||
registration_schema=prereg_schema | ||
) | ||
if action == 'download' and \ | ||
auth.user is not None and \ | ||
prereg_draft_registration.count() > 0 and \ | ||
auth.user.has_perm('osf.administer_prereg'): | ||
return True | ||
parent = parent.parent_node | ||
|
||
# Users with the prereg admin permission should be allowed to download files | ||
# from prereg challenge draft registrations. | ||
try: | ||
prereg_schema = MetaSchema.objects.get(name='Prereg Challenge', schema_version=2) | ||
allowed_nodes = [node] + node.parents | ||
prereg_draft_registration = DraftRegistration.objects.filter( | ||
branched_from__in=allowed_nodes, | ||
registration_schema=prereg_schema | ||
) | ||
if action == 'download' and \ | ||
auth.user is not None and \ | ||
prereg_draft_registration.count() > 0 and \ | ||
auth.user.has_perm('osf.administer_prereg'): | ||
return True | ||
except MetaSchema.DoesNotExist: | ||
pass | ||
except MetaSchema.DoesNotExist: | ||
pass | ||
|
||
raise HTTPError(httplib.FORBIDDEN if auth.user else httplib.UNAUTHORIZED) | ||
|
||
|
||
def make_auth(user): | ||
if user is not None: | ||
return { | ||
|
@@ -266,32 +276,37 @@ def get_auth(auth, **kwargs): | |
|
||
node = AbstractNode.load(node_id) | ||
if not node: | ||
raise HTTPError(httplib.NOT_FOUND) | ||
|
||
check_access(node, auth, action, cas_resp) | ||
|
||
provider_settings = node.get_addon(provider_name) | ||
if not provider_settings: | ||
raise HTTPError(httplib.BAD_REQUEST) | ||
preprint = Preprint.load(node_id) | ||
if not preprint: | ||
raise HTTPError(httplib.NOT_FOUND) | ||
|
||
if node: | ||
check_access(node, auth, action, cas_resp) | ||
provider_settings = node.get_addon(provider_name) | ||
if not provider_settings: | ||
raise HTTPError(httplib.BAD_REQUEST) | ||
else: | ||
check_access(preprint, auth, action, cas_resp) | ||
|
||
try: | ||
credentials = provider_settings.serialize_waterbutler_credentials() | ||
waterbutler_settings = provider_settings.serialize_waterbutler_settings() | ||
credentials = provider_settings.serialize_waterbutler_credentials() if node else preprint.serialize_waterbutler_credentials() | ||
waterbutler_settings = provider_settings.serialize_waterbutler_settings() if node else preprint.serialize_waterbutler_settings() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Preprints don't have addons, so a few things you might find on OSFStorage NodeSettings, for example, are located on the preprint itself. The Preprint model has a serialize_waterbutler_settings() and serialize_waterbutler_credentials() method. |
||
except exceptions.AddonError: | ||
log_exception() | ||
raise HTTPError(httplib.BAD_REQUEST) | ||
|
||
node = node or preprint | ||
return {'payload': jwe.encrypt(jwt.encode({ | ||
'exp': timezone.now() + datetime.timedelta(seconds=settings.WATERBUTLER_JWT_EXPIRATION), | ||
'data': { | ||
'auth': make_auth(auth.user), # A waterbutler auth dict not an Auth object | ||
'credentials': credentials, | ||
'settings': waterbutler_settings, | ||
'callback_url': node.api_url_for( | ||
('create_waterbutler_log' if not node.is_registration else 'registration_callbacks'), | ||
('create_waterbutler_log' if not getattr(node, 'is_registration', False) else 'registration_callbacks'), | ||
_absolute=True, | ||
_internal=True | ||
), | ||
) | ||
} | ||
}, settings.WATERBUTLER_JWT_SECRET, algorithm=settings.WATERBUTLER_JWT_ALGORITHM), WATERBUTLER_JWE_KEY)} | ||
|
||
|
@@ -329,14 +344,16 @@ def mark_file_version_as_seen(user, path, version): | |
|
||
@must_be_signed | ||
@no_auto_transaction | ||
@must_be_valid_project(quickfiles_valid=True) | ||
@must_be_valid_project(quickfiles_valid=True, preprints_valid=True) | ||
def create_waterbutler_log(payload, **kwargs): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Modified to create waterbutler-related logs like copying/uploading files for either a node or a preprint. Source/destination can be either a node/preprint, and nodes have addons and preprints do not, so most of this logic relates to that. |
||
with transaction.atomic(): | ||
try: | ||
auth = payload['auth'] | ||
# Don't log download actions, but do update analytics | ||
if payload['action'] in DOWNLOAD_ACTIONS: | ||
node = AbstractNode.load(payload['metadata']['nid']) | ||
if not node: | ||
node = Preprint.load(payload['metadata'].get('nid')) | ||
return {'status': 'success'} | ||
|
||
user = OSFUser.load(auth['id']) | ||
|
@@ -348,7 +365,7 @@ def create_waterbutler_log(payload, **kwargs): | |
raise HTTPError(httplib.BAD_REQUEST) | ||
|
||
auth = Auth(user=user) | ||
node = kwargs['node'] or kwargs['project'] | ||
node = kwargs.get('node') or kwargs.get('project') or Preprint.load(kwargs.get('nid')) or Preprint.load(kwargs.get('pid')) | ||
|
||
if action in (NodeLog.FILE_MOVED, NodeLog.FILE_COPIED): | ||
|
||
|
@@ -376,6 +393,8 @@ def create_waterbutler_log(payload, **kwargs): | |
|
||
destination_node = node # For clarity | ||
source_node = AbstractNode.load(payload['source']['nid']) | ||
if not source_node: | ||
source_node = Preprint.load(payload['metadata'].get('nid')) | ||
|
||
source = source_node.get_addon(payload['source']['provider']) | ||
destination = node.get_addon(payload['destination']['provider']) | ||
|
@@ -445,7 +464,7 @@ def create_waterbutler_log(payload, **kwargs): | |
else: | ||
try: | ||
metadata = payload['metadata'] | ||
node_addon = node.get_addon(payload['provider']) | ||
node_addon = node if isinstance(node, Preprint) else node.get_addon(payload['provider']) | ||
except KeyError: | ||
raise HTTPError(httplib.BAD_REQUEST) | ||
|
||
|
@@ -457,7 +476,9 @@ def create_waterbutler_log(payload, **kwargs): | |
node_addon.create_waterbutler_log(auth, action, metadata) | ||
|
||
with transaction.atomic(): | ||
file_signals.file_updated.send(target=node, user=user, event_type=action, payload=payload) | ||
if not isinstance(node, Preprint): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: The if block should be around the transaction, no reason to open it if nothings happening in it. |
||
# Preprints not getting emails about file updates for now | ||
file_signals.file_updated.send(target=node, user=user, event_type=action, payload=payload) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Verified with product, we will not be sending emails to preprint contributors when a file is updated. This was functionality that we got for free with the node-preprint link. Product also says 2.6% of preprint authors update their preprint file version. |
||
|
||
return {'status': 'success'} | ||
|
||
|
@@ -640,26 +661,27 @@ def addon_view_or_download_file(auth, path, provider, **kwargs): | |
if not path: | ||
raise HTTPError(httplib.BAD_REQUEST) | ||
|
||
node_addon = target.get_addon(provider) | ||
if isinstance(target, AbstractNode): | ||
node_addon = target.get_addon(provider) | ||
|
||
if not isinstance(node_addon, BaseStorageAddon): | ||
object_text = markupsafe.escape(getattr(target, 'project_or_component', 'this object')) | ||
raise HTTPError(httplib.BAD_REQUEST, data={ | ||
'message_short': 'Bad Request', | ||
'message_long': 'The {} add-on containing {} is no longer connected to {}.'.format(provider_safe, path_safe, object_text) | ||
}) | ||
if not isinstance(node_addon, BaseStorageAddon): | ||
object_text = markupsafe.escape(getattr(target, 'project_or_component', 'this object')) | ||
raise HTTPError(httplib.BAD_REQUEST, data={ | ||
'message_short': 'Bad Request', | ||
'message_long': 'The {} add-on containing {} is no longer connected to {}.'.format(provider_safe, path_safe, object_text) | ||
}) | ||
|
||
if not node_addon.has_auth: | ||
raise HTTPError(httplib.UNAUTHORIZED, data={ | ||
'message_short': 'Unauthorized', | ||
'message_long': 'The {} add-on containing {} is no longer authorized.'.format(provider_safe, path_safe) | ||
}) | ||
if not node_addon.has_auth: | ||
raise HTTPError(httplib.UNAUTHORIZED, data={ | ||
'message_short': 'Unauthorized', | ||
'message_long': 'The {} add-on containing {} is no longer authorized.'.format(provider_safe, path_safe) | ||
}) | ||
|
||
if not node_addon.complete: | ||
raise HTTPError(httplib.BAD_REQUEST, data={ | ||
'message_short': 'Bad Request', | ||
'message_long': 'The {} add-on containing {} is no longer configured.'.format(provider_safe, path_safe) | ||
}) | ||
if not node_addon.complete: | ||
raise HTTPError(httplib.BAD_REQUEST, data={ | ||
'message_short': 'Bad Request', | ||
'message_long': 'The {} add-on containing {} is no longer configured.'.format(provider_safe, path_safe) | ||
}) | ||
|
||
savepoint_id = transaction.savepoint() | ||
file_node = BaseFileNode.resolve_class(provider, BaseFileNode.FILE).get_or_create(target, path) | ||
|
@@ -720,6 +742,13 @@ def addon_view_or_download_file(auth, path, provider, **kwargs): | |
if len(request.path.strip('/').split('/')) > 1: | ||
guid = file_node.get_guid(create=True) | ||
return redirect(furl.furl('/{}/'.format(guid._id)).set(args=extras).url) | ||
if isinstance(target, Preprint): | ||
# Cannot currently view preprint files in file view | ||
raise HTTPError(httplib.NOT_FOUND, data={ | ||
'message_short': 'File Not Found', | ||
'message_long': 'The requested file could not be found.' | ||
}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Verified with Steve, we are not going to be showing preprint-files in the file-detail view. |
||
|
||
return addon_view_file(auth, target, file_node, version) | ||
|
||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
from django.db import models | ||
from django.dispatch import receiver | ||
from django.db.models.signals import post_save | ||
|
||
from addons.osfstorage.models import OsfStorageFolder | ||
|
||
|
||
class UploadMixin(models.Model): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Courtesy of @erinspace, borrowed this from one of her old commits. Preprints don't have addons, but this UploadMixin adds a root_folder to the preprint on first save. Your primary file can go in this root folder (primary_file parent is root folder). |
||
root_folder = models.ForeignKey(OsfStorageFolder, null=True, blank=True, related_name='%(class)s_object') | ||
|
||
class Meta: | ||
abstract = True | ||
|
||
|
||
@receiver(post_save, sender='osf.Preprint') | ||
def create_file_node(sender, instance, **kwargs): | ||
if instance.root_folder: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Prefer |
||
return | ||
|
||
# Note: The "root" node will always be "named" empty string | ||
root_folder = OsfStorageFolder(name='', target=instance, is_root=True) | ||
root_folder.save() | ||
|
||
instance.__class__.objects.filter(id=instance.id).update(root_folder=root_folder) | ||
instance.refresh_from_db() |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -152,7 +152,7 @@ def _check_delete_allowed(self): | |
|
||
@property | ||
def is_preprint_primary(self): | ||
return getattr(self.target, 'preprint_file', None) == self and not getattr(self.target, '_has_abandoned_preprint', None) | ||
return getattr(self.target, 'primary_file', None) == self | ||
|
||
def delete(self, user=None, parent=None, **kwargs): | ||
self._path = self.path | ||
|
@@ -425,9 +425,9 @@ def is_checked_out(self): | |
|
||
@property | ||
def is_preprint_primary(self): | ||
if hasattr(self.target, 'preprint_file') and self.target.preprint_file: | ||
if hasattr(self.target, 'primary_file') and self.target.primary_file: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
for child in self.children.all().prefetch_related('target'): | ||
if getattr(child.target, 'preprint_file', None): | ||
if getattr(child.target, 'primary_file', None): | ||
if child.is_preprint_primary: | ||
return True | ||
return False | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
check_access
modified to check if someone can access a file on either a node or a preprintNew: non-contributors prevented from accessing a withdrawn preprint file