Beispiel #1
0
  def get(self):
    unsubscribe_cookie = self.get_argument('cookie', None)
    if unsubscribe_cookie is None:
      raise InvalidRequestError(MISSING_PARAMETER, name='unsubscribe_cookie')

    # Get information about the unsubscribe operation from the cookie (passed as query parameter).
    unsubscribe_dict = User.DecodeUnsubscribeCookie(unsubscribe_cookie)
    if unsubscribe_dict is None:
      # The signature doesn't match, so the secret has probably been changed.
      raise InvalidRequestError(EXPIRED_LINK)

    user_id = unsubscribe_dict['user_id']
    email_type = unsubscribe_dict['email_type']
    message = None

    if email_type == AccountSettings.EMAIL_ALERTS:
      logging.info('user %d unsubscribed from email alerts', user_id)
      settings = AccountSettings.CreateForUser(user_id, email_alerts=AccountSettings.EMAIL_NONE)
      message = 'You have successfully unsubscribed from Viewfinder Conversation notifications.'
    else:
      assert email_type == AccountSettings.MARKETING, unsubscribe_dict
      logging.info('user %d unsubscribed from marketing communication', user_id)
      settings = AccountSettings.CreateForUser(user_id, marketing=AccountSettings.MARKETING_NONE)
      message = 'You have successfully unsubscribed from Viewfinder announcements.'

    yield gen.Task(settings.Update, self._client)
    self.render('info.html',
                title='Sad to see you go!',
                message=message,
                button_url=None,
                button_text=None)
  def _Check(self):
    """Gathers pre-mutation information:
       1. Episode and photos to upload.
       2. Checkpoints list of episode and photo ids that need to be (re)created.
       3. Checkpoints whether to attempt to set episode location and placemark from photos.

       Validates the following:
       1. Permissions to upload to the given episode.
       2. Each photo can be uploaded into at most one episode.
    """
    # Get existing episode, if it exists.
    self._episode = yield gen.Task(Episode.Query,
                                   self._client,
                                   self._episode_id,
                                   None,
                                   must_exist=False)

    if self._episode is not None and self._episode.parent_ep_id != None:
      raise InvalidRequestError('Cannot upload photos into an episode that was saved.')

    # Query for all photos in a batch.
    photo_keys = [DBKey(ph_dict['photo_id'], None) for ph_dict in self._ph_dicts]
    photos = yield gen.Task(Photo.BatchQuery,
                            self._client,
                            photo_keys,
                            None,
                            must_exist=False)

    # Start populating the checkpoint if this the first time the operation has been run.
    if self._op.checkpoint is None:
      # Gather list of ids of new episode and photos that need to be created.
      self._new_ids = set()
      if self._episode is None:
        self._new_ids.add(self._episode_id)

      for photo, ph_dict in zip(photos, self._ph_dicts):
        if photo is None:
          self._new_ids.add(ph_dict['photo_id'])
        elif photo.episode_id != self._episode_id:
          raise InvalidRequestError('Cannot upload photo "%s" into multiple episodes.' % ph_dict['photo_id'])

      # Determine whether episode location/placemark needs to be set.
      self._set_location = self._episode is None or self._episode.location is None
      self._set_placemark = self._episode is None or self._episode.placemark is None

      # Set checkpoint.
      # List of new episode/photo ids, and whether to set location/placemark need to be check-
      # pointed because they may change in the UPDATE phase. If we fail after UPDATE, but
      # before NOTIFY, we would not send correct notifications on retry.
      checkpoint = {'new': list(self._new_ids),
                    'location': self._set_location,
                    'placemark': self._set_placemark}
      yield self._op.SetCheckpoint(self._client, checkpoint)
    else:
      # Restore state from checkpoint.
      self._new_ids = set(self._op.checkpoint['new'])
      self._set_location = self._op.checkpoint['location']
      self._set_placemark = self._op.checkpoint['placemark']
Beispiel #3
0
    def QueryIfVisible(cls,
                       client,
                       user_id,
                       episode_id,
                       must_exist=True,
                       consistent_read=False):
        """If the user has viewing rights to the specified episode, returns that episode, otherwise
    returns None. The user has viewing rights if the user is a follower of the episode's
    viewpoint. If must_exist is true and the episode does not exist, raises an InvalidRequest
    exception.
    """
        episode = yield gen.Task(Episode.Query,
                                 client,
                                 episode_id,
                                 None,
                                 must_exist=False,
                                 consistent_read=consistent_read)

        if episode is None:
            if must_exist == True:
                raise InvalidRequestError('Episode "%s" does not exist.' %
                                          episode_id)
        else:
            follower = yield gen.Task(Follower.Query,
                                      client,
                                      user_id,
                                      episode.viewpoint_id,
                                      col_names=None,
                                      must_exist=False)
            if follower is None or not follower.CanViewContent():
                raise gen.Return(None)

        raise gen.Return(episode)
Beispiel #4
0
    def _UpdateEpisode(self):
        """Orchestrates the update_episode operation by executing each of the phases in turn."""
        # Get the viewpoint_id from the episode (which must exist).
        self._episode = yield gen.Task(Episode.Query,
                                       self._client,
                                       self._episode_id,
                                       None,
                                       must_exist=False)
        if not self._episode:
            raise InvalidRequestError(
                'Episode "%s" does not exist and so cannot be updated.' %
                self._episode_id)
        self._viewpoint_id = self._episode.viewpoint_id

        lock = yield gen.Task(Viewpoint.AcquireLock, self._client,
                              self._viewpoint_id)
        try:
            yield self._Check()
            self._client.CheckDBNotModified()
            yield self._Update()
            yield self._Account()
            yield Operation.TriggerFailpoint(self._client)
            yield self._Notify()
        finally:
            yield gen.Task(Viewpoint.ReleaseLock, self._client,
                           self._viewpoint_id, lock)
