def auth_email_logout(token, user): """ When a user is adding an email or merging an account, add the email to the user and log them out. """ redirect_url = cas.get_logout_url(service_url=cas.get_login_url(service_url=web_url_for('index', _absolute=True))) try: unconfirmed_email = user.get_unconfirmed_email_for_token(token) except InvalidTokenError: raise HTTPError(http.BAD_REQUEST, data={ 'message_short': 'Bad token', 'message_long': 'The provided token is invalid.' }) except ExpiredTokenError: status.push_status_message('The private link you used is expired.') raise HTTPError(http.BAD_REQUEST, data={ 'message_short': 'Expired link', 'message_long': 'The private link you used is expired.' }) try: user_merge = User.find_one(Q('emails', 'eq', unconfirmed_email)) except NoResultsFound: user_merge = False if user_merge: remove_sessions_for_user(user_merge) user.email_verifications[token]['confirmed'] = True user.save() remove_sessions_for_user(user) resp = redirect(redirect_url) resp.delete_cookie(settings.COOKIE_NAME, domain=settings.OSF_COOKIE_DOMAIN) return resp
def auth_email_logout(token, user): """ When a user is adding an email or merging an account, add the email to the user and log them out. """ redirect_url = cas.get_logout_url(service_url=cas.get_login_url( service_url=web_url_for('index', _absolute=True))) try: unconfirmed_email = user.get_unconfirmed_email_for_token(token) except InvalidTokenError: raise HTTPError(http.BAD_REQUEST, data={ 'message_short': 'Bad token', 'message_long': 'The provided token is invalid.' }) except ExpiredTokenError: status.push_status_message('The private link you used is expired.') raise HTTPError(http.BAD_REQUEST, data={ 'message_short': 'Expired link', 'message_long': 'The private link you used is expired.' }) try: user_merge = OSFUser.objects.get(emails__address=unconfirmed_email) except OSFUser.DoesNotExist: user_merge = False if user_merge: remove_sessions_for_user(user_merge) user.email_verifications[token]['confirmed'] = True user.save() remove_sessions_for_user(user) resp = redirect(redirect_url) resp.delete_cookie(settings.COOKIE_NAME, domain=settings.OSF_COOKIE_DOMAIN) return resp
def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) user = self.get_user() existing_password = request.data['existing_password'] new_password = request.data['new_password'] # It has been more than 1 hour since last invalid attempt to change password. Reset the counter for invalid attempts. if throttle_period_expired(user.change_password_last_attempt, settings.TIME_RESET_CHANGE_PASSWORD_ATTEMPTS): user.reset_old_password_invalid_attempts() # There have been more than 3 failed attempts and throttle hasn't expired. if user.old_password_invalid_attempts >= settings.INCORRECT_PASSWORD_ATTEMPTS_ALLOWED and not throttle_period_expired( user.change_password_last_attempt, settings.CHANGE_PASSWORD_THROTTLE, ): time_since_throttle = (timezone.now() - user.change_password_last_attempt.replace(tzinfo=pytz.utc)).total_seconds() wait_time = settings.CHANGE_PASSWORD_THROTTLE - time_since_throttle raise Throttled(wait=wait_time) try: # double new password for confirmation because validation is done on the front-end. user.change_password(existing_password, new_password, new_password) except ChangePasswordError as error: # A response object must be returned instead of raising an exception to avoid rolling back the transaction # and losing the incrementation of failed password attempts user.save() return JsonResponse( {'errors': [{'detail': message} for message in error.messages]}, status=400, content_type='application/vnd.api+json; application/json', ) user.save() remove_sessions_for_user(user) return Response(status=status.HTTP_204_NO_CONTENT)
def test_remove_session_for_user(self): SessionFactory(user=self.user) # sanity check assert Session.find().count() == 1 utils.remove_sessions_for_user(self.user) assert Session.find().count() == 0 SessionFactory() SessionFactory(user=self.user) # sanity check assert Session.find().count() == 2 utils.remove_sessions_for_user(self.user) assert Session.find().count() == 1
def test_remove_session_for_user(self): factories.SessionFactory(user=self.user) # sanity check assert_equal(1, Session.find().count()) utils.remove_sessions_for_user(self.user) assert_equal(0, Session.find().count()) factories.SessionFactory() factories.SessionFactory(user=self.user) # sanity check assert_equal(2, Session.find().count()) utils.remove_sessions_for_user(self.user) assert_equal(1, Session.find().count())
def set_password(self, raw_password, notify=True): """Set the password for this user to the hash of ``raw_password``. If this is a new user, we're done. If this is a password change, then email the user about the change and clear all the old sessions so that users will have to log in again with the new password. :param raw_password: the plaintext value of the new password :param notify: Only meant for unit tests to keep extra notifications from being sent :rtype: list :returns: Changed fields from the user save """ had_existing_password = self.has_usable_password() if self.username == raw_password: raise ChangePasswordError( ['Password cannot be the same as your email address']) super(OSFUser, self).set_password(raw_password) if had_existing_password and notify: mails.send_mail(to_addr=self.username, mail=mails.PASSWORD_RESET, mimetype='plain', user=self) remove_sessions_for_user(self)
def merge_user(self, user): """Merge a registered user into this account. This user will be a contributor on any project. if the registered user and this account are both contributors of the same project. Then it will remove the registered user and set this account to the highest permission of the two and set this account to be visible if either of the two are visible on the project. :param user: A User object to be merged. """ # Fail if the other user has conflicts. if not user.can_be_merged: raise exceptions.MergeConflictError("Users cannot be merged") # Move over the other user's attributes # TODO: confirm for system_tag in user.system_tags: if system_tag not in self.system_tags: self.system_tags.append(system_tag) self.is_claimed = self.is_claimed or user.is_claimed self.is_invited = self.is_invited or user.is_invited # copy over profile only if this user has no profile info if user.jobs and not self.jobs: self.jobs = user.jobs if user.schools and not self.schools: self.schools = user.schools if user.social and not self.social: self.social = user.social unclaimed = user.unclaimed_records.copy() unclaimed.update(self.unclaimed_records) self.unclaimed_records = unclaimed # - unclaimed records should be connected to only one user user.unclaimed_records = {} security_messages = user.security_messages.copy() security_messages.update(self.security_messages) self.security_messages = security_messages for key, value in user.mailing_lists.iteritems(): # subscribe to each list if either user was subscribed subscription = value or self.mailing_lists.get(key) signals.user_merged.send(self, list_name=key, subscription=subscription) # clear subscriptions for merged user signals.user_merged.send(user, list_name=key, subscription=False) for node_id, timestamp in user.comments_viewed_timestamp.iteritems(): if not self.comments_viewed_timestamp.get(node_id): self.comments_viewed_timestamp[node_id] = timestamp elif timestamp > self.comments_viewed_timestamp[node_id]: self.comments_viewed_timestamp[node_id] = timestamp self.emails.extend(user.emails) user.emails = [] for k, v in user.email_verifications.iteritems(): email_to_confirm = v['email'] if k not in self.email_verifications and email_to_confirm != user.username: self.email_verifications[k] = v user.email_verifications = {} # FOREIGN FIELDS for watched in user.watched: if watched not in self.watched: self.watched.append(watched) user.watched = [] for account in user.external_accounts: if account not in self.external_accounts: self.external_accounts.append(account) user.external_accounts = [] # - addons # Note: This must occur before the merged user is removed as a # contributor on the nodes, as an event hook is otherwise fired # which removes the credentials. for addon in user.get_addons(): user_settings = self.get_or_add_addon(addon.config.short_name) user_settings.merge(addon) user_settings.save() # - projects where the user was a contributor for node in user.node__contributed: # Skip dashboard node if node.is_dashboard: continue # if both accounts are contributor of the same project if node.is_contributor(self) and node.is_contributor(user): if node.permissions[user._id] > node.permissions[self._id]: permissions = node.permissions[user._id] else: permissions = node.permissions[self._id] node.set_permissions(user=self, permissions=permissions) visible1 = self._id in node.visible_contributor_ids visible2 = user._id in node.visible_contributor_ids if visible1 != visible2: node.set_visible(user=self, visible=True, log=True, auth=Auth(user=self)) else: node.add_contributor( contributor=self, permissions=node.get_permissions(user), visible=node.get_visible(user), log=False, ) try: node.remove_contributor( contributor=user, auth=Auth(user=self), log=False, ) except ValueError: logger.error('Contributor {0} not in list on node {1}'.format( user._id, node._id)) node.save() # - projects where the user was the creator for node in user.node__created: node.creator = self node.save() # finalize the merge remove_sessions_for_user(user) # - username is set to None so the resultant user can set it primary # in the future. user.username = None user.password = None user.verification_key = None user.merged_by = self user.save()
def merge_user(self, user): """Merge a registered user into this account. This user will be a contributor on any project. if the registered user and this account are both contributors of the same project. Then it will remove the registered user and set this account to the highest permission of the two and set this account to be visible if either of the two are visible on the project. :param user: A User object to be merged. """ # Fail if the other user has conflicts. if not user.can_be_merged: raise MergeConflictError("Users cannot be merged") # Move over the other user's attributes # TODO: confirm for system_tag in user.system_tags: if system_tag not in self.system_tags: self.system_tags.append(system_tag) self.is_claimed = self.is_claimed or user.is_claimed self.is_invited = self.is_invited or user.is_invited # copy over profile only if this user has no profile info if user.jobs and not self.jobs: self.jobs = user.jobs if user.schools and not self.schools: self.schools = user.schools if user.social and not self.social: self.social = user.social unclaimed = user.unclaimed_records.copy() unclaimed.update(self.unclaimed_records) self.unclaimed_records = unclaimed # - unclaimed records should be connected to only one user user.unclaimed_records = {} security_messages = user.security_messages.copy() security_messages.update(self.security_messages) self.security_messages = security_messages for key, value in user.mailchimp_mailing_lists.iteritems(): # subscribe to each list if either user was subscribed subscription = value or self.mailchimp_mailing_lists.get(key) signals.user_merged.send(self, list_name=key, subscription=subscription) # clear subscriptions for merged user signals.user_merged.send(user, list_name=key, subscription=False) for node_id, timestamp in user.comments_viewed_timestamp.iteritems(): if not self.comments_viewed_timestamp.get(node_id): self.comments_viewed_timestamp[node_id] = timestamp elif timestamp > self.comments_viewed_timestamp[node_id]: self.comments_viewed_timestamp[node_id] = timestamp self.emails.extend(user.emails) user.emails = [] for k, v in user.email_verifications.iteritems(): email_to_confirm = v['email'] if k not in self.email_verifications and email_to_confirm != user.username: self.email_verifications[k] = v user.email_verifications = {} # FOREIGN FIELDS for watched in user.watched: if watched not in self.watched: self.watched.append(watched) user.watched = [] for account in user.external_accounts: if account not in self.external_accounts: self.external_accounts.append(account) user.external_accounts = [] # - addons # Note: This must occur before the merged user is removed as a # contributor on the nodes, as an event hook is otherwise fired # which removes the credentials. for addon in user.get_addons(): user_settings = self.get_or_add_addon(addon.config.short_name) user_settings.merge(addon) user_settings.save() # - projects where the user was a contributor for node in user.node__contributed: # Skip dashboard node if node.is_dashboard: continue # if both accounts are contributor of the same project if node.is_contributor(self) and node.is_contributor(user): if node.permissions[user._id] > node.permissions[self._id]: permissions = node.permissions[user._id] else: permissions = node.permissions[self._id] node.set_permissions(user=self, permissions=permissions) visible1 = self._id in node.visible_contributor_ids visible2 = user._id in node.visible_contributor_ids if visible1 != visible2: node.set_visible(user=self, visible=True, log=True, auth=Auth(user=self)) else: node.add_contributor( contributor=self, permissions=node.get_permissions(user), visible=node.get_visible(user), log=False, ) try: node.remove_contributor( contributor=user, auth=Auth(user=self), log=False, ) except ValueError: logger.error('Contributor {0} not in list on node {1}'.format( user._id, node._id )) node.save() # - projects where the user was the creator for node in user.node__created: node.creator = self node.save() # finalize the merge remove_sessions_for_user(user) # - username is set to None so the resultant user can set it primary # in the future. user.username = None user.password = None user.verification_key = None user.osf_mailing_lists = {} user.merged_by = self user.save()
def merge_user(self, user): """Merge a registered user into this account. This user will be a contributor on any project. if the registered user and this account are both contributors of the same project. Then it will remove the registered user and set this account to the highest permission of the two and set this account to be visible if either of the two are visible on the project. :param user: A User object to be merged. """ # Fail if the other user has conflicts. if not user.can_be_merged: raise MergeConflictError('Users cannot be merged') # Move over the other user's attributes # TODO: confirm for system_tag in user.system_tags.all(): self.add_system_tag(system_tag) self.is_claimed = self.is_claimed or user.is_claimed self.is_invited = self.is_invited or user.is_invited # copy over profile only if this user has no profile info if user.jobs and not self.jobs: self.jobs = user.jobs if user.schools and not self.schools: self.schools = user.schools if user.social and not self.social: self.social = user.social unclaimed = user.unclaimed_records.copy() unclaimed.update(self.unclaimed_records) self.unclaimed_records = unclaimed # - unclaimed records should be connected to only one user user.unclaimed_records = {} security_messages = user.security_messages.copy() security_messages.update(self.security_messages) self.security_messages = security_messages notifications_configured = user.notifications_configured.copy() notifications_configured.update(self.notifications_configured) self.notifications_configured = notifications_configured if not website_settings.RUNNING_MIGRATION: for key, value in user.mailchimp_mailing_lists.iteritems(): # subscribe to each list if either user was subscribed subscription = value or self.mailchimp_mailing_lists.get(key) signals.user_merged.send(self, list_name=key, subscription=subscription) # clear subscriptions for merged user signals.user_merged.send(user, list_name=key, subscription=False, send_goodbye=False) for target_id, timestamp in user.comments_viewed_timestamp.iteritems(): if not self.comments_viewed_timestamp.get(target_id): self.comments_viewed_timestamp[target_id] = timestamp elif timestamp > self.comments_viewed_timestamp[target_id]: self.comments_viewed_timestamp[target_id] = timestamp self.emails.extend(user.emails) user.emails = [] for k, v in user.email_verifications.iteritems(): email_to_confirm = v['email'] if k not in self.email_verifications and email_to_confirm != user.username: self.email_verifications[k] = v user.email_verifications = {} self.affiliated_institutions.add( *user.affiliated_institutions.values_list('pk', flat=True)) for service in user.external_identity: for service_id in user.external_identity[service].iterkeys(): if not (service_id in self.external_identity.get(service, '') and self.external_identity[service][service_id] == 'VERIFIED'): # Prevent 'CREATE', merging user has already been created. external = user.external_identity[service][service_id] status = 'VERIFIED' if external == 'VERIFIED' else 'LINK' if self.external_identity.get(service): self.external_identity[service].update( {service_id: status}) else: self.external_identity[service] = {service_id: status} user.external_identity = {} # FOREIGN FIELDS WatchConfig.objects.filter(user=user).update(user=self) self.external_accounts.add( *user.external_accounts.values_list('pk', flat=True)) # - addons # Note: This must occur before the merged user is removed as a # contributor on the nodes, as an event hook is otherwise fired # which removes the credentials. for addon in user.get_addons(): user_settings = self.get_or_add_addon(addon.config.short_name) user_settings.merge(addon) user_settings.save() # - projects where the user was a contributor for node in user.contributed: # Skip bookmark collection node if node.is_bookmark_collection: continue # if both accounts are contributor of the same project if node.is_contributor(self) and node.is_contributor(user): user_permissions = node.get_permissions(user) self_permissions = node.get_permissions(self) permissions = max([user_permissions, self_permissions]) node.set_permissions(user=self, permissions=permissions) visible1 = self._id in node.visible_contributor_ids visible2 = user._id in node.visible_contributor_ids if visible1 != visible2: node.set_visible(user=self, visible=True, log=True, auth=Auth(user=self)) node.contributor_set.filter(user=user).delete() else: node.contributor_set.filter(user=user).update(user=self) node.save() # - projects where the user was the creator user.created.update(creator=self) # - file that the user has checked_out, import done here to prevent import error # TODO: Uncoment when StoredFileNode is implemented # from website.files.models.base import FileNode # for file_node in FileNode.files_checked_out(user=user): # file_node.checkout = self # file_node.save() # finalize the merge remove_sessions_for_user(user) # - username is set to the GUID so the merging user can set it primary # in the future (note: it cannot be set to None due to non-null constraint) user.set_unusable_username() user.set_unusable_password() user.verification_key = None user.osf_mailing_lists = {} user.merged_by = self user.save()