def test_http_error_raised(self): """When an HTTPError is raised in the view function, it is passed as input to the renderer. ``WebRenderer`` should return a 2-tuple, where the first element is the rendered error template. Each HTTPError exposes a ``to_data()`` method, which yields the appropriate error message text. """ self.app.app.preprocess_request() err = HTTPError(http.NOT_FOUND) resp = self.r(err) self.assertIn(err.to_data()["message_short"], resp[0]) self.assertEqual(http.NOT_FOUND, resp[1])
def verify_user_match(auth, **kwargs): uid = kwargs.get('uid') if uid and uid != auth.user._id: raise HTTPError(http.FORBIDDEN)
def get_auth(auth, **kwargs): cas_resp = None if not auth.user: # Central Authentication Server OAuth Bearer Token authorization = request.headers.get('Authorization') if authorization and authorization.startswith('Bearer '): client = cas.get_client() try: access_token = cas.parse_auth_header(authorization) cas_resp = client.profile(access_token) except cas.CasError as err: sentry.log_exception() # NOTE: We assume that the request is an AJAX request return json_renderer(err) if cas_resp.authenticated: auth.user = User.load(cas_resp.user) try: data = jwt.decode( jwe.decrypt(request.args.get('payload', '').encode('utf-8'), WATERBUTLER_JWE_KEY), settings.WATERBUTLER_JWT_SECRET, options={'require_exp': True}, algorithm=settings.WATERBUTLER_JWT_ALGORITHM )['data'] except (jwt.InvalidTokenError, KeyError): raise HTTPError(httplib.FORBIDDEN) if not auth.user: auth.user = User.from_cookie(data.get('cookie', '')) try: action = data['action'] node_id = data['nid'] provider_name = data['provider'] except KeyError: raise HTTPError(httplib.BAD_REQUEST) node = Node.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) try: credentials = provider_settings.serialize_waterbutler_credentials() waterbutler_settings = provider_settings.serialize_waterbutler_settings() except exceptions.AddonError: log_exception() raise HTTPError(httplib.BAD_REQUEST) return {'payload': jwe.encrypt(jwt.encode({ 'exp': datetime.datetime.utcnow() + 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'), _absolute=True, ), } }, settings.WATERBUTLER_JWT_SECRET, algorithm=settings.WATERBUTLER_JWT_ALGORITHM), WATERBUTLER_JWE_KEY)}
def claim_user_form(auth, **kwargs): """ View for rendering the set password page for a claimed user. Must have ``token`` as a querystring argument. Renders the set password form, validates it, and sets the user's password. HTTP Method: GET, POST """ uid, pid = kwargs['uid'], kwargs['pid'] token = request.form.get('token') or request.args.get('token') user = OSFUser.load(uid) # If unregistered user is not in database, or url bears an invalid token raise HTTP 400 error if not user or not verify_claim_token(user, token, pid): error_data = { 'message_short': 'Invalid url.', 'message_long': 'Claim user does not exists, the token in the URL is invalid or has expired.' } raise HTTPError(http_status.HTTP_400_BAD_REQUEST, data=error_data) # If user is logged in, redirect to 're-enter password' page if auth.logged_in: return redirect( web_url_for('claim_user_registered', uid=uid, pid=pid, token=token)) unclaimed_record = user.unclaimed_records[pid] user.fullname = unclaimed_record['name'] user.update_guessed_names() # The email can be the original referrer email if no claimer email has been specified. claimer_email = unclaimed_record.get( 'claimer_email') or unclaimed_record.get('email') # If there is a registered user with this email, redirect to 're-enter password' page try: user_from_email = OSFUser.objects.get( emails__address=claimer_email.lower().strip( )) if claimer_email else None except OSFUser.DoesNotExist: user_from_email = None if user_from_email and user_from_email.is_registered: return redirect( web_url_for('claim_user_registered', uid=uid, pid=pid, token=token)) form = SetEmailAndPasswordForm(request.form, token=token) if request.method == 'POST': if not form.validate(): forms.push_errors_to_status(form.errors) elif settings.RECAPTCHA_SITE_KEY and not validate_recaptcha( request.form.get('g-recaptcha-response'), remote_ip=request.remote_addr): status.push_status_message('Invalid captcha supplied.', kind='error') else: username, password = claimer_email, form.password.data if not username: raise HTTPError( http_status.HTTP_400_BAD_REQUEST, data=dict( message_long= 'No email associated with this account. Please claim this ' 'account on the project to which you were invited.')) user.register( username=username, password=password, accepted_terms_of_service=form.accepted_terms_of_service.data) # Clear unclaimed records user.unclaimed_records = {} user.verification_key = generate_verification_key() user.save() # Authenticate user and redirect to project page status.push_status_message(language.CLAIMED_CONTRIBUTOR, kind='success', trust=True) # Redirect to CAS and authenticate the user with a verification key. provider = PreprintProvider.load(pid) redirect_url = None if provider: redirect_url = web_url_for('auth_login', next=provider.landing_url, _absolute=True) else: # Add related claimed tags to user _add_related_claimed_tag_to_user(pid, user) redirect_url = web_url_for('resolve_guid', guid=pid, _absolute=True) return redirect( cas.get_login_url(redirect_url, username=user.username, verification_key=user.verification_key)) return { 'firstname': user.given_name, 'email': claimer_email if claimer_email else '', 'fullname': user.fullname, 'form': forms.utils.jsonify(form) if is_json_request() else form, 'osf_contact_email': settings.OSF_CONTACT_EMAIL, }
def send_claim_email(email, unclaimed_user, node, notify=True, throttle=24 * 3600, email_template='default'): """ Unregistered user claiming a user account as an contributor to a project. Send an email for claiming the account. Either sends to the given email or the referrer's email, depending on the email address provided. :param str email: The address given in the claim user form :param User unclaimed_user: The User record to claim. :param Node node: The node where the user claimed their account. :param bool notify: If True and an email is sent to the referrer, an email will also be sent to the invited user about their pending verification. :param int throttle: Time period (in seconds) after the referrer is emailed during which the referrer will not be emailed again. :param str email_template: the email template to use :return :raise http_status.HTTP_400_BAD_REQUEST """ claimer_email = email.lower().strip() unclaimed_record = unclaimed_user.get_unclaimed_record(node._primary_key) referrer = OSFUser.load(unclaimed_record['referrer_id']) claim_url = unclaimed_user.get_claim_url(node._primary_key, external=True) # Option 1: # When adding the contributor, the referrer provides both name and email. # The given email is the same provided by user, just send to that email. logo = None if unclaimed_record.get('email') == claimer_email: # check email template for branded preprints if email_template == 'preprint': if node.provider.is_default: mail_tpl = mails.INVITE_OSF_PREPRINT logo = settings.OSF_PREPRINTS_LOGO else: mail_tpl = mails.INVITE_PREPRINT(node.provider) logo = node.provider._id else: mail_tpl = mails.INVITE_DEFAULT to_addr = claimer_email unclaimed_record['claimer_email'] = claimer_email unclaimed_user.save() # Option 2: # TODO: [new improvement ticket] this option is disabled from preprint but still available on the project page # When adding the contributor, the referred only provides the name. # The account is later claimed by some one who provides the email. # Send email to the referrer and ask her/him to forward the email to the user. else: # check throttle timestamp = unclaimed_record.get('last_sent') if not throttle_period_expired(timestamp, throttle): raise HTTPError( http_status.HTTP_400_BAD_REQUEST, data=dict( message_long= 'User account can only be claimed with an existing user once every 24 hours' )) # roll the valid token for each email, thus user cannot change email and approve a different email address verification_key = generate_verification_key(verification_type='claim') unclaimed_record['last_sent'] = get_timestamp() unclaimed_record['token'] = verification_key['token'] unclaimed_record['expires'] = verification_key['expires'] unclaimed_record['claimer_email'] = claimer_email unclaimed_user.save() claim_url = unclaimed_user.get_claim_url(node._primary_key, external=True) # send an email to the invited user without `claim_url` if notify: pending_mail = mails.PENDING_VERIFICATION mails.send_mail( claimer_email, pending_mail, user=unclaimed_user, referrer=referrer, fullname=unclaimed_record['name'], node=node, can_change_preferences=False, osf_contact_email=settings.OSF_CONTACT_EMAIL, ) mail_tpl = mails.FORWARD_INVITE to_addr = referrer.username # Send an email to the claimer (Option 1) or to the referrer (Option 2) with `claim_url` mails.send_mail( to_addr, mail_tpl, user=unclaimed_user, referrer=referrer, node=node, claim_url=claim_url, email=claimer_email, fullname=unclaimed_record['name'], branded_service=node.provider, can_change_preferences=False, logo=logo if logo else settings.OSF_LOGO, osf_contact_email=settings.OSF_CONTACT_EMAIL, ) return to_addr
def publish_dataverse(dataverse): try: dataverse.publish() except OperationFailedError: raise HTTPError(http.BAD_REQUEST)
def get_folders(self, folder_id=None, **kwargs): """Get list of folders underneath the folder with id ``folder_id``. If ``folder_id`` is ``None``, return a single entry representing the root folder. In OneDrive, the root folder has a unique id, so fetch that and return it. This method returns a list of dicts with metadata about each folder under ``folder_id``. These dicts have the following properties:: { 'addon': 'onedrive', # short name of the addon 'id': folder_id, # id of the folder. root may need special casing 'path': '/', # human-readable path of the folder 'kind': 'folder', # always 'folder' 'name': '/ (Full OneDrive)', # human readable name of the folder. root may need special casing 'urls': { # urls to fetch information about the folder 'folders': api_v2_url( # url to get subfolders of this folder. 'nodes/{}/addons/onedrive/folders/'.format(self.owner._id), params={'id': folder_id} ), } } Some providers include additional information:: * figshare includes ``permissions``, ``hasChildren`` * googledrive includes ``urls.fetch`` :param str folder_id: the id of the folder to fetch subfolders of. Defaults to ``None`` :rtype: list :return: a list of dicts with metadata about the subfolder of ``folder_id``. """ if folder_id is None: return [{ 'id': DEFAULT_ROOT_ID, 'path': '/', 'addon': 'onedrive', 'kind': 'folder', 'name': '/ (Full OneDrive)', 'urls': { 'folders': api_v2_url('nodes/{}/addons/onedrive/folders/'.format(self.owner._id), params={'id': DEFAULT_ROOT_ID}), } }] try: access_token = self.fetch_access_token() except exceptions.InvalidAuthError: raise HTTPError(403) client = OneDriveClient(access_token) items = client.folders(self.drive_id, folder_id) return [ { 'addon': 'onedrive', 'kind': 'folder', 'id': item['id'], 'name': item['name'], 'path': item['name'], 'urls': { 'folders': api_v2_url('nodes/{}/addons/onedrive/folders/'.format(self.owner._id), params={'id': item['id']}), } } for item in items ]
def external_login_email_post(): """ View to handle email submission for first-time oauth-login user. HTTP Method: POST """ form = ResendConfirmationForm(request.form) session = get_session() if not session.is_external_first_login: raise HTTPError(http.UNAUTHORIZED) external_id_provider = session.data['auth_user_external_id_provider'] external_id = session.data['auth_user_external_id'] fullname = session.data['auth_user_fullname'] service_url = session.data['service_url'] destination = 'dashboard' for campaign in campaigns.get_campaigns(): if campaign != 'institution': # Handle different url encoding schemes between `furl` and `urlparse/urllib`. # OSF use `furl` to parse service url during service validation with CAS. However, `web_url_for()` uses # `urlparse/urllib` to generate service url. `furl` handles `urlparser/urllib` generated urls while ` but # not vice versa. campaign_url = furl.furl(campaigns.campaign_url_for(campaign)).url if campaigns.is_proxy_login(campaign): # proxy campaigns: OSF Preprints and branded ones if check_service_url_with_proxy_campaign( service_url, campaign_url): destination = campaign # continue to check branded preprints even service url matches osf preprints if campaign != 'osf-preprints': break elif service_url.startswith(campaign_url): # osf campaigns: OSF Prereg and ERPC destination = campaign break if form.validate(): clean_email = form.email.data user = get_user(email=clean_email) external_identity = { external_id_provider: { external_id: None, }, } try: ensure_external_identity_uniqueness(external_id_provider, external_id, user) except ValidationError as e: raise HTTPError(http.FORBIDDEN, e.message) if user: # 1. update user oauth, with pending status external_identity[external_id_provider][external_id] = 'LINK' if external_id_provider in user.external_identity: user.external_identity[external_id_provider].update( external_identity[external_id_provider]) else: user.external_identity.update(external_identity) # 2. add unconfirmed email and send confirmation email user.add_unconfirmed_email(clean_email, external_identity=external_identity) user.save() send_confirm_email(user, clean_email, external_id_provider=external_id_provider, external_id=external_id, destination=destination) # 3. notify user message = language.EXTERNAL_LOGIN_EMAIL_LINK_SUCCESS.format( external_id_provider=external_id_provider, email=user.username) kind = 'success' # 4. remove session and osf cookie remove_session(session) else: # 1. create unconfirmed user with pending status external_identity[external_id_provider][external_id] = 'CREATE' user = User.create_unconfirmed(username=clean_email, password=str(uuid.uuid4()), fullname=fullname, external_identity=external_identity, campaign=None) # TODO: [#OSF-6934] update social fields, verified social fields cannot be modified user.save() # 3. send confirmation email send_confirm_email(user, user.username, external_id_provider=external_id_provider, external_id=external_id, destination=destination) # 4. notify user message = language.EXTERNAL_LOGIN_EMAIL_CREATE_SUCCESS.format( external_id_provider=external_id_provider, email=user.username) kind = 'success' # 5. remove session remove_session(session) status.push_status_message(message, kind=kind, trust=False) else: forms.push_errors_to_status(form.errors) # Don't go anywhere return {'form': form, 'external_id_provider': external_id_provider}
def addon_view_or_download_file(auth, path, provider, **kwargs): extras = request.args.to_dict() extras.pop('_', None) # Clean up our url params a bit action = extras.get('action', 'view') node = kwargs.get('node') or kwargs['project'] node_addon = node.get_addon(provider) provider_safe = markupsafe.escape(provider) path_safe = markupsafe.escape(path) project_safe = markupsafe.escape(node.project_or_component) if not path: raise HTTPError(httplib.BAD_REQUEST) if not isinstance(node_addon, StorageAddonBase): 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, project_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) }) file_node = FileNode.resolve_class(provider, FileNode.FILE).get_or_create( node, path) # Note: Cookie is provided for authentication to waterbutler # it is overriden to force authentication as the current user # the auth header is also pass to support basic auth version = file_node.touch( request.headers.get('Authorization'), **dict(extras, cookie=request.cookies.get(settings.COOKIE_NAME))) if version is None: return addon_deleted_file(file_node=file_node, path=path, **kwargs) # TODO clean up these urls and unify what is used as a version identifier if request.method == 'HEAD': return make_response(('', 200, { 'Location': file_node.generate_waterbutler_url( **dict(extras, direct=None, version=version.identifier)) })) if action == 'download': return redirect( file_node.generate_waterbutler_url( **dict(extras, direct=None, version=version.identifier))) if action == 'get_guid': draft_id = extras.get('draft') draft = DraftRegistration.load(draft_id) if draft is None or draft.is_approved: raise HTTPError(httplib.BAD_REQUEST, data={ 'message_short': 'Bad Request', 'message_long': 'File not associated with required object.' }) guid = file_node.get_guid(create=True) guid.referent.save() return dict(guid=guid._id) 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) return addon_view_file(auth, node, file_node, version)
def addon_view_or_download_file(auth, path, provider, **kwargs): extras = request.args.to_dict() extras.pop('_', None) # Clean up our url params a bit action = extras.get('action', 'view') guid = kwargs.get('guid') guid_target = getattr(Guid.load(guid), 'referent', None) target = guid_target or kwargs.get('node') or kwargs['project'] provider_safe = markupsafe.escape(provider) path_safe = markupsafe.escape(path) if not path: raise HTTPError(http_status.HTTP_400_BAD_REQUEST) if hasattr(target, 'get_addon'): 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( http_status.HTTP_400_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( http_status.HTTP_401_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( http_status.HTTP_400_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) # Note: Cookie is provided for authentication to waterbutler # it is overriden to force authentication as the current user # the auth header is also pass to support basic auth version = file_node.touch( request.headers.get('Authorization'), **dict(extras, cookie=request.cookies.get(settings.COOKIE_NAME))) if version is None: # File is either deleted or unable to be found in the provider location # Rollback the insertion of the file_node transaction.savepoint_rollback(savepoint_id) if not file_node.pk: file_node = BaseFileNode.load(path) if not file_node: raise HTTPError(http_status.HTTP_404_NOT_FOUND, data={ 'message_short': 'File Not Found', 'message_long': 'The requested file could not be found.' }) if file_node.kind == 'folder': raise HTTPError( http_status.HTTP_400_BAD_REQUEST, data={ 'message_short': 'Bad Request', 'message_long': 'You cannot request a folder from this endpoint.' }) # Allow osfstorage to redirect if the deep url can be used to find a valid file_node if file_node.provider == 'osfstorage' and not file_node.is_deleted: return redirect( file_node.target.web_url_for('addon_view_or_download_file', path=file_node._id, provider=file_node.provider)) return addon_deleted_file(target=target, file_node=file_node, path=path, **kwargs) else: transaction.savepoint_commit(savepoint_id) # TODO clean up these urls and unify what is used as a version identifier if request.method == 'HEAD': return make_response(('', http_status.HTTP_302_FOUND, { 'Location': file_node.generate_waterbutler_url( **dict(extras, direct=None, version=version.identifier, _internal=extras.get('mode') == 'render')) })) if action == 'download': format = extras.get('format') _, extension = os.path.splitext(file_node.name) # avoid rendering files with the same format type. if format and '.{}'.format(format.lower()) != extension.lower(): return redirect('{}/export?format={}&url={}'.format( get_mfr_url(target, provider), format, quote( file_node.generate_waterbutler_url( **dict(extras, direct=None, version=version.identifier, _internal=extras.get('mode') == 'render'))))) return redirect( file_node.generate_waterbutler_url( **dict(extras, direct=None, version=version.identifier, _internal=extras.get('mode') == 'render'))) if action == 'get_guid': draft_id = extras.get('draft') draft = DraftRegistration.load(draft_id) if draft is None or draft.is_approved: raise HTTPError(http_status.HTTP_400_BAD_REQUEST, data={ 'message_short': 'Bad Request', 'message_long': 'File not associated with required object.' }) guid = file_node.get_guid(create=True) guid.referent.save() return dict(guid=guid._id) 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): # Redirecting preprint file guids to the preprint detail page return redirect('/{}/'.format(target._id)) return addon_view_file(auth, target, file_node, version)
def create_waterbutler_log(payload, **kwargs): with transaction.atomic(): try: auth = payload['auth'] # Don't log download actions if payload['action'] in DOWNLOAD_ACTIONS: guid = Guid.load(payload['metadata'].get('nid')) if guid: node = guid.referent return {'status': 'success'} user = OSFUser.load(auth['id']) if user is None: raise HTTPError(http_status.HTTP_400_BAD_REQUEST) action = LOG_ACTION_MAP[payload['action']] except KeyError: raise HTTPError(http_status.HTTP_400_BAD_REQUEST) auth = Auth(user=user) 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): for bundle in ('source', 'destination'): for key in ('provider', 'materialized', 'name', 'nid'): if key not in payload[bundle]: raise HTTPError(http_status.HTTP_400_BAD_REQUEST) dest = payload['destination'] src = payload['source'] 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 = LOG_ACTION_MAP['rename'] destination_node = node # For clarity source_node = AbstractNode.load(src['nid']) or Preprint.load( src['nid']) # We return provider fullname so we need to load node settings, if applicable 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']) 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, } }) 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, } }) if not payload.get('errors'): destination_node.add_log(action=action, auth=auth, params=payload) if payload.get('email') is True or payload.get('errors'): mails.send_mail( user.username, mails.FILE_OPERATION_FAILED if payload.get('errors') else mails.FILE_OPERATION_SUCCESS, action=payload['action'], source_node=source_node, destination_node=destination_node, source_path=payload['source']['materialized'], source_addon=payload['source']['addon'], destination_addon=payload['destination']['addon'], osf_support_email=settings.OSF_SUPPORT_EMAIL) if payload.get('errors'): # Action failed but our function succeeded # Bail out to avoid file_signals return {'status': 'success'} else: node.create_waterbutler_log(auth, action, payload) with transaction.atomic(): file_signals.file_updated.send(target=node, user=user, event_type=action, payload=payload) return {'status': 'success'}
def get_auth(auth, **kwargs): cas_resp = None if not auth.user: # Central Authentication Server OAuth Bearer Token authorization = request.headers.get('Authorization') if authorization and authorization.startswith('Bearer '): client = cas.get_client() try: access_token = cas.parse_auth_header(authorization) cas_resp = client.profile(access_token) except cas.CasError as err: sentry.log_exception() # NOTE: We assume that the request is an AJAX request return json_renderer(err) if cas_resp.authenticated: auth.user = OSFUser.load(cas_resp.user) try: data = jwt.decode(jwe.decrypt( request.args.get('payload', '').encode('utf-8'), WATERBUTLER_JWE_KEY), settings.WATERBUTLER_JWT_SECRET, options={'require_exp': True}, algorithm=settings.WATERBUTLER_JWT_ALGORITHM)['data'] except (jwt.InvalidTokenError, KeyError) as err: sentry.log_message(str(err)) raise HTTPError(http_status.HTTP_403_FORBIDDEN) if not auth.user: auth.user = OSFUser.from_cookie(data.get('cookie', '')) try: action = data['action'] node_id = data['nid'] provider_name = data['provider'] except KeyError: raise HTTPError(http_status.HTTP_400_BAD_REQUEST) node = AbstractNode.load(node_id) or Preprint.load(node_id) if node and node.is_deleted: raise HTTPError(http_status.HTTP_410_GONE) elif not node: raise HTTPError(http_status.HTTP_404_NOT_FOUND) check_access(node, auth, action, cas_resp) provider_settings = None if hasattr(node, 'get_addon'): provider_settings = node.get_addon(provider_name) if not provider_settings: raise HTTPError(http_status.HTTP_400_BAD_REQUEST) path = data.get('path') credentials = None waterbutler_settings = None fileversion = None if provider_name == 'osfstorage': if path: file_id = path.strip('/') # check to see if this is a file or a folder filenode = OsfStorageFileNode.load(path.strip('/')) if filenode and filenode.is_file: # default to most recent version if none is provided in the response version = int(data['version']) if data.get( 'version') else filenode.versions.count() try: fileversion = FileVersion.objects.filter( basefilenode___id=file_id, identifier=version).select_related('region').get() except FileVersion.DoesNotExist: raise HTTPError(http_status.HTTP_400_BAD_REQUEST) if auth.user: # mark fileversion as seen FileVersionUserMetadata.objects.get_or_create( user=auth.user, file_version=fileversion) if not node.is_contributor_or_group_member(auth.user): from_mfr = download_is_from_mfr(request, payload=data) # version index is 0 based version_index = version - 1 if action == 'render': update_analytics(node, filenode, version_index, 'view') elif action == 'download' and not from_mfr: update_analytics(node, filenode, version_index, 'download') if waffle.switch_is_active(features.ELASTICSEARCH_METRICS): if isinstance(node, Preprint): metric_class = get_metric_class_for_action( action, from_mfr=from_mfr) if metric_class: try: metric_class.record_for_preprint( preprint=node, user=auth.user, version=fileversion.identifier if fileversion else None, path=path) except es_exceptions.ConnectionError: log_exception() if fileversion and provider_settings: region = fileversion.region credentials = region.waterbutler_credentials waterbutler_settings = fileversion.serialize_waterbutler_settings( node_id=provider_settings.owner._id, root_id=provider_settings.root_node._id, ) # If they haven't been set by version region, use the NodeSettings or Preprint directly if not (credentials and waterbutler_settings): credentials = node.serialize_waterbutler_credentials(provider_name) waterbutler_settings = node.serialize_waterbutler_settings( provider_name) 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 getattr(node, 'is_registration', False) else 'registration_callbacks'), _absolute=True, _internal=True) } }, settings.WATERBUTLER_JWT_SECRET, algorithm=settings.WATERBUTLER_JWT_ALGORITHM), WATERBUTLER_JWE_KEY) }
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. """ permission = permission_map.get(action, None) if permission is None: raise HTTPError(http_status.HTTP_400_BAD_REQUEST) if cas_resp: if permission == permissions.READ: if node.can_view_files(auth=None): return True required_scope = node.file_read_scope else: required_scope = node.file_write_scope if not cas_resp.authenticated \ or required_scope not in oauth_scopes.normalize_scopes(cas_resp.attributes['accessTokenScope']): raise HTTPError(http_status.HTTP_403_FORBIDDEN) if permission == permissions.READ: if node.can_view_files(auth): return True # The user may have admin privileges on a parent node, in which # case they should have read permissions if getattr(node, 'is_registration', False) and node.registered_from.can_view(auth): return True if permission == permissions.WRITE and node.can_edit(auth): return True # Users attempting to register projects with components might not have # `write` permissions for all components. This will result in a 403 for # all `upload` actions as well as `copyfrom` actions if the component # in question is not public. To get around this, we have to recursively # check the node's parent node to determine if they have `write` # permissions up the stack. # TODO(hrybacki): is there a way to tell if this is for a registration? # All nodes being registered that receive the `upload` action will have # `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 isinstance(node, AbstractNode): if action == 'copyfrom' or (action == 'upload' 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 = RegistrationSchema.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 RegistrationSchema.DoesNotExist: pass raise HTTPError(http_status.HTTP_403_FORBIDDEN if auth. user else http_status.HTTP_401_UNAUTHORIZED)
def addon_view_or_download_file(auth, path, provider, **kwargs): extras = request.args.to_dict() extras.pop('_', None) # Clean up our url params a bit action = extras.get('action', 'view') node = kwargs.get('node') or kwargs['project'] node_addon = node.get_addon(provider) if not path: raise HTTPError(httplib.BAD_REQUEST) if not isinstance(node_addon, StorageAddonBase): raise HTTPError( httplib.BAD_REQUEST, { 'message_short': 'Bad Request', 'message_long': 'The add-on containing this file is no longer connected to the {}.' .format(node.project_or_component) }) if not node_addon.has_auth: raise HTTPError( httplib.UNAUTHORIZED, { 'message_short': 'Unauthorized', 'message_long': 'The add-on containing this file is no longer authorized.' }) if not node_addon.complete: raise HTTPError( httplib.BAD_REQUEST, { 'message_short': 'Bad Request', 'message_long': 'The add-on containing this file is no longer configured.' }) file_node = FileNode.resolve_class(provider, FileNode.FILE).get_or_create( node, path) # Note: Cookie is provided for authentication to waterbutler # it is overriden to force authentication as the current user # the auth header is also pass to support basic auth version = file_node.touch( request.headers.get('Authorization'), **dict(extras, cookie=request.cookies.get(settings.COOKIE_NAME))) if version is None: if file_node.get_guid(): # If this file has been successfully view before but no longer exists # Show a nice error message return addon_deleted_file(file_node=file_node, **kwargs) raise HTTPError( httplib.NOT_FOUND, { 'message_short': 'Not Found', 'message_long': 'This file does not exist' }) # TODO clean up these urls and unify what is used as a version identifier if request.method == 'HEAD': return make_response(('', 200, { 'Location': file_node.generate_waterbutler_url( **dict(extras, direct=None, version=version.identifier)) })) if action == 'download': return redirect( file_node.generate_waterbutler_url( **dict(extras, direct=None, version=version.identifier))) 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) return addon_view_file(auth, node, file_node, version)
def wrapped(*args, **kwargs): remote = request.remote_addr if remote not in addrs: raise HTTPError(httplib.FORBIDDEN) return func(*args, **kwargs)
def external_login_confirm_email_get(auth, uid, token): """ View for email confirmation links when user first login through external identity provider. HTTP Method: GET When users click the confirm link, they are expected not to be logged in. If not, they will be logged out first and redirected back to this view. After OSF verifies the link and performs all actions, they will be automatically logged in through CAS and redirected back to this view again being authenticated. :param auth: the auth context :param uid: the user's primary key :param token: the verification token """ user = User.load(uid) if not user: raise HTTPError(http.BAD_REQUEST) destination = request.args.get('destination') if not destination: raise HTTPError(http.BAD_REQUEST) # if user is already logged in if auth and auth.user: # if it is a wrong user if auth.user._id != user._id: return auth_logout(redirect_url=request.url) # if it is the expected user new = request.args.get('new', None) if destination in campaigns.get_campaigns(): return redirect(campaigns.campaign_url_for(destination)) if new: status.push_status_message(language.WELCOME_MESSAGE, kind='default', jumbotron=True, trust=True) return redirect(web_url_for('dashboard')) # token is invalid if token not in user.email_verifications: raise HTTPError(http.BAD_REQUEST) verification = user.email_verifications[token] email = verification['email'] provider = verification['external_identity'].keys()[0] provider_id = verification['external_identity'][provider].keys()[0] # wrong provider if provider not in user.external_identity: raise HTTPError(http.BAD_REQUEST) external_status = user.external_identity[provider][provider_id] try: ensure_external_identity_uniqueness(provider, provider_id, user) except ValidationError as e: raise HTTPError(http.FORBIDDEN, e.message) if not user.is_registered: user.set_password(uuid.uuid4(), notify=False) user.register(email) if email.lower() not in user.emails: user.emails.append(email.lower()) user.date_last_logged_in = datetime.datetime.utcnow() user.external_identity[provider][provider_id] = 'VERIFIED' user.social[provider.lower()] = provider_id del user.email_verifications[token] user.verification_key = generate_verification_key() user.save() service_url = request.url if external_status == 'CREATE': mails.send_mail(to_addr=user.username, mail=mails.WELCOME, mimetype='html', user=user) service_url += '&{}'.format(urllib.urlencode({'new': 'true'})) elif external_status == 'LINK': mails.send_mail( user=user, to_addr=user.username, mail=mails.EXTERNAL_LOGIN_LINK_SUCCESS, external_id_provider=provider, ) # redirect to CAS and authenticate the user with the verification key return redirect( cas.get_login_url(service_url, username=user.username, verification_key=user.verification_key))
def confirm_email_get(token, auth=None, **kwargs): """ View for email confirmation links. Authenticates and redirects to user settings page if confirmation is successful, otherwise shows an "Expired Link" error. HTTP Method: GET """ user = User.load(kwargs['uid']) is_merge = 'confirm_merge' in request.args is_initial_confirmation = not user.date_confirmed log_out = request.args.get('logout', None) if user is None: raise HTTPError(http.NOT_FOUND) # if the user is merging or adding an email (they already are an osf user) if log_out: return auth_email_logout(token, user) if auth and auth.user and (auth.user._id == user._id or auth.user._id == user.merged_by._id): if not is_merge: # determine if the user registered through a campaign campaign = campaigns.campaign_for_user(user) if campaign: return redirect(campaigns.campaign_url_for(campaign)) # go to home page with push notification if len(auth.user.emails) == 1 and len( auth.user.email_verifications) == 0: status.push_status_message(language.WELCOME_MESSAGE, kind='default', jumbotron=True, trust=True) if token in auth.user.email_verifications: status.push_status_message( language.CONFIRM_ALTERNATE_EMAIL_ERROR, kind='danger', trust=True) return redirect(web_url_for('index')) status.push_status_message(language.MERGE_COMPLETE, kind='success', trust=False) return redirect(web_url_for('user_account')) try: user.confirm_email(token, merge=is_merge) except exceptions.EmailConfirmTokenError as e: raise HTTPError(http.BAD_REQUEST, data={ 'message_short': e.message_short, 'message_long': e.message_long }) if is_initial_confirmation: user.date_last_login = datetime.datetime.utcnow() user.save() # send out our welcome message mails.send_mail(to_addr=user.username, mail=mails.WELCOME, mimetype='html', user=user) # new random verification key, allows CAS to authenticate the user w/o password one-time only. user.verification_key = generate_verification_key() user.save() # redirect to CAS and authenticate the user with a verification key. return redirect( cas.get_login_url(request.url, username=user.username, verification_key=user.verification_key))
def get_record_or_404(path, node_addon): record = model.OsfStorageFileRecord.find_by_path(path, node_addon) if record is not None: return record raise HTTPError(httplib.NOT_FOUND)
def github_set_config(auth, **kwargs): node_settings = kwargs.get('node_addon', None) node = kwargs.get('node', None) user_settings = kwargs.get('user_addon', None) try: if not node: node = node_settings.owner if not user_settings: user_settings = node_settings.user_settings except AttributeError: raise HTTPError(http.BAD_REQUEST) # Parse request github_user_name = request.json.get('github_user', '') github_repo_name = request.json.get('github_repo', '') if not github_user_name or not github_repo_name: raise HTTPError(http.BAD_REQUEST) # Verify that repo exists and that user can access connection = GitHubClient(external_account=node_settings.external_account) repo = connection.repo(github_user_name, github_repo_name) if repo is None: if user_settings: message = ('Cannot access repo. Either the repo does not exist ' 'or your account does not have permission to view it.') else: message = ('Cannot access repo.') return {'message': message}, http.BAD_REQUEST changed = (github_user_name != node_settings.user or github_repo_name != node_settings.repo) # Update hooks if changed: # Delete existing hook, if any node_settings.delete_hook() # Update node settings node_settings.user = github_user_name node_settings.repo = github_repo_name # Log repo select node.add_log( action='github_repo_linked', params={ 'project': node.parent_id, 'node': node._id, 'github': { 'user': github_user_name, 'repo': github_repo_name, } }, auth=auth, ) # Add new hook if node_settings.user and node_settings.repo: node_settings.add_hook(save=False) node_settings.save() return {}
def create_waterbutler_log(payload, **kwargs): with transaction.atomic(): try: auth = payload['auth'] action = LOG_ACTION_MAP[payload['action']] except KeyError: raise HTTPError(httplib.BAD_REQUEST) user = User.load(auth['id']) if user is None: raise HTTPError(httplib.BAD_REQUEST) auth = Auth(user=user) node = kwargs['node'] or kwargs['project'] if action in (NodeLog.FILE_MOVED, NodeLog.FILE_COPIED): for bundle in ('source', 'destination'): for key in ('provider', 'materialized', 'name', 'nid'): if key not in payload[bundle]: raise HTTPError(httplib.BAD_REQUEST) dest = payload['destination'] src = payload['source'] 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 = LOG_ACTION_MAP['rename'] destination_node = node # For clarity source_node = Node.load(payload['source']['nid']) source = source_node.get_addon(payload['source']['provider']) destination = node.get_addon(payload['destination']['provider']) payload['source'].update({ 'materialized': payload['source']['materialized'].lstrip('/'), 'addon': source.config.full_name, '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, } }) payload['destination'].update({ 'materialized': payload['destination']['materialized'].lstrip('/'), 'addon': destination.config.full_name, '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, } }) payload.update({ 'node': destination_node._id, 'project': destination_node.parent_id, }) if not payload.get('errors'): destination_node.add_log(action=action, auth=auth, params=payload) if payload.get('email') is True or payload.get('errors'): mails.send_mail( user.username, mails.FILE_OPERATION_FAILED if payload.get('errors') else mails.FILE_OPERATION_SUCCESS, action=payload['action'], source_node=source_node, destination_node=destination_node, source_path=payload['source']['materialized'], destination_path=payload['source']['materialized'], source_addon=payload['source']['addon'], destination_addon=payload['destination']['addon'], ) if payload.get('errors'): # Action failed but our function succeeded # Bail out to avoid file_signals return {'status': 'success'} else: try: metadata = payload['metadata'] node_addon = node.get_addon(payload['provider']) except KeyError: raise HTTPError(httplib.BAD_REQUEST) if node_addon is None: raise HTTPError(httplib.BAD_REQUEST) metadata['path'] = metadata['path'].lstrip('/') node_addon.create_waterbutler_log(auth, action, metadata) with transaction.atomic(): file_signals.file_updated.send(node=node, user=user, event_type=action, payload=payload) return {'status': 'success'}
def validate_page_num(page, pages): if page < 0 or (pages and page >= pages): raise HTTPError(http.BAD_REQUEST, data=dict( message_long='Invalid value for "page".' ))
def addon_view_or_download_file(auth, path, provider, **kwargs): extras = request.args.to_dict() extras.pop('_', None) # Clean up our url params a bit action = extras.get('action', 'view') node = kwargs.get('node') or kwargs['project'] node_addon = node.get_addon(provider) provider_safe = markupsafe.escape(provider) path_safe = markupsafe.escape(path) project_safe = markupsafe.escape(node.project_or_component) if not path: raise HTTPError(httplib.BAD_REQUEST) if not isinstance(node_addon, BaseStorageAddon): 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, project_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) }) savepoint_id = transaction.savepoint() file_node = FileNode.resolve_class(provider, FileNode.FILE).get_or_create( node, path) # Note: Cookie is provided for authentication to waterbutler # it is overriden to force authentication as the current user # the auth header is also pass to support basic auth version = file_node.touch( request.headers.get('Authorization'), **dict(extras, cookie=request.cookies.get(settings.COOKIE_NAME))) if version is None: # File is either deleted or unable to be found in the provider location # Rollback the insertion of the file_node transaction.savepoint_rollback(savepoint_id) if not file_node.pk: raise HTTPError(httplib.NOT_FOUND, data={ 'message_short': 'File Not Found', 'message_long': 'The requested file could not be found.' }) return addon_deleted_file(file_node=file_node, path=path, **kwargs) else: transaction.savepoint_commit(savepoint_id) # TODO clean up these urls and unify what is used as a version identifier if request.method == 'HEAD': return make_response(('', 200, { 'Location': file_node.generate_waterbutler_url( **dict(extras, direct=None, version=version.identifier, _internal=extras.get('mode') == 'render')) })) if action == 'download': format = extras.get('format') _, extension = os.path.splitext(file_node.name) # avoid rendering files with the same format type. if format and '.{}'.format(format) != extension: return redirect('{}/export?format={}&url={}'.format( MFR_SERVER_URL, format, urllib.quote( file_node.generate_waterbutler_url( **dict(extras, direct=None, version=version.identifier, _internal=extras.get('mode') == 'render'))))) return redirect( file_node.generate_waterbutler_url( **dict(extras, direct=None, version=version.identifier, _internal=extras.get('mode') == 'render'))) if action == 'get_guid': draft_id = extras.get('draft') draft = DraftRegistration.load(draft_id) if draft is None or draft.is_approved: raise HTTPError(httplib.BAD_REQUEST, data={ 'message_short': 'Bad Request', 'message_long': 'File not associated with required object.' }) guid = file_node.get_guid(create=True) guid.referent.save() return dict(guid=guid._id) 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) return addon_view_file(auth, node, file_node, version)
def send_claim_registered_email(claimer, unclaimed_user, node, throttle=24 * 3600): """ A registered user claiming the unclaimed user account as an contributor to a project. Send an email for claiming the account to the referrer and notify the claimer. :param claimer: the claimer :param unclaimed_user: the user account to claim :param node: the project node where the user account is claimed :param throttle: the time period in seconds before another claim for the account can be made :return: :raise: http_status.HTTP_400_BAD_REQUEST """ unclaimed_record = unclaimed_user.get_unclaimed_record(node._primary_key) # check throttle timestamp = unclaimed_record.get('last_sent') if not throttle_period_expired(timestamp, throttle): raise HTTPError( http_status.HTTP_400_BAD_REQUEST, data=dict( message_long= 'User account can only be claimed with an existing user once every 24 hours' )) # roll the valid token for each email, thus user cannot change email and approve a different email address verification_key = generate_verification_key(verification_type='claim') unclaimed_record['token'] = verification_key['token'] unclaimed_record['expires'] = verification_key['expires'] unclaimed_record['claimer_email'] = claimer.username unclaimed_user.save() referrer = OSFUser.load(unclaimed_record['referrer_id']) claim_url = web_url_for( 'claim_user_registered', uid=unclaimed_user._primary_key, pid=node._primary_key, token=unclaimed_record['token'], _absolute=True, ) # Send mail to referrer, telling them to forward verification link to claimer mails.send_mail( referrer.username, mails.FORWARD_INVITE_REGISTERED, user=unclaimed_user, referrer=referrer, node=node, claim_url=claim_url, fullname=unclaimed_record['name'], can_change_preferences=False, osf_contact_email=settings.OSF_CONTACT_EMAIL, ) unclaimed_record['last_sent'] = get_timestamp() unclaimed_user.save() # Send mail to claimer, telling them to wait for referrer mails.send_mail( claimer.username, mails.PENDING_VERIFICATION_REGISTERED, fullname=claimer.fullname, referrer=referrer, node=node, can_change_preferences=False, osf_contact_email=settings.OSF_CONTACT_EMAIL, )
def auth_register(auth): """ View for OSF register. Land on the register page, redirect or go to `auth_logout` depending on `data` returned by `login_and_register_handler`. `/register` only takes a valid campaign, a valid next, the logout flag or no query parameter `login_and_register_handler()` handles the following cases: if campaign and logged in, go to campaign landing page (or valid next_url if presents) if campaign and logged out, go to campaign register page (with next_url if presents) if next_url and logged in, go to next url if next_url and logged out, go to cas login page with current request url as service parameter if next_url and logout flag, log user out first and then go to the next_url if none, go to `/dashboard` which is decorated by `@must_be_logged_in` :param auth: the auth context :return: land, redirect or `auth_logout` :raise: http.BAD_REQUEST """ context = {} # a target campaign in `auth.campaigns` campaign = request.args.get('campaign') # the service url for CAS login or redirect url for OSF next_url = request.args.get('next') # used only for `claim_user_registered` logout = request.args.get('logout') # logout must have next_url if logout and not next_url: raise HTTPError(http.BAD_REQUEST) data = login_and_register_handler(auth, login=False, campaign=campaign, next_url=next_url, logout=logout) # land on register page if data['status_code'] == http.OK: if data['must_login_warning']: status.push_status_message(language.MUST_LOGIN, trust=False) destination = cas.get_login_url(data['next_url']) # "Already have and account?" link context['non_institution_login_url'] = destination # "Sign In" button in navigation bar, overwrite the default value set in routes.py context['login_url'] = destination # "Login through your institution" link context['institution_login_url'] = cas.get_login_url( data['next_url'], campaign='institution') context['preprint_campaigns'] = { k._id + '-preprints': { 'id': k._id, 'name': k.name, 'logo_path': settings.PREPRINTS_ASSETS + k._id + '/square_color_no_transparent.png' } for k in PreprintProvider.objects.all() if k._id != 'osf' } context['campaign'] = data['campaign'] return context, http.OK # redirect to url elif data['status_code'] == http.FOUND: return redirect(data['next_url']) # go to other views elif data['status_code'] == 'auth_logout': return auth_logout(redirect_url=data['next_url']) raise HTTPError(http.BAD_REQUEST)
def claim_user_registered(auth, node, **kwargs): """ View that prompts user to enter their password in order to claim being a contributor on a project. A user must be logged in. """ current_user = auth.user sign_out_url = cas.get_logout_url(service_url=cas.get_login_url( service_url=request.url)) if not current_user: return redirect(sign_out_url) # Logged in user should not be a contributor the project if hasattr(node, 'is_contributor') and node.is_contributor(current_user): data = { 'message_short': 'Already a contributor', 'message_long': ('The logged-in user is already a contributor to this ' 'project. Would you like to <a href="{}">log out</a>?' ).format(sign_out_url) } raise HTTPError(http_status.HTTP_400_BAD_REQUEST, data=data) # Logged in user is already a member of the OSF Group if hasattr(node, 'is_member') and node.is_member(current_user): data = { 'message_short': 'Already a member', 'message_long': ('The logged-in user is already a member of this OSF Group. ' 'Would you like to <a href="{}">log out</a>?' ).format(sign_out_url) } raise HTTPError(http_status.HTTP_400_BAD_REQUEST, data=data) uid, pid, token = kwargs['uid'], kwargs['pid'], kwargs['token'] unreg_user = OSFUser.load(uid) if not verify_claim_token(unreg_user, token, pid=node._primary_key): error_data = { 'message_short': 'Invalid url.', 'message_long': 'The token in the URL is invalid or has expired.' } raise HTTPError(http_status.HTTP_400_BAD_REQUEST, data=error_data) # Store the unreg_user data on the session in case the user registers # a new account session.data['unreg_user'] = {'uid': uid, 'pid': pid, 'token': token} session.save() # If a user is already validated though external auth, it is OK to claim should_claim = check_external_auth(auth.user) form = PasswordForm(request.form) if request.method == 'POST': if form.validate(): if current_user.check_password(form.password.data): should_claim = True else: status.push_status_message(language.LOGIN_FAILED, kind='warning', trust=False) else: forms.push_errors_to_status(form.errors) if should_claim: node.replace_contributor(old=unreg_user, new=current_user) node.save() if isinstance(node, OSFGroup): status.push_status_message( 'You are now a member of this OSFGroup.', kind='success', trust=False) else: status.push_status_message( 'You are now a contributor to this project.', kind='success', trust=False) return redirect(node.url) if is_json_request(): form_ret = forms.utils.jsonify(form) user_ret = profile_utils.serialize_user(current_user, full=False) else: form_ret = form user_ret = current_user return {'form': form_ret, 'user': user_ret, 'signOutUrl': sign_out_url}
def make_url_map(app): """Set up all the routes for the OSF app. :param app: A Flask/Werkzeug app to bind the rules to. """ # Set default views to 404, using URL-appropriate renderers process_rules(app, [ Rule('/<path:_>', ['get', 'post'], HTTPError(http.NOT_FOUND), OsfWebRenderer('', render_mako_string)), Rule('/api/v1/<path:_>', ['get', 'post'], HTTPError(http.NOT_FOUND), json_renderer), ]) ### GUID ### process_rules(app, [ Rule( [ '/<guid>/', '/<guid>/<path:suffix>', ], ['get', 'post', 'put', 'patch', 'delete'], website_views.resolve_guid, notemplate, ), Rule( [ '/api/v1/<guid>/', '/api/v1/<guid>/<path:suffix>', ], ['get', 'post', 'put', 'patch', 'delete'], website_views.resolve_guid, json_renderer, ), ]) # Static files process_rules(app, [ Rule('/favicon.ico', 'get', favicon, json_renderer), Rule('/robots.txt', 'get', robots, json_renderer), ]) ### Base ### process_rules(app, [ Rule('/dashboard/', 'get', website_views.dashboard, OsfWebRenderer('dashboard.mako')), Rule('/reproducibility/', 'get', website_views.reproducibility, OsfWebRenderer('', render_mako_string)), Rule( '/about/', 'get', website_views.redirect_about, json_renderer, ), Rule( '/howosfworks/', 'get', website_views.redirect_howosfworks, json_renderer, ), Rule('/faq/', 'get', {}, OsfWebRenderer('public/pages/faq.mako')), Rule('/getting-started/', 'get', {}, OsfWebRenderer('public/pages/getting_started.mako')), Rule('/explore/', 'get', {}, OsfWebRenderer('public/explore.mako')), Rule(['/messages/', '/help/'], 'get', {}, OsfWebRenderer('public/comingsoon.mako')), Rule( '/view/<meeting>/', 'get', conference_views.conference_results, OsfWebRenderer('public/pages/meeting.mako'), ), Rule( '/view/<meeting>/plain/', 'get', conference_views.conference_results, OsfWebRenderer('public/pages/meeting_plain.mako'), endpoint_suffix='__plain', ), Rule( '/api/v1/view/<meeting>/', 'get', conference_views.conference_data, json_renderer, ), Rule( '/meetings/', 'get', conference_views.conference_view, OsfWebRenderer('public/pages/meeting_landing.mako'), ), Rule( '/presentations/', 'get', conference_views.redirect_to_meetings, json_renderer, ), Rule('/news/', 'get', {}, OsfWebRenderer('public/pages/news.mako')), ]) # Site-wide API routes process_rules(app, [ Rule( '/citations/styles/', 'get', citation_views.list_citation_styles, json_renderer, ), ], prefix='/api/v1') process_rules(app, [ Rule( [ '/project/<pid>/<addon>/settings/disable/', '/project/<pid>/node/<nid>/<addon>/settings/disable/', ], 'post', addon_views.disable_addon, json_renderer, ), Rule( '/profile/<uid>/<addon>/settings/', 'get', addon_views.get_addon_user_config, json_renderer, ), ], prefix='/api/v1') # OAuth process_rules(app, [ Rule( '/oauth/connect/<service_name>/', 'get', oauth_views.oauth_connect, json_renderer, ), Rule( '/oauth/callback/<service_name>/', 'get', oauth_views.oauth_callback, OsfWebRenderer('util/oauth_complete.mako'), ), ]) process_rules(app, [ Rule( [ '/oauth/accounts/<external_account_id>/', ], 'delete', oauth_views.oauth_disconnect, json_renderer, ) ], prefix='/api/v1') process_rules(app, [ Rule('/dashboard/get_nodes/', 'get', website_views.get_dashboard_nodes, json_renderer), Rule([ '/dashboard/<nid>', '/dashboard/', ], 'get', website_views.get_dashboard, json_renderer), ], prefix='/api/v1') ### Metadata ### process_rules(app, [ Rule( [ '/project/<pid>/comments/', '/project/<pid>/node/<nid>/comments/', ], 'get', project_views.comment.list_comments, json_renderer, ), Rule( [ '/project/<pid>/comments/discussion/', '/project/<pid>/node/<nid>/comments/discussion/', ], 'get', project_views.comment.comment_discussion, json_renderer, ), Rule( [ '/project/<pid>/comment/', '/project/<pid>/node/<nid>/comment/', ], 'post', project_views.comment.add_comment, json_renderer, ), Rule( [ '/project/<pid>/comment/<cid>/', '/project/<pid>/node/<nid>/comment/<cid>/', ], 'put', project_views.comment.edit_comment, json_renderer, ), Rule( [ '/project/<pid>/comment/<cid>/', '/project/<pid>/node/<nid>/comment/<cid>/', ], 'delete', project_views.comment.delete_comment, json_renderer, ), Rule( [ '/project/<pid>/comment/<cid>/undelete/', '/project/<pid>/node/<nid>/comment/<cid>/undelete/', ], 'put', project_views.comment.undelete_comment, json_renderer, ), Rule( [ '/project/<pid>/comments/timestamps/', '/project/<pid>/node/<nid>/comments/timestamps/', ], 'put', project_views.comment.update_comments_timestamp, json_renderer, ), Rule( [ '/project/<pid>/comment/<cid>/report/', '/project/<pid>/node/<nid>/comment/<cid>/report/', ], 'post', project_views.comment.report_abuse, json_renderer, ), Rule( [ '/project/<pid>/comment/<cid>/unreport/', '/project/<pid>/node/<nid>/comment/<cid>/unreport/', ], 'post', project_views.comment.unreport_abuse, json_renderer, ), Rule( [ '/project/<pid>/citation/', '/project/<pid>/node/<nid>/citation/', ], 'get', citation_views.node_citation, json_renderer, ), ], prefix='/api/v1') ### Forms ### process_rules(app, [ Rule('/forms/registration/', 'get', website_views.registration_form, json_renderer), Rule('/forms/signin/', 'get', website_views.signin_form, json_renderer), Rule('/forms/forgot_password/', 'get', website_views.forgot_password_form, json_renderer), Rule('/forms/reset_password/', 'get', website_views.reset_password_form, json_renderer), ], prefix='/api/v1') ### Discovery ### process_rules(app, [ Rule('/explore/activity/', 'get', discovery_views.activity, OsfWebRenderer('public/pages/active_nodes.mako', trust=False)), ]) ### Auth ### # Web process_rules( app, [ Rule( '/confirm/<uid>/<token>/', 'get', auth_views.confirm_email_get, # View will either redirect or display error message OsfWebRenderer('error.mako', render_mako_string)), Rule( '/resetpassword/<verification_key>/', ['get', 'post'], auth_views.reset_password, OsfWebRenderer('public/resetpassword.mako', render_mako_string)), # Resend confirmation URL linked to in CAS login page Rule('/resend/', ['get', 'post'], auth_views.resend_confirmation, OsfWebRenderer('resend.mako', render_mako_string)), # TODO: Remove `auth_register_post` Rule('/register/', 'post', auth_views.auth_register_post, OsfWebRenderer('public/login.mako')), Rule('/api/v1/register/', 'post', auth_views.register_user, json_renderer), Rule(['/login/', '/account/'], 'get', auth_views.auth_login, OsfWebRenderer('public/login.mako')), Rule('/login/first/', 'get', auth_views.auth_login, OsfWebRenderer('public/login.mako'), endpoint_suffix='__first', view_kwargs={'first': True}), Rule('/logout/', 'get', auth_views.auth_logout, notemplate), Rule('/forgotpassword/', 'get', auth_views.forgot_password_get, OsfWebRenderer('public/forgot_password.mako')), Rule('/forgotpassword/', 'post', auth_views.forgot_password_post, OsfWebRenderer('public/login.mako')), Rule(['/midas/', '/summit/', '/accountbeta/', '/decline/'], 'get', auth_views.auth_registerbeta, OsfWebRenderer('', render_mako_string)), Rule('/login/connected_tools/', 'get', landing_page_views.connected_tools, OsfWebRenderer('public/login_landing.mako')), Rule('/login/enriched_profile/', 'get', landing_page_views.enriched_profile, OsfWebRenderer('public/login_landing.mako')), ]) ### Profile ### # Web process_rules( app, [ Rule('/profile/', 'get', profile_views.profile_view, OsfWebRenderer('profile.mako', trust=False)), Rule('/profile/<uid>/', 'get', profile_views.profile_view_id, OsfWebRenderer('profile.mako', trust=False)), Rule(["/user/merge/"], 'get', auth_views.merge_user_get, OsfWebRenderer("merge_accounts.mako", trust=False)), Rule(["/user/merge/"], 'post', auth_views.merge_user_post, OsfWebRenderer("merge_accounts.mako", trust=False)), # Route for claiming and setting email and password. # Verification token must be querystring argument Rule(['/user/<uid>/<pid>/claim/'], ['get', 'post'], project_views.contributor.claim_user_form, OsfWebRenderer('claim_account.mako', trust=False)), Rule(['/user/<uid>/<pid>/claim/verify/<token>/'], ['get', 'post'], project_views.contributor.claim_user_registered, OsfWebRenderer('claim_account_registered.mako', trust=False)), Rule( '/settings/', 'get', profile_views.user_profile, OsfWebRenderer('profile/settings.mako', trust=False), ), Rule( '/settings/account/', 'get', profile_views.user_account, OsfWebRenderer('profile/account.mako', trust=False), ), Rule( '/settings/account/password', 'post', profile_views.user_account_password, OsfWebRenderer('profile/account.mako', trust=False), ), Rule( '/settings/addons/', 'get', profile_views.user_addons, OsfWebRenderer('profile/addons.mako', trust=False), ), Rule( '/settings/notifications/', 'get', profile_views.user_notifications, OsfWebRenderer('profile/notifications.mako', trust=False), ), # TODO: Uncomment once outstanding issues with this feature are addressed # Rule( # '/@<twitter_handle>/', # 'get', # profile_views.redirect_to_twitter, # OsfWebRenderer('error.mako', render_mako_string, trust=False) # ), ]) # API process_rules( app, [ Rule('/profile/', 'get', profile_views.profile_view, json_renderer), Rule('/profile/', 'put', profile_views.update_user, json_renderer), Rule('/resend/', 'put', profile_views.resend_confirmation, json_renderer), Rule('/profile/<uid>/', 'get', profile_views.profile_view_id, json_renderer), # Used by profile.html Rule('/profile/<uid>/edit/', 'post', profile_views.edit_profile, json_renderer), Rule('/profile/<uid>/public_projects/', 'get', profile_views.get_public_projects, json_renderer), Rule('/profile/<uid>/public_components/', 'get', profile_views.get_public_components, json_renderer), Rule('/profile/<user_id>/summary/', 'get', profile_views.get_profile_summary, json_renderer), Rule('/user/<uid>/<pid>/claim/email/', 'post', project_views.contributor.claim_user_post, json_renderer), Rule( '/profile/export/', 'post', profile_views.request_export, json_renderer, ), Rule( '/profile/deactivate/', 'post', profile_views.request_deactivation, json_renderer, ), Rule( [ '/profile/gravatar/', '/users/gravatar/', '/profile/gravatar/<size>', '/users/gravatar/<size>', ], 'get', profile_views.current_user_gravatar, json_renderer, ), Rule( [ '/profile/<uid>/gravatar/', '/users/<uid>/gravatar/', '/profile/<uid>/gravatar/<size>', '/users/<uid>/gravatar/<size>', ], 'get', profile_views.get_gravatar, json_renderer, ), # Rules for user profile configuration Rule('/settings/names/', 'get', profile_views.serialize_names, json_renderer), Rule('/settings/names/', 'put', profile_views.unserialize_names, json_renderer), Rule('/settings/names/impute/', 'get', profile_views.impute_names, json_renderer), Rule( [ '/settings/social/', '/settings/social/<uid>/', ], 'get', profile_views.serialize_social, json_renderer, ), Rule( [ '/settings/jobs/', '/settings/jobs/<uid>/', ], 'get', profile_views.serialize_jobs, json_renderer, ), Rule( [ '/settings/schools/', '/settings/schools/<uid>/', ], 'get', profile_views.serialize_schools, json_renderer, ), Rule([ '/settings/social/', '/settings/social/<uid>/', ], 'put', profile_views.unserialize_social, json_renderer), Rule([ '/settings/jobs/', '/settings/jobs/<uid>/', ], 'put', profile_views.unserialize_jobs, json_renderer), Rule([ '/settings/schools/', '/settings/schools/<uid>/', ], 'put', profile_views.unserialize_schools, json_renderer), ], prefix='/api/v1', ) ### Search ### # Web process_rules(app, [ Rule('/search/', 'get', {}, OsfWebRenderer('search.mako')), Rule('/share/', 'get', {}, OsfWebRenderer('share_search.mako')), Rule('/share/registration/', 'get', {'register': settings.SHARE_REGISTRATION_URL}, OsfWebRenderer('share_registration.mako')), Rule('/share/help/', 'get', {'help': settings.SHARE_API_DOCS_URL}, OsfWebRenderer('share_api_docs.mako')), Rule('/share_dashboard/', 'get', {}, OsfWebRenderer('share_dashboard.mako')), Rule('/share/atom/', 'get', search_views.search_share_atom, xml_renderer), Rule('/api/v1/user/search/', 'get', search_views.search_contributor, json_renderer), Rule( '/api/v1/search/node/', 'post', project_views.node.search_node, json_renderer, ), ]) # API process_rules(app, [ Rule(['/search/', '/search/<type>/'], ['get', 'post'], search_views.search_search, json_renderer), Rule('/search/projects/', 'get', search_views.search_projects_by_title, json_renderer), Rule('/share/search/', ['get', 'post'], search_views.search_share, json_renderer), Rule('/share/stats/', 'get', search_views.search_share_stats, json_renderer), Rule('/share/providers/', 'get', search_views.search_share_providers, json_renderer), ], prefix='/api/v1') # Project # Web process_rules( app, [ Rule('/', 'get', website_views.index, OsfWebRenderer('index.mako')), Rule('/goodbye/', 'get', goodbye, OsfWebRenderer('index.mako')), Rule([ '/project/<pid>/', '/project/<pid>/node/<nid>/', ], 'get', project_views.node.view_project, OsfWebRenderer('project/project.mako', trust=False)), # Create a new subproject/component Rule('/project/<pid>/newnode/', 'post', project_views.node.project_new_node, notemplate), # # TODO: Add API endpoint for tags # Rule('/tags/<tag>/', 'get', project_views.tag.project_tag, OsfWebRenderer('tags.mako')), Rule('/api/v1/folder/<nid>', 'post', project_views.node.folder_new_post, json_renderer), Rule('/project/new/<pid>/beforeTemplate/', 'get', project_views.node.project_before_template, json_renderer), Rule( [ '/project/<pid>/contributors/', '/project/<pid>/node/<nid>/contributors/', ], 'get', project_views.node.node_contributors, OsfWebRenderer('project/contributors.mako', trust=False), ), Rule([ '/project/<pid>/settings/', '/project/<pid>/node/<nid>/settings/', ], 'get', project_views.node.node_setting, OsfWebRenderer('project/settings.mako', trust=False)), # Permissions Rule( [ '/project/<pid>/permissions/<permissions>/', '/project/<pid>/node/<nid>/permissions/<permissions>/', ], 'post', project_views.node.project_set_privacy, OsfWebRenderer( 'project/project.mako' ) # TODO: Should this be notemplate? (post request) ), ### Logs ### # View forks Rule([ '/project/<pid>/forks/', '/project/<pid>/node/<nid>/forks/', ], 'get', project_views.node.node_forks, OsfWebRenderer('project/forks.mako', trust=False)), # Registrations Rule([ '/project/<pid>/register/', '/project/<pid>/node/<nid>/register/', ], 'get', project_views.register.node_register_page, OsfWebRenderer('project/register.mako', trust=False)), Rule([ '/project/<pid>/register/<template>/', '/project/<pid>/node/<nid>/register/<template>/', ], 'get', project_views.register.node_register_template_page, OsfWebRenderer('project/register.mako', trust=False)), Rule([ '/project/<pid>/registrations/', '/project/<pid>/node/<nid>/registrations/', ], 'get', project_views.node.node_registrations, OsfWebRenderer('project/registrations.mako', trust=False)), # TODO: Can't create a registration locally, so can't test this one..? Rule([ '/project/<pid>/retraction/', '/project/<pid>/node/<nid>/retraction/', ], 'get', project_views.register.node_registration_retraction_get, OsfWebRenderer('project/retract_registration.mako', trust=False)), Rule( '/ids/<category>/<path:value>/', 'get', project_views.register.get_referent_by_identifier, notemplate, ), # Statistics Rule( [ '/project/<pid>/statistics/', '/project/<pid>/node/<nid>/statistics/', ], 'get', project_views.node.project_statistics_redirect, notemplate, ), Rule([ '/project/<pid>/analytics/', '/project/<pid>/node/<nid>/analytics/', ], 'get', project_views.node.project_statistics, OsfWebRenderer('project/statistics.mako', trust=False)), ### Files ### # Note: Web endpoint for files view must pass `mode` = `page` to # include project view data and JS includes # TODO: Start waterbutler to test Rule( [ '/project/<pid>/files/', '/project/<pid>/node/<nid>/files/', ], 'get', project_views.file.collect_file_trees, OsfWebRenderer('project/files.mako', trust=False), view_kwargs={'mode': 'page'}, ), Rule([ '/project/<pid>/files/<provider>/<path:path>/', '/project/<pid>/node/<nid>/files/<provider>/<path:path>/', ], 'get', addon_views.addon_view_or_download_file, OsfWebRenderer('project/view_file.mako', trust=False)), Rule( [ # Legacy Addon view file paths '/project/<pid>/<provider>/files/<path:path>/', '/project/<pid>/node/<nid>/<provider>/files/<path:path>/', '/project/<pid>/<provider>/files/<path:path>/download/', '/project/<pid>/node/<nid>/<provider>/files/<path:path>/download/', # Legacy routes for `download_file` '/project/<pid>/osffiles/<fid>/download/', '/project/<pid>/node/<nid>/osffiles/<fid>/download/', # Legacy routes for `view_file` '/project/<pid>/osffiles/<fid>/', '/project/<pid>/node/<nid>/osffiles/<fid>/', # Note: Added these old URLs for backwards compatibility with # hard-coded links. '/project/<pid>/osffiles/download/<fid>/', '/project/<pid>/node/<nid>/osffiles/download/<fid>/', '/project/<pid>/files/<fid>/', '/project/<pid>/node/<nid>/files/<fid>/', '/project/<pid>/files/download/<fid>/', '/project/<pid>/node/<nid>/files/download/<fid>/', # Legacy routes for `download_file_by_version` '/project/<pid>/osffiles/<fid>/version/<vid>/download/', '/project/<pid>/node/<nid>/osffiles/<fid>/version/<vid>/download/', # Note: Added these old URLs for backwards compatibility with # hard-coded links. '/project/<pid>/osffiles/<fid>/version/<vid>/', '/project/<pid>/node/<nid>/osffiles/<fid>/version/<vid>/', '/project/<pid>/osffiles/download/<fid>/version/<vid>/', '/project/<pid>/node/<nid>/osffiles/download/<fid>/version/<vid>/', '/project/<pid>/files/<fid>/version/<vid>/', '/project/<pid>/node/<nid>/files/<fid>/version/<vid>/', '/project/<pid>/files/download/<fid>/version/<vid>/', '/project/<pid>/node/<nid>/files/download/<fid>/version/<vid>/', ], 'get', addon_views.addon_view_or_download_file_legacy, OsfWebRenderer('project/view_file.mako', trust=False), ), Rule( [ # api/v1 Legacy routes for `download_file` '/api/v1/project/<pid>/osffiles/<fid>/', '/api/v1/project/<pid>/node/<nid>/osffiles/<fid>/', '/api/v1/project/<pid>/files/download/<fid>/', '/api/v1/project/<pid>/node/<nid>/files/download/<fid>/', #api/v1 Legacy routes for `download_file_by_version` '/api/v1/project/<pid>/osffiles/<fid>/version/<vid>/', '/api/v1/project/<pid>/node/<nid>/osffiles/<fid>/version/<vid>/', '/api/v1/project/<pid>/files/download/<fid>/version/<vid>/', '/api/v1/project/<pid>/node/<nid>/files/download/<fid>/version/<vid>/', ], 'get', addon_views.addon_view_or_download_file_legacy, json_renderer), ]) # API process_rules( app, [ Rule( '/email/meeting/', 'post', conference_views.meeting_hook, json_renderer, ), Rule('/mailchimp/hooks/', 'get', profile_views.mailchimp_get_endpoint, json_renderer), Rule('/mailchimp/hooks/', 'post', profile_views.sync_data_from_mailchimp, json_renderer), # Create project, used by projectCreator.js Rule('/project/new/', 'post', project_views.node.project_new_post, json_renderer), Rule([ '/project/<pid>/contributors_abbrev/', '/project/<pid>/node/<nid>/contributors_abbrev/', ], 'get', project_views.contributor.get_node_contributors_abbrev, json_renderer), Rule('/tags/<tag>/', 'get', project_views.tag.project_tag, json_renderer), Rule([ '/project/<pid>/', '/project/<pid>/node/<nid>/', ], 'get', project_views.node.view_project, json_renderer), Rule([ '/project/<pid>/expand/', '/project/<pid>/node/<nid>/expand/', ], 'post', project_views.node.expand, json_renderer), Rule([ '/project/<pid>/collapse/', '/project/<pid>/node/<nid>/collapse/', ], 'post', project_views.node.collapse, json_renderer), Rule( [ '/project/<pid>/pointer/', '/project/<pid>/node/<nid>/pointer/', ], 'get', project_views.node.get_pointed, json_renderer, ), Rule( [ '/project/<pid>/pointer/', '/project/<pid>/node/<nid>/pointer/', ], 'post', project_views.node.add_pointers, json_renderer, ), Rule( [ '/pointer/', ], 'post', project_views.node.add_pointer, json_renderer, ), Rule( [ '/pointers/move/', ], 'post', project_views.node.move_pointers, json_renderer, ), Rule( [ '/project/<pid>/pointer/', '/project/<pid>/node/<nid>pointer/', ], 'delete', project_views.node.remove_pointer, json_renderer, ), Rule( [ '/folder/<pid>/pointer/<pointer_id>', ], 'delete', project_views.node.remove_pointer_from_folder, json_renderer, ), Rule( [ '/folder/<pid>/pointers/', ], 'delete', project_views.node.remove_pointers_from_folder, json_renderer, ), Rule( [ '/folder/<pid>', ], 'delete', project_views.node.delete_folder, json_renderer, ), Rule('/folder/', 'put', project_views.node.add_folder, json_renderer), Rule([ '/project/<pid>/get_summary/', '/project/<pid>/node/<nid>/get_summary/', ], 'get', project_views.node.get_summary, json_renderer), Rule([ '/project/<pid>/get_children/', '/project/<pid>/node/<nid>/get_children/', ], 'get', project_views.node.get_children, json_renderer), Rule(['/project/<pid>/get_folder_pointers/'], 'get', project_views.node.get_folder_pointers, json_renderer), Rule([ '/project/<pid>/get_forks/', '/project/<pid>/node/<nid>/get_forks/', ], 'get', project_views.node.get_forks, json_renderer), Rule([ '/project/<pid>/get_registrations/', '/project/<pid>/node/<nid>/get_registrations/', ], 'get', project_views.node.get_registrations, json_renderer), Rule('/log/<log_id>/', 'get', project_views.log.get_log, json_renderer), Rule([ '/project/<pid>/log/', '/project/<pid>/node/<nid>/log/', ], 'get', project_views.log.get_logs, json_renderer), Rule([ '/project/<pid>/get_contributors/', '/project/<pid>/node/<nid>/get_contributors/', ], 'get', project_views.contributor.get_contributors, json_renderer), Rule([ '/project/<pid>/get_contributors_from_parent/', '/project/<pid>/node/<nid>/get_contributors_from_parent/', ], 'get', project_views.contributor.get_contributors_from_parent, json_renderer), # Reorder contributors Rule( [ '/project/<pid>/contributors/manage/', '/project/<pid>/node/<nid>/contributors/manage/', ], 'POST', project_views.contributor.project_manage_contributors, json_renderer, ), Rule([ '/project/<pid>/get_most_in_common_contributors/', '/project/<pid>/node/<nid>/get_most_in_common_contributors/', ], 'get', project_views.contributor.get_most_in_common_contributors, json_renderer), Rule([ '/project/<pid>/get_recently_added_contributors/', '/project/<pid>/node/<nid>/get_recently_added_contributors/', ], 'get', project_views.contributor.get_recently_added_contributors, json_renderer), Rule([ '/project/<pid>/get_editable_children/', '/project/<pid>/node/<nid>/get_editable_children/', ], 'get', project_views.node.get_editable_children, json_renderer), # Private Link Rule([ '/project/<pid>/private_link/', '/project/<pid>/node/<nid>/private_link/', ], 'post', project_views.node.project_generate_private_link_post, json_renderer), Rule([ '/project/<pid>/private_link/edit/', '/project/<pid>/node/<nid>/private_link/edit/', ], 'put', project_views.node.project_private_link_edit, json_renderer), Rule([ '/project/<pid>/private_link/', '/project/<pid>/node/<nid>/private_link/', ], 'delete', project_views.node.remove_private_link, json_renderer), Rule([ '/project/<pid>/private_link/', '/project/<pid>/node/<nid>/private_link/', ], 'get', project_views.node.private_link_table, json_renderer), # Create, using existing project as a template Rule([ '/project/new/<nid>/', ], 'post', project_views.node.project_new_from_template, json_renderer), # Update Rule( [ '/project/<pid>/', '/project/<pid>/node/<nid>/', ], 'put', project_views.node.update_node, json_renderer, ), # Remove Rule( [ '/project/<pid>/', '/project/<pid>/node/<nid>/', ], 'delete', project_views.node.component_remove, json_renderer, ), # Reorder components Rule('/project/<pid>/reorder_components/', 'post', project_views.node.project_reorder_components, json_renderer), # Edit node Rule([ '/project/<pid>/edit/', '/project/<pid>/node/<nid>/edit/', ], 'post', project_views.node.edit_node, json_renderer), # Add / remove tags Rule([ '/project/<pid>/tags/', '/project/<pid>/node/<nid>/tags/', '/project/<pid>/tags/<tag>/', '/project/<pid>/node/<nid>/tags/<tag>/', ], 'post', project_views.tag.project_add_tag, json_renderer), Rule([ '/project/<pid>/tags/', '/project/<pid>/node/<nid>/tags/', '/project/<pid>/tags/<tag>/', '/project/<pid>/node/<nid>/tags/<tag>/', ], 'delete', project_views.tag.project_remove_tag, json_renderer), # Add / remove contributors Rule([ '/project/<pid>/contributors/', '/project/<pid>/node/<nid>/contributors/', ], 'post', project_views.contributor.project_contributors_post, json_renderer), Rule([ '/project/<pid>/beforeremovecontributors/', '/project/<pid>/node/<nid>/beforeremovecontributors/', ], 'post', project_views.contributor.project_before_remove_contributor, json_renderer), # TODO(sloria): should be a delete request to /contributors/ Rule([ '/project/<pid>/removecontributors/', '/project/<pid>/node/<nid>/removecontributors/', ], 'post', project_views.contributor.project_removecontributor, json_renderer), # Forks Rule( [ '/project/<pid>/fork/before/', '/project/<pid>/node/<nid>/fork/before/', ], 'get', project_views.node.project_before_fork, json_renderer, ), Rule( [ '/project/<pid>/fork/', '/project/<pid>/node/<nid>/fork/', ], 'post', project_views.node.node_fork_page, json_renderer, ), Rule( [ '/project/<pid>/pointer/fork/', '/project/<pid>/node/<nid>/pointer/fork/', ], 'post', project_views.node.fork_pointer, json_renderer, ), # View forks Rule([ '/project/<pid>/forks/', '/project/<pid>/node/<nid>/forks/', ], 'get', project_views.node.node_forks, json_renderer), # Registrations Rule([ '/project/<pid>/beforeregister/', '/project/<pid>/node/<nid>/beforeregister', ], 'get', project_views.register.project_before_register, json_renderer), Rule([ '/project/<pid>/register/<template>/', '/project/<pid>/node/<nid>/register/<template>/', ], 'get', project_views.register.node_register_template_page, json_renderer), Rule([ '/project/<pid>/retraction/', '/project/<pid>/node/<nid>/retraction/' ], 'post', project_views.register.node_registration_retraction_post, json_renderer), Rule([ '/project/<pid>/register/<template>/', '/project/<pid>/node/<nid>/register/<template>/', ], 'post', project_views.register.node_register_template_page_post, json_renderer), Rule( [ '/project/<pid>/identifiers/', '/project/<pid>/node/<nid>/identifiers/', ], 'get', project_views.register.node_identifiers_get, json_renderer, ), Rule( [ '/project/<pid>/identifiers/', '/project/<pid>/node/<nid>/identifiers/', ], 'post', project_views.register.node_identifiers_post, json_renderer, ), # Statistics Rule([ '/project/<pid>/statistics/', '/project/<pid>/node/<nid>/statistics/', ], 'get', project_views.node.project_statistics, json_renderer), # Permissions Rule([ '/project/<pid>/permissions/<permissions>/', '/project/<pid>/node/<nid>/permissions/<permissions>/', ], 'post', project_views.node.project_set_privacy, json_renderer), Rule([ '/project/<pid>/permissions/beforepublic/', '/project/<pid>/node/<nid>/permissions/beforepublic/', ], 'get', project_views.node.project_before_set_public, json_renderer), ### Watching ### Rule(['/project/<pid>/watch/', '/project/<pid>/node/<nid>/watch/'], 'post', project_views.node.watch_post, json_renderer), Rule([ '/project/<pid>/unwatch/', '/project/<pid>/node/<nid>/unwatch/' ], 'post', project_views.node.unwatch_post, json_renderer), Rule([ '/project/<pid>/togglewatch/', '/project/<pid>/node/<nid>/togglewatch/' ], 'post', project_views.node.togglewatch_post, json_renderer), Rule(['/watched/logs/'], 'get', website_views.watched_logs_get, json_renderer), ### Accounts ### Rule(['/user/merge/'], 'post', auth_views.merge_user_post, json_renderer), # Combined files Rule( ['/project/<pid>/files/', '/project/<pid>/node/<nid>/files/'], 'get', project_views.file.collect_file_trees, json_renderer, ), # Endpoint to fetch Rubeus.JS/Hgrid-formatted data Rule([ '/project/<pid>/files/grid/', '/project/<pid>/node/<nid>/files/grid/' ], 'get', project_views.file.grid_data, json_renderer), # Settings Rule( '/files/auth/', 'get', addon_views.get_auth, json_renderer, ), Rule( [ '/project/<pid>/waterbutler/logs/', '/project/<pid>/node/<nid>/waterbutler/logs/', ], 'put', addon_views.create_waterbutler_log, json_renderer, ), Rule( [ '/registration/<pid>/callbacks/', ], 'put', project_views.register.registration_callbacks, json_renderer, ), Rule( '/settings/addons/', 'post', profile_views.user_choose_addons, json_renderer, ), Rule( '/settings/notifications/', 'get', profile_views.user_notifications, json_renderer, ), Rule( '/settings/notifications/', 'post', profile_views.user_choose_mailing_lists, json_renderer, ), Rule( '/subscriptions/', 'get', notification_views.get_subscriptions, json_renderer, ), Rule( [ '/project/<pid>/subscriptions/', '/project/<pid>/node/<nid>/subscriptions/' ], 'get', notification_views.get_node_subscriptions, json_renderer, ), Rule( '/subscriptions/', 'post', notification_views.configure_subscription, json_renderer, ), Rule( [ '/project/<pid>/settings/addons/', '/project/<pid>/node/<nid>/settings/addons/', ], 'post', project_views.node.node_choose_addons, json_renderer, ), Rule( [ '/project/<pid>/settings/comments/', '/project/<pid>/node/<nid>/settings/comments/', ], 'post', project_views.node.configure_comments, json_renderer, ), # Invite Users Rule([ '/project/<pid>/invite_contributor/', '/project/<pid>/node/<nid>/invite_contributor/' ], 'post', project_views.contributor.invite_contributor_post, json_renderer), ], prefix='/api/v1') # Set up static routing for addons # NOTE: We use nginx to serve static addon assets in production addon_base_path = os.path.abspath('website/addons') if settings.DEV_MODE: @app.route('/static/addons/<addon>/<path:filename>') def addon_static(addon, filename): addon_path = os.path.join(addon_base_path, addon, 'static') return send_from_directory(addon_path, filename)
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. """ permission = permission_map.get(action, None) if permission is None: 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 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) if permission == 'read' and node.can_view(auth): return True if permission == 'write' and node.can_edit(auth): return True # Users attempting to register projects with components might not have # `write` permissions for all components. This will result in a 403 for # all `copyto` actions as well as `copyfrom` actions if the component # in question is not public. To get around this, we have to recursively # check the node's parent node to determine if they have `write` # permissions up the stack. # TODO(hrybacki): is there a way to tell if this is for a registration? # All nodes being registered that receive the `copyto` action will have # `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): return True parent = parent.parent_node # Users with the PREREG_ADMIN_TAG should be allowed to download files # from prereg challenge draft registrations. try: prereg_schema = MetaSchema.find_one( Q('name', 'eq', 'Prereg Challenge') & Q('schema_version', 'eq', 2) ) prereg_draft_registration = DraftRegistration.find( Q('branched_from', 'eq', node) & Q('registration_schema', 'eq', prereg_schema) ) if action == 'download' and \ auth.user is not None and \ prereg_draft_registration.count() > 0 and \ settings.PREREG_ADMIN_TAG in auth.user.system_tags: return True except NoResultsFound: pass raise HTTPError(httplib.FORBIDDEN if auth.user else httplib.UNAUTHORIZED)
def register_user(**kwargs): """ Register new user account. HTTP Method: POST :param-json str email1: :param-json str email2: :param-json str password: :param-json str fullName: :param-json str campaign: :raises: HTTPError(http.BAD_REQUEST) if validation fails or user already exists """ # Verify that email address match. # Note: Both `landing.mako` and `register.mako` already have this check on the form. Users can not submit the form # if emails do not match. However, this check should not be removed given we may use the raw api call directly. json_data = request.get_json() if str(json_data['email1']).lower() != str(json_data['email2']).lower(): raise HTTPError( http.BAD_REQUEST, data=dict(message_long='Email addresses must match.') ) # Verify that captcha is valid if settings.RECAPTCHA_SITE_KEY and not validate_recaptcha(json_data.get('g-recaptcha-response'), remote_ip=request.remote_addr): raise HTTPError( http.BAD_REQUEST, data=dict(message_long='Invalid Captcha') ) try: full_name = request.json['fullName'] full_name = strip_html(full_name) campaign = json_data.get('campaign') if campaign and campaign not in campaigns.get_campaigns(): campaign = None accepted_terms_of_service = timezone.now() if json_data.get('acceptedTermsOfService') else None user = framework_auth.register_unconfirmed( request.json['email1'], request.json['password'], full_name, campaign=campaign, accepted_terms_of_service=accepted_terms_of_service ) framework_auth.signals.user_registered.send(user) except (ValidationValueError, DuplicateEmailError): raise HTTPError( http.BAD_REQUEST, data=dict( message_long=language.ALREADY_REGISTERED.format( email=markupsafe.escape(request.json['email1']) ) ) ) except BlacklistedEmailError as e: raise HTTPError( http.BAD_REQUEST, data=dict(message_long=language.BLACKLISTED_EMAIL) ) except ValidationError as e: raise HTTPError( http.BAD_REQUEST, data=dict(message_long=e.message) ) if settings.CONFIRM_REGISTRATIONS_BY_EMAIL: send_confirm_email(user, email=user.username) message = language.REGISTRATION_SUCCESS.format(email=user.username) return {'message': message} else: return {'message': 'You may now log in.'}
def login_and_register_handler(auth, login=True, campaign=None, next_url=None, logout=None): """ Non-view helper to handle `login` and `register` requests. :param auth: the auth context :param login: `True` if `GET /login`, `False` if `GET /register` :param campaign: a target campaign defined in `auth.campaigns` :param next_url: the service url for CAS login or redirect url for OSF :param logout: used only for `claim_user_registered` :return: data object that contains actions for `auth_register` and `auth_login` :raises: http.BAD_REQUEST """ # Only allow redirects which are relative root or full domain. Disallows external redirects. if next_url and not validate_next_url(next_url): raise HTTPError(http.BAD_REQUEST) data = { 'status_code': http.FOUND if login else http.OK, 'next_url': next_url, 'campaign': None, 'must_login_warning': False, } # login or register with campaign parameter if campaign: if validate_campaign(campaign): # GET `/register` or '/login` with `campaign=institution` # unlike other campaigns, institution login serves as an alternative for authentication if campaign == 'institution': next_url = web_url_for('dashboard', _absolute=True) data['status_code'] = http.FOUND if auth.logged_in: data['next_url'] = next_url else: data['next_url'] = cas.get_login_url( next_url, campaign='institution') # for non-institution campaigns else: destination = next_url if next_url else campaigns.campaign_url_for( campaign) if auth.logged_in: # if user is already logged in, go to the campaign landing page data['status_code'] = http.FOUND data['next_url'] = destination else: # if user is logged out, go to the osf register page with campaign context if login: # `GET /login?campaign=...` data['next_url'] = web_url_for('auth_register', campaign=campaign, next=destination) else: # `GET /register?campaign=...` data['campaign'] = campaign if campaigns.is_proxy_login(campaign): data['next_url'] = web_url_for('auth_login', next=destination, _absolute=True) else: data['next_url'] = destination else: # invalid campaign raise HTTPError(http.BAD_REQUEST) # login or register with next parameter elif next_url: if logout: # handle `claim_user_registered` data['next_url'] = next_url if auth.logged_in: # log user out and come back data['status_code'] = 'auth_logout' else: # after logout, land on the register page with "must_login" warning data['status_code'] = http.OK data['must_login_warning'] = True elif auth.logged_in: # if user is already logged in, redirect to `next_url` data['status_code'] = http.FOUND data['next_url'] = next_url elif login: # `/login?next=next_url`: go to CAS login page with current request url as service url data['status_code'] = http.FOUND data['next_url'] = cas.get_login_url(request.url) else: # `/register?next=next_url`: land on OSF register page with request url as next url data['status_code'] = http.OK data['next_url'] = request.url else: # `/login/` or `/register/` without any parameter if auth.logged_in: data['status_code'] = http.FOUND data['next_url'] = web_url_for('dashboard', _absolute=True) return data
def get_target_user(auth, uid=None): target = User.load(uid) if uid else auth.user if target is None: raise HTTPError(http.NOT_FOUND) return target