Beispiel #5
0
    def VerifyConfirmedIdentity(cls, client, identity_key, access_token):
        """Verifies that the specified access token matches the one stored in the identity. If
    this is the case, then the caller has confirmed control of the identity. Returns the
    identity DB object if so, else raises a permission exception.
    """
        # Validate the identity and access token.
        Identity.ValidateKey(identity_key)
        identity = yield gen.Task(Identity.Query,
                                  client,
                                  identity_key,
                                  None,
                                  must_exist=False)
        if identity is None:
            raise InvalidRequestError(BAD_IDENTITY, identity_key=identity_key)

        yield identity.VerifyAccessToken(client, access_token)

        # Expire the access token now that it has been used.
        identity.expires = 0

        # Reset auth throttling limit since access token has been successfully redeemed.
        identity.auth_throttle = None

        identity.Update(client)

        raise gen.Return(identity)
    def post(self):
        """POST is used when authenticating via the mobile application."""
        yield gen.Task(self._StartJSONRequest, 'verify', self.request,
                       json_schema.VERIFY_VIEWFINDER_REQUEST)

        # Validate the identity and access token.
        identity = yield Identity.VerifyConfirmedIdentity(
            self._client, self._request_message.dict['identity'],
            self._request_message.dict['access_token'])

        # Get the ShortURL associated with the access token.
        group_id = identity.json_attrs['group_id']
        random_key = identity.json_attrs['random_key']
        short_url = yield gen.Task(ShortURL.Query, self._client, group_id,
                                   random_key, None)

        # Extract parameters that shouldn't be passed to handler.
        json = short_url.json
        self._action = json.pop('action')
        json.pop('identity_key')
        json.pop('user_name')
        json.pop('access_token')

        # If there is no verification handler, then token was not intended to be redeemed via
        # /verify/viewfinder.
        handler = VerifyIdBaseHandler.ACTION_MAP[self._action].handler
        if handler is None:
            raise InvalidRequestError(INVALID_VERIFY_VIEWFINDER,
                                      action=self._action)

        # Invoke the action handler.
        handler(self, self._client, **json)
Beispiel #7
0
    def _CheckCopySources(cls, action, client, user_id, source_ep_dicts):
        """Ensures that the sharer or saver has access to the source episodes and that the source
    photos are part of the source episodes. Caller is expected to check permission to add to
    the given viewpoint.

    Returns a list of the source episodes and posts in the form of (episode, posts) tuples.
    """
        # Gather list of (episode_id, photo_ids) tuples and check for duplicate posts.
        unique_keys = set()
        ep_ph_ids_list = []
        for ep_dict in source_ep_dicts:
            ph_ids = []
            for photo_id in ep_dict['photo_ids']:
                db_key = (ep_dict['new_episode_id'], photo_id)
                if db_key in unique_keys:
                    raise InvalidRequestError(
                        'Photo "%s" cannot be %sd into episode "%s" more than once in same request.'
                        % (photo_id, action, ep_dict['new_episode_id']))
                unique_keys.add(db_key)
                ph_ids.append(photo_id)

            ep_ph_ids_list.append((ep_dict['existing_episode_id'], ph_ids))

        ep_posts_list = yield ViewfinderOperation._CheckEpisodePostAccess(
            action, client, user_id, ep_ph_ids_list)
        raise gen.Return(ep_posts_list)
Beispiel #8
0
    def CanonicalizePhone(cls, phone):
        """Given an arbitrary string, validates that it is in the expected E.164 phone number
    format. Since E.164 phone numbers are already canonical, there is no additional
    normalization step to take. Returns the valid, canonical phone number in E.164 format.
    """
        if not phone:
            raise InvalidRequestError('Phone number cannot be empty.')

        if phone[0] != '+':
            raise InvalidRequestError(
                'Phone number "%s" is not in E.164 format.' % phone)

        try:
            phone_num = phonenumbers.parse(phone)
        except phonenumbers.phonenumberutil.NumberParseException:
            raise InvalidRequestError('"%s" is not a phone number.' % phone)

        if not phonenumbers.is_possible_number(phone_num):
            raise InvalidRequestError('"%s" is not a possible phone number.' %
                                      phone)

        return phone
Beispiel #9
0
 def GetDescription(cls, identity_key):
     """Returns a description of the specified identity key suitable for UI display."""
     identity_type, value = Identity.SplitKey(identity_key)
     if identity_type == 'Email':
         return value
     elif identity_type == 'FacebookGraph':
         return 'your Facebook account'
     elif identity_type == 'Phone':
         phone = phonenumbers.parse(value)
         return phonenumbers.format_number(
             phone, phonenumbers.PhoneNumberFormat.INTERNATIONAL)
     elif identity_type == 'Local':
         return 'local identity'
     raise InvalidRequestError('Scheme for identity %s unknown.' %
                               identity_key)
Beispiel #10
0
def GeneratePasswordHash(password):
    """Generates a password hash from the given password str, using a newly generated salt.

  Returns a tuple: (pwd_hash, salt).
  """
    # Ensure that password is at least 8 bytes long.
    if len(password) < 8:
        raise InvalidRequestError(_PASSWORD_TOO_SHORT)

    # The salt value is 16 bytes according to the official recommendation:
    # http://csrc.nist.gov/publications/nistpubs/800-132/nist-sp800-132.pdf
    salt = base64.b64encode(os.urandom(16))

    # Generate the password hash and return it + the salt.
    pwd_hash = HashPassword(password, salt)
    return (pwd_hash, salt)
Beispiel #11
0
    def Canonicalize(cls, identity_key):
        """Returns the canonical form of the given identity key."""
        for prefix in ['Email:', 'Phone:', 'FacebookGraph:', 'Local:', 'VF:']:
            if identity_key.startswith(prefix):
                value = identity_key[len(prefix):]
                if prefix == 'Email:':
                    canonical_value = Identity.CanonicalizeEmail(value)
                    if value is not canonical_value:
                        identity_key = prefix + canonical_value
                elif prefix == 'Phone:':
                    canonical_value = Identity.CanonicalizePhone(value)
                    if value is not canonical_value:
                        identity_key = prefix + canonical_value

                # Valid prefix, but no canonicalization necessary.
                return identity_key

        raise InvalidRequestError('Scheme for identity %s unknown.' %
                                  identity_key)
Beispiel #12
0
 def CheckCreate(cls, client, **ph_dict):
     """For a photo that already exists, check that its attributes match.
 Return: photo, if it already exists.
 """
     assert 'photo_id' in ph_dict and 'user_id' in ph_dict, ph_dict
     photo = yield Photo.Query(client,
                               ph_dict['photo_id'],
                               None,
                               must_exist=False)
     # All attributes should match between the ph_dict and persisted photo metadata
     # (except those allowed to be different).
     if photo is not None and \
         photo.HasMismatchedValues(Photo.PHOTO_CREATE_ATTRIBUTE_UPDATE_ALLOWED_SET, **ph_dict):
         logging.warning(
             'Photo.CheckCreate: keyword mismatch failure: %s, %s' %
             (photo, ph_dict))
         raise InvalidRequestError(
             'There is a mismatch between request and persisted photo metadata during photo '
             'metadata creation.')
     raise gen.Return(photo)
Beispiel #13
0
    def _CheckUpdate(cls, client, **ph_dict):
        """Checks that the photo exists.  Checks photo metadata object against the provided dictionary.
    Checks that the user_id in the dictionary matches the one on the photo.
    Returns: photo
    """
        assert 'photo_id' in ph_dict and 'user_id' in ph_dict, ph_dict
        photo = yield Photo.Query(client,
                                  ph_dict['photo_id'],
                                  None,
                                  must_exist=False)

        if photo is None:
            raise InvalidRequestError(
                'Photo "%s" does not exist and so cannot be updated.' %
                ph_dict['photo_id'])

        if photo.user_id != ph_dict['user_id']:
            raise PermissionError(
                'User id of photo does not match requesting user')

        raise gen.Return(photo)
Beispiel #14
0
def VerifyAssetId(client, user_id, device_id, prefix_id, asset_id,
                  has_timestamp):
    """Verifies that "asset_id" conforms to the following requirements:
     1. The asset prefix must match "prefix_id" and the asset id's format must be valid.
     2. The embedded device_id must match "device_id", or must match another device owned by
        "user_id". A device can only create assets with ids that match itself.
     3. The asset_id's uniquifier cannot include a server_id part. Only the server can create
        uniquifiers with this part.
  """
    try:
        asset_name = IdPrefix.GetAssetName(prefix_id).lower()
        if has_timestamp:
            truncated_ts, embedded_device_id, uniquifier = DeconstructTimestampAssetId(
                prefix_id, asset_id)
        else:
            embedded_device_id, uniquifier = DeconstructAssetId(
                prefix_id, asset_id)
    except:
        raise InvalidRequestError('%s id "%s" does not have a valid format.' %
                                  (asset_name, asset_id))

    if embedded_device_id != device_id:
        # Query the database to see if the client owns the embedded device id.
        device = yield gen.Task(Device.Query,
                                client,
                                user_id,
                                embedded_device_id,
                                None,
                                must_exist=False)
        if device is None:
            raise PermissionError(
                'User %d and device %d do not have permission to create %s "%s".'
                % (user_id, device_id, asset_name, asset_id))

    if uniquifier.server_id is not None:
        raise PermissionError(
            'Clients do not have permission to create %s "%s".' %
            (asset_name, asset_id))
Beispiel #15
0
        def _ProcessEpisode(ep_dict):
            """Makes several permission and validation checks and initiates the gathering of unshare
      information via traversal of the sharing tree.
      """
            episode, posts = yield [
                gen.Task(Episode.QueryIfVisible, self._client,
                         self._unsharer_id, ep_dict['episode_id']),
                gen.Task(_QueryPosts, ep_dict['episode_id'],
                         ep_dict['photo_ids'])
            ]

            # Validate that the episode is visible to the caller.
            if episode is None or episode.user_id != self._unsharer_id:
                raise PermissionError(
                    'User %d does not have permission to unshare photos from episode "%s".'
                    % (self._unsharer_id, ep_dict['episode_id']))

            # Validate that the episode is in the requested viewpoint.
            if episode.viewpoint_id != self._viewpoint_id:
                raise InvalidRequestError(
                    'Episode "%s" is not in viewpoint "%s".' %
                    (episode.episode_id, self._viewpoint_id))

            # Validate that the episode was shared no more than N days ago.
            if now - episode.publish_timestamp > Photo.CLAWBACK_GRACE_PERIOD:
                days = Photo.CLAWBACK_GRACE_PERIOD / constants.SECONDS_PER_DAY
                raise PermissionError(
                    'Photos from episode "%s" cannot be unshared because they were shared '
                    'more than %d days ago.' % (episode.episode_id, days))

            # Validate that each photo is in the requested episode.
            for photo_id, post in zip(ep_dict['photo_ids'], posts):
                if post is None:
                    raise PermissionError(
                        'Photo "%s" is not in episode "%s".' %
                        (photo_id, ep_dict['episode_id']))

            yield self._GatherUnshares(episode, posts)
Beispiel #16
0
 def ValidateKey(cls, identity_key):
     """Validates that the identity key has a valid format and is canonicalized."""
     if Identity.Canonicalize(identity_key) != identity_key:
         raise InvalidRequestError('Identity %s is not in canonical form.' %
                                   identity_key)
Beispiel #17
0
    def SendVerifyIdMessage(cls, client, action, use_short_token,
                            is_mobile_app, identity_key, user_id, user_name,
                            **kwargs):
        """Sends a verification email or SMS message to the given identity. This message may
    directly contain an access code (e.g. if an SMS is sent), or it may contain a ShortURL
    link to a page which reveals the access code (e.g. if email was triggered by the mobile
    app). Or it may contain a link to a page which confirms the user's password and redirects
    them to the web site (e.g. if email was triggered by the web site).
    """
        # Ensure that identity exists.
        identity = yield gen.Task(Identity.Query,
                                  client,
                                  identity_key,
                                  None,
                                  must_exist=False)
        if identity is None:
            identity = Identity.CreateFromKeywords(key=identity_key)
            yield gen.Task(identity.Update, client)

        identity_type, identity_value = Identity.SplitKey(identity.key)
        message_type = 'emails' if identity_type == 'Email' else 'messages'

        # Throttle the rate at which email/SMS messages can be sent to this identity. The updated
        # count will be saved by CreateAccessTokenURL.
        auth_throttle = identity.auth_throttle or {}

        per_min_dict, is_throttled = util.ThrottleRate(
            auth_throttle.get('per_min',
                              None), VerifyIdBaseHandler._MAX_MESSAGES_PER_MIN,
            constants.SECONDS_PER_MINUTE)
        if is_throttled:
            # Bug 485: Silently do not send the email if throttled. We don't want to give user error
            #          if they exit out of confirm code screen, then re-create account, etc.
            return

        per_day_dict, is_throttled = util.ThrottleRate(
            auth_throttle.get('per_day',
                              None), VerifyIdBaseHandler._MAX_MESSAGES_PER_DAY,
            constants.SECONDS_PER_DAY)
        if is_throttled:
            raise InvalidRequestError(TOO_MANY_MESSAGES_DAY,
                                      message_type=message_type,
                                      identity_value=Identity.GetDescription(
                                          identity.key))

        identity.auth_throttle = {
            'per_min': per_min_dict,
            'per_day': per_day_dict
        }

        # Create a ShortURL link that will supply the access token to the user when clicked.
        # Use a URL path like "idm/*" for the mobile app, and "idw/*" for the web.
        encoded_user_id = base64hex.B64HexEncode(
            util.EncodeVarLengthNumber(user_id), padding=False)
        group_id = '%s/%s' % ('idm' if is_mobile_app else 'idw',
                              encoded_user_id)
        short_url = yield gen.Task(identity.CreateAccessTokenURL,
                                   client,
                                   group_id,
                                   use_short_token=use_short_token,
                                   action=action,
                                   identity_key=identity.key,
                                   user_name=user_name,
                                   **kwargs)

        # Send email/SMS in order to verify that the user controls the identity.
        if identity_type == 'Email':
            args = VerifyIdBaseHandler._GetAuthEmail(client, action,
                                                     use_short_token,
                                                     user_name, identity,
                                                     short_url)
            yield gen.Task(EmailManager.Instance().SendEmail,
                           description=action,
                           **args)
        else:
            args = VerifyIdBaseHandler._GetAccessTokenSms(identity)
            yield gen.Task(SMSManager.Instance().SendSMS,
                           description=action,
                           **args)

        # In dev servers, display a popup with the generated code (OS X 10.9-only).
        if (options.options.localdb and platform.system() == 'Darwin'
                and platform.mac_ver()[0] == '10.9'):
            subprocess.call([
                'osascript', '-e',
                'display notification "%s" with title "Viewfinder"' %
                identity.access_token
            ])
    def _Check(self):
        """Check and prepare for remove.
    Along with checks, builds two lists for the Update phase.
    1) self._contacts_to_delete: list of contacts which have been superseded by more recent
        contacts with the same contact_id and because they no longer serve any purpose should
        be deleted.  These may exist due to lack of transactional atomicity when replacing
        a contact as part of removal or upload.  If too many removed contacts are or will exist, this
        list will also contain all of the removed contacts as well as any contacts that are
        part of the removal request.
    2) self._contacts_to_remove: list of contacts which should be transitioned to 'removed', but
        currently are not in the 'removed' state.
    """
        # Check for well formed input and build dict of all contacts that have been requested for removal.
        for request_contact_id in self._request_contact_ids:
            if Contact.GetContactSourceFromContactId(
                    request_contact_id) not in Contact.UPLOAD_SOURCES:
                # remove_contacts only allows removal of Manual and iPhone sourced contacts.
                raise InvalidRequestError(
                    BAD_CONTACT_SOURCE,
                    Contact.GetContactSourceFromContactId(request_contact_id))

        # Get existing contacts along with list of contacts that should be deleted as part of dedup.
        existing_contacts_dict, self._contacts_to_delete = \
            yield RemoveContactsOperation._GetAllContactsWithDedup(self._client, self._user_id)

        # Projected count of removed contacts after this operation.
        removed_contact_count = 0
        # Build list of contacts to be removed.
        for existing_contact in existing_contacts_dict.itervalues():
            if existing_contact.IsRemoved():
                removed_contact_count += 1
            elif existing_contact.contact_id in self._request_contact_ids:
                # Not already in removed state, so add to list of contacts to remove during update stage.
                self._contacts_to_remove.append(existing_contact)
                removed_contact_count += 1
            # else case is no-op because there's no match of existing present contact and one being requested for removal.

        if removed_contact_count >= Contact.MAX_REMOVED_CONTACTS_LIMIT:
            # Trigger removed contact reset (garbage collection).
            self._removed_contacts_reset = True

        if self._op.checkpoint is None:
            # Set checkpoint.
            # Checkpoint whether or not we decided to reset the removed contacts.
            yield self._op.SetCheckpoint(
                self._client,
                {'removed_contacts_reset': self._removed_contacts_reset})
        elif not self._removed_contacts_reset:
            # Even if we didn't decide during this operation retry to reset the removed contacts, we still need to do it
            #   if we previously decided to because we may have already deleted some of the removed contacts and need to
            #   indicate the removed contact reset in the notification.
            self._removed_contacts_reset = self._op.checkpoint[
                'removed_contacts_reset']

        if self._removed_contacts_reset:
            # Because we've decided to reset the contacts, we'll add more contacts to the delete list:
            #  * Any current contact that's in the 'removed' state.
            #  * Any current non-removed contact that was in the list of contacts to remove.
            for existing_contact in existing_contacts_dict.itervalues():
                if existing_contact.IsRemoved(
                ) or existing_contact.contact_id in self._request_contact_ids:
                    self._contacts_to_delete.append(existing_contact)
Beispiel #19
0
    def _ResolveContactIds(self, contact_dicts):
        """Examines each contact in "contact_dicts" (in the CONTACT_METADATA format). Returns a
    list of the same length containing the (True, user_id, webapp_dev_id) of the contact if
    it is already a Viewfinder user, or allocates new user and web device ids, and returns the
    tuple (False, user_id, webapp_dev_id).

    Raises an InvalidRequestError if any of the user ids do not correspond to real users.
    """
        # Get identity objects for all contacts for which no user_id is given.
        identity_keys = [
            DBKey(contact_dict['identity'], None)
            for contact_dict in contact_dicts if 'user_id' not in contact_dict
        ]

        identities = yield gen.Task(Identity.BatchQuery,
                                    self._client,
                                    identity_keys,
                                    None,
                                    must_exist=False)

        # Get user objects for all contacts with a user_id given, or if identity is already bound.
        user_keys = []
        ident_iter = iter(identities)
        for contact_dict in contact_dicts:
            if 'user_id' in contact_dict:
                user_keys.append(DBKey(contact_dict['user_id'], None))
            else:
                identity = next(ident_iter)
                if identity is not None and identity.user_id is not None:
                    user_keys.append(DBKey(identity.user_id, None))

        users = yield gen.Task(User.BatchQuery,
                               self._client,
                               user_keys,
                               None,
                               must_exist=False)

        # Construct result tuples; if a user does not exist, allocate a user_id and webapp_dev_id.
        results = []
        ident_iter = iter(identities)
        user_iter = iter(users)
        for contact_dict in contact_dicts:
            if 'user_id' not in contact_dict:
                identity = next(ident_iter)
                if identity is None or identity.user_id is None:
                    # User doesn't yet exist, so allocate new user and web device ids.
                    user_id, webapp_dev_id = yield User.AllocateUserAndWebDeviceIds(
                        self._client)
                    results.append((False, user_id, webapp_dev_id))
                    continue

            user = next(user_iter)
            if user is None:
                assert 'user_id' in contact_dict, contact_dict
                raise InvalidRequestError(
                    'A user with id %d cannot be found.' %
                    contact_dict['user_id'])

            # User already exists.
            results.append((True, user.user_id, user.webapp_dev_id))

        raise gen.Return(results)
Beispiel #20
0
    def _CheckEpisodePostAccess(cls, action, client, user_id, ep_ph_ids_list):
        """Ensures that given user has access to the set of episodes and photos in "ep_ph_ids_list",
    which is a list of (episode_id, photo_ids) tuples.

    Returns list of (episode, posts) tuples that corresponds to "ep_ph_ids_list".
    """
        # Gather db keys for all source episodes and posts, and check for duplicate episodes and photos.
        episode_keys = []
        post_keys = []
        for episode_id, photo_ids in ep_ph_ids_list:
            episode_keys.append(DBKey(episode_id, None))
            for photo_id in photo_ids:
                post_keys.append(DBKey(episode_id, photo_id))

        # Query for all episodes and posts in parallel and in batches.
        episodes, posts = yield [
            gen.Task(Episode.BatchQuery,
                     client,
                     episode_keys,
                     None,
                     must_exist=False),
            gen.Task(Post.BatchQuery,
                     client,
                     post_keys,
                     None,
                     must_exist=False)
        ]

        # Check that user has ability to access all source episodes and posts.
        ep_posts_list = []
        posts_iter = iter(posts)
        for (episode_id, photo_ids), episode in zip(ep_ph_ids_list, episodes):
            if episode is None:
                raise InvalidRequestError('Episode "%s" does not exist.' %
                                          episode_id)

            posts_list = []
            for photo_id in photo_ids:
                post = next(posts_iter)
                if post is None:
                    raise InvalidRequestError(
                        'Photo "%s" does not exist or is not in episode "%s".'
                        % (photo_id, episode_id))

                # Do not raise error if removing a photo that has already been unshared or removed.
                if action != 'remove':
                    if post.IsUnshared():
                        raise PermissionError(
                            'Cannot %s photo "%s", because it was unshared.' %
                            (action, photo_id))

                    if post.IsRemoved():
                        raise PermissionError(
                            'Cannot %s photo "%s", because it was removed.' %
                            (action, photo_id))

                posts_list.append(post)

            ep_posts_list.append((episode, posts_list))

        # Query for followers of all unique source viewpoints in parallel and in a batch.
        follower_keys = {
            episode.viewpoint_id: DBKey(user_id, episode.viewpoint_id)
            for episode in episodes
        }
        followers = yield gen.Task(Follower.BatchQuery,
                                   client,
                                   follower_keys.values(),
                                   None,
                                   must_exist=False)

        # Get set of all viewpoints that are accessible to this user.
        allowed_viewpoint_ids = set(
            follower.viewpoint_id for follower in followers
            if follower is not None and follower.CanViewContent())

        # Check access permission to the source viewpoints.
        for episode in episodes:
            if episode.viewpoint_id not in allowed_viewpoint_ids:
                raise PermissionError(
                    'User %d does not have permission to %s episode "%s".' %
                    (user_id, action, episode.episode_id))

        raise gen.Return(ep_posts_list)
    def _HandleGet(self,
                   short_url,
                   identity_key,
                   viewpoint_id,
                   default_url,
                   is_sms=False,
                   is_first_click=True):
        """Invoked when a user follows a prospective user invitation URL. Sets a prospective user
    cookie that identifies the user and restricts access to a single viewpoint. Typically
    redirects the user to the corresponding website conversation page.
    """
        identity = yield gen.Task(Identity.Query,
                                  self._client,
                                  identity_key,
                                  None,
                                  must_exist=False)

        # Check for rare case where the identity has been unlinked since issuing the prospective user link.
        if identity is None or identity.user_id is None:
            raise ExpiredError(
                'The requested link has expired and can no longer be used.')

        # If the "next" query argument is specified, redirect to that, otherwise fall back on default_url.
        next_url = self.get_argument('next', default_url)
        if urlparse.urlparse(next_url).hostname is not None:
            raise InvalidRequestError('Cannot redirect to absolute URL: %s' %
                                      next_url)

        # Detect photo store redirect, as we should not set a cookie or return redirection to photo store in this case.
        photostore_re = re.match(r'.*/episodes/(.*)/photos/(.*)(\..)',
                                 next_url)

        # If the link was sent via SMS, then reset the SMS alert count (since the link was followed).
        if is_sms:
            settings = AccountSettings.CreateForUser(identity.user_id,
                                                     sms_count=0)
            yield gen.Task(settings.Update, self._client)

        # A registered user can no longer use prospective user links.
        user = yield gen.Task(User.Query, self._client, identity.user_id, None)
        if user.IsRegistered():
            # If not already logged in with the same user with full access, redirect to the auth page.
            context = ViewfinderContext.current()
            if context is None:
                current_user = None
                current_viewpoint_id = None
            else:
                current_user = context.user
                current_viewpoint_id = context.viewpoint_id

            if current_user is None or current_user.user_id != identity.user_id or current_viewpoint_id is not None:
                self.ClearUserCookie()
                self.redirect('/auth?%s' % urlencode(dict(next=next_url)))
                return
        else:
            # If this is the first time the link was clicked, then create a confirmed cookie.
            if is_first_click:
                confirm_time = util.GetCurrentTimestamp()

                # Update is_first_click.
                new_json = deepcopy(short_url.json)
                new_json['is_first_click'] = False
                short_url.json = new_json
                yield gen.Task(short_url.Update, self._client)
            else:
                confirm_time = None

            # Set the prospective user cookie. Make it a session cookie so that it will go away when
            # browser is closed.
            user_cookie_dict = self.CreateUserCookieDict(
                user.user_id,
                user.webapp_dev_id,
                user_name=user.name,
                viewpoint_id=viewpoint_id,
                confirm_time=confirm_time,
                is_session_cookie=True)

            # Do not set the user cookie if this is a photo view request.
            self.LoginUser(user,
                           user_cookie_dict,
                           set_cookie=photostore_re is None)

        # Handle photostore redirect request internally rather than returning the redirect to the
        # client. Email clients do not keep cookies, so it is not possible to redirect to an
        # authenticated URL.
        if photostore_re:
            episode_id = photostore_re.group(1)
            photo_id = photostore_re.group(2)
            suffix = photostore_re.group(3)
            next_url = yield PhotoStoreHandler.GetPhotoUrl(
                self._client, self._obj_store, episode_id, photo_id, suffix)

        # Redirect to the URL of the next page.
        self.redirect(next_url)
Beispiel #22
0
    def _Check(self):
        """Gathers pre-mutation information:
       1. Queries for existing followers and viewpoint.
       2. Checkpoints list of followers that need to be revived.

       Validates the following:
       1. Permission to update viewpoint metadata.
    """
        # Get the viewpoint to be modified, along with the follower that is adding the additional users.
        # This state will not be changed by add followers, and so doesn't need to be part of the checkpoint.
        self._viewpoint, self._follower = yield gen.Task(
            Viewpoint.QueryWithFollower, self._client, self._user_id,
            self._viewpoint_id)

        if self._viewpoint is None:
            raise InvalidRequestError(
                'Viewpoint "%s" does not exist and so cannot be updated.' %
                (self._viewpoint_id))

        if self._follower is None or not self._follower.CanContribute():
            raise PermissionError(
                'User %d does not have permission to update viewpoint "%s".' %
                (self._user_id, self._viewpoint_id))

        # Get all existing followers.
        self._followers, _ = yield gen.Task(Viewpoint.QueryFollowers,
                                            self._client,
                                            self._viewpoint_id,
                                            limit=Viewpoint.MAX_FOLLOWERS)

        # Check that cover photo exists in this viewpoint.
        cover_photo = self._vp_dict.get('cover_photo', None)
        if cover_photo is not None:
            if self._viewpoint.IsDefault():
                # cover_photo isn't supported creating default viewpoint.
                raise InvalidRequestError(
                    'A cover photo cannot be set on your library.')

            cover_photo_episode, cover_photo_post = yield [
                gen.Task(Episode.Query,
                         self._client,
                         cover_photo['episode_id'],
                         None,
                         must_exist=False),
                gen.Task(Post.Query,
                         self._client,
                         cover_photo['episode_id'],
                         cover_photo['photo_id'],
                         None,
                         must_exist=False)
            ]

            if cover_photo_post is None:
                raise InvalidRequestError(
                    'The requested cover photo does not exist.')

            if cover_photo_post.IsUnshared():
                raise PermissionError(
                    'The requested cover photo has been unshared.')

            if cover_photo_episode.viewpoint_id != self._viewpoint_id:
                raise InvalidRequestError(
                    'The requested cover photo is not in viewpoint "%s".' %
                    self._viewpoint_id)

        # Start populating the checkpoint if this the first time the operation has been run.
        if self._op.checkpoint is None:
            # Get list of followers which have removed themselves from the viewpoint and will need to be revived.
            self._revive_follower_ids = self._GetRevivableFollowers(
                self._followers)

            # Get previous values of title and cover_photo, if they are being updated.
            self._prev_values = {}
            if 'title' in self._vp_dict:
                util.SetIfNotNone(self._prev_values, 'prev_title',
                                  self._viewpoint.title)
            if 'cover_photo' in self._vp_dict:
                util.SetIfNotNone(self._prev_values, 'prev_cover_photo',
                                  self._viewpoint.cover_photo)

            # Set checkpoint.
            # Followers to revive need to be check-pointed because they are changed in the UPDATE phase.
            # If we fail after UPDATE, but before NOTIFY, we would not send correct notifications on retry.
            checkpoint = {'revive': self._revive_follower_ids}
            util.SetIfNotEmpty(checkpoint, 'prev', self._prev_values)
            yield self._op.SetCheckpoint(self._client, checkpoint)
        else:
            # Restore state from checkpoint.
            self._revive_follower_ids = self._op.checkpoint['revive']
            self._prev_values = self._op.checkpoint.get('prev', {})
Beispiel #23
0
    def _Check(self):
        """Gathers pre-mutation information:
       1. Checkpoints list of contacts that need to be made prospective users.
       2. Cover photo, if not specified.

       Validates the following:
       1. Max follower limit.
       2. Permissions to share from source episodes.
       3. Permission to create new viewpoint.
       4. Cover photo is contained in the request.

    Returns True if all checks succeeded and operation execution should continue, or False
    if the operation should end immediately.
    """
        if len(
                self._contact_dicts
        ) + 1 > Viewpoint.MAX_FOLLOWERS:  # +1 to account for user creating viewpoint.
            raise LimitExceededError(
                'User %d attempted to exceed follower limit on viewpoint "%s" by creating a viewpoint with %d followers.'
                % (self._user_id, self._viewpoint_id,
                   len(self._contact_dicts) + 1))

        # Validate source episodes and posts.
        source_ep_posts_list = yield ViewfinderOperation._CheckCopySources(
            'share', self._client, self._user_id, self._ep_dicts)

        # Get dicts describing the target episodes and posts.
        target_ep_ids = [
            ep_dict['new_episode_id'] for ep_dict in self._ep_dicts
        ]
        self._new_ep_dicts = ViewfinderOperation._CreateCopyTargetDicts(
            self._op.timestamp, self._user_id, self._viewpoint_id,
            source_ep_posts_list, target_ep_ids)

        # Does request explicitly set a cover photo?
        if self._vp_dict.has_key('cover_photo'):
            if self._vp_dict['type'] == Viewpoint.DEFAULT:
                # cover_photo isn't supported creating default viewpoint.
                raise InvalidRequestError(
                    'cover_photo is invalid in share_new request for default viewpoint.'
                )
            # Make sure the designated cover photo is contained in the request.
            elif not Viewpoint.IsCoverPhotoContainedInEpDicts(
                    self._vp_dict['cover_photo']['episode_id'],
                    self._vp_dict['cover_photo']['photo_id'],
                    self._new_ep_dicts):
                logging.warning(
                    'cover_photo is specified but not contained in request: vp_dict: %s, ep_dicts: %s',
                    self._vp_dict, self._ep_dicts)
                raise InvalidRequestError(
                    'cover_photo is specified but not contained in request.')
        else:
            # Select cover photo from the set being shared.
            self._vp_dict[
                'cover_photo'] = Viewpoint.SelectCoverPhotoFromEpDicts(
                    self._new_ep_dicts)

        # Start populating the checkpoint if this the first time the operation has been run.
        if self._op.checkpoint is None:
            # If viewpoint already exists, then just warn and do nothing. We do not raise an error
            # because sometimes the client resubmits the same operation with different ids.
            viewpoint = yield gen.Task(Viewpoint.Query,
                                       self._client,
                                       self._viewpoint_id,
                                       None,
                                       must_exist=False)
            if viewpoint is not None:
                logging.warning('target viewpoint "%s" already exists',
                                self._viewpoint_id)
                raise gen.Return(False)

            # Get a tuple for each contact: (user_exists?, user_id, webapp_dev_id).
            self._contact_ids = yield self._ResolveContactIds(
                self._contact_dicts)

            # Set checkpoint.
            # List of contacts need to be check-pointed because it may change in the UPDATE phase (when contacts
            # can be bound to prospective users). If we fail after UPDATE, but before NOTIFY, we would not send
            # correct notifications on retry.
            checkpoint = {'contacts': self._contact_ids}
            yield self._op.SetCheckpoint(self._client, checkpoint)
        else:
            # Restore state from checkpoint.
            self._contact_ids = self._op.checkpoint['contacts']

        self._contact_user_ids = [
            user_id
            for user_exists, user_id, webapp_dev_id in self._contact_ids
        ]

        raise gen.Return(True)
Beispiel #24
0
    def _CheckCopyTargets(cls, action, client, user_id, viewpoint_id,
                          target_ep_dicts):
        """Compiles a list of target episode and post ids that do not exist or are removed. These
    episodes and posts will not be copied as part of the operation.

    Returns the set of target episode and post ids that will be (re)created by the caller.
    """
        # Gather db keys for all target episodes and posts.
        episode_keys = []
        post_keys = []
        for ep_dict in target_ep_dicts:
            episode_keys.append(DBKey(ep_dict['episode_id'], None))
            for photo_id in ep_dict['photo_ids']:
                post_keys.append(DBKey(ep_dict['episode_id'], photo_id))

        # Query for all episodes and posts in parallel and in batches.
        episodes, posts = yield [
            gen.Task(Episode.BatchQuery,
                     client,
                     episode_keys,
                     None,
                     must_exist=False),
            gen.Task(Post.BatchQuery,
                     client,
                     post_keys,
                     None,
                     must_exist=False)
        ]

        # If a viewable post already exists, don't add it to the set to copy.
        new_ids = set()
        post_iter = iter(posts)
        for ep_dict, episode in zip(target_ep_dicts, episodes):
            if episode is None:
                # Add the episode id to the set to copy.
                new_ids.add(ep_dict['episode_id'])
            else:
                # Only owner user should get this far, since we check that new episode id contains the user's device id.
                assert episode.user_id == user_id, (episode, user_id)

                # Enforce sharing *tree* - no sharing acyclic graph allowed!
                if episode.parent_ep_id != ep_dict['parent_ep_id']:
                    raise InvalidRequestError(
                        'Cannot %s to episode "%s". It was created from a different parent episode.'
                        % (action, ep_dict['episode_id']))

                # Cannot share into episodes which are not in the target viewpoint.
                if episode.viewpoint_id != viewpoint_id:
                    raise InvalidRequestError(
                        'Cannot %s to episode "%s". It is not in viewpoint "%s".'
                        % (action, episode.episode_id, viewpoint_id))

            for photo_id in ep_dict['photo_ids']:
                post = next(post_iter)

                # If the post does not exist or is removed, add it to the new list.
                if post is None or post.IsRemoved():
                    new_ids.add(
                        Post.ConstructPostId(ep_dict['episode_id'], photo_id))

        raise gen.Return(new_ids)
Beispiel #25
0
  def _CreateSaveEpisodeDicts(self):
    """Creates a list of dicts describing the source and target episodes of the save. The
    episode dicts passed in the save_photos request are combined with episodes in any of the
    viewpoints passed in the save_photos request. Returns the list.
    """
    # Query episode ids from viewpoints given in the request, but skip those that are already in the request.
    vp_ep_ids = []
    skip_vp_ep_ids = set(ep_dict['existing_episode_id'] for ep_dict in self._ep_dicts)

    # Query photo_ids from viewpoint episodes.
    ep_ph_ids = {}

    @gen.coroutine
    def _VisitPosts(photo_ids, post):
      photo_ids.append(post.photo_id)

    @gen.coroutine
    def _VisitEpisodeKeys(episode_key):
      episode_id = episode_key.hash_key

      # Get list of episodes in the viewpoint that need a target episode id discovered/generated.
      if episode_id not in skip_vp_ep_ids:
        vp_ep_ids.append(episode_id)

      # For each episode in the viewpoint, get the complete list of photo ids in that episode.
      photo_ids = []
      yield gen.Task(Post.VisitRange, self._client, episode_id, None, None, partial(_VisitPosts, photo_ids))
      ep_ph_ids[episode_id] = photo_ids

    tasks = []
    for viewpoint_id in set(self._viewpoint_ids):
      query_expr = ('episode.viewpoint_id={id}', {'id': viewpoint_id})
      tasks.append(gen.Task(Episode.VisitIndexKeys, self._client, query_expr, _VisitEpisodeKeys))
    yield tasks

    # Allocate target ids for all episodes not given by the client.
    target_ep_ids = yield ViewfinderOperation._AllocateTargetEpisodeIds(self._client,
                                                                        self._user.user_id,
                                                                        self._user.webapp_dev_id,
                                                                        self._user.private_vp_id,
                                                                        vp_ep_ids)

    # Create save dicts for each of the viewpoint episodes to save.
    save_ep_dicts = {}
    for source_ep_id, target_ep_id in zip(vp_ep_ids, target_ep_ids):
      save_ep_dicts[target_ep_id] = {'existing_episode_id': source_ep_id,
                                     'new_episode_id': target_ep_id,
                                     'photo_ids': ep_ph_ids[source_ep_id]}

    # Now add the save dicts from the request, validating rules as we go.
    for ep_dict in self._ep_dicts:
      existing_ep_dict = save_ep_dicts.get(ep_dict['new_episode_id'], None)
      if existing_ep_dict is not None:
        if ep_dict['existing_episode_id'] != existing_ep_dict['existing_episode_id']:
          raise InvalidRequestError('Cannot save episodes "%s" and "%s" to same target episode "%s".' %
                                    (existing_ep_dict['existing_episode_id'],
                                     ep_dict['existing_episode_id'],
                                     ep_dict['new_episode_id']))

        existing_ep_dict['photo_ids'].extend(ep_dict['photo_ids'])
        existing_ep_dict['photo_ids'] = sorted(set(existing_ep_dict['photo_ids']))
      else:
        photo_ids = ep_dict['photo_ids']
        if ep_dict['existing_episode_id'] in ep_ph_ids:
          photo_ids.extend(ep_ph_ids[ep_dict['existing_episode_id']])

        save_ep_dicts[ep_dict['new_episode_id']] = {'existing_episode_id': ep_dict['existing_episode_id'],
                                                    'new_episode_id': ep_dict['new_episode_id'],
                                                    'photo_ids': sorted(set(photo_ids))}

    save_ep_dicts = [ep_dict for ep_dict in save_ep_dicts.itervalues()]
    save_ep_dicts.sort(key=itemgetter('new_episode_id'))
    raise gen.Return(save_ep_dicts)