Ejemplo n.º 1
0
    def _Check(self):
        """Gathers pre-mutation information:
       1. Queries for episodes.
       2. Queries for user posts.
       3. Checkpoints list of post ids that need to be removed.

       Validates the following:
       1. Permission to remove photos from episodes.
       2. Photos cannot be removed from shared viewpoints.
    """
        ep_ph_ids_list = [(ep_dict['episode_id'], ep_dict['photo_ids'])
                          for ep_dict in self._ep_dicts]
        self._ep_posts_list = yield self._CheckEpisodePostAccess(
            'remove', self._client, self._user.user_id, ep_ph_ids_list)

        for episode, post in self._ep_posts_list:
            if episode.viewpoint_id != self._user.private_vp_id:
                raise PermissionError(INVALID_REMOVE_PHOTOS_VIEWPOINT,
                                      viewpoint_id=episode.viewpoint_id)

        if self._op.checkpoint is None:
            # Get subset of photos that need to be removed.
            self._remove_ids = set(
                Post.ConstructPostId(episode.episode_id, post.photo_id)
                for episode, posts in self._ep_posts_list for post in posts
                if not post.IsRemoved())

            # Set checkpoint.
            # List of post ids to remove need to be check-pointed because they will change in the
            # UPDATE phase. If we fail after UPDATE, but before NOTIFY, we would not send correct
            # notifications on retry.
            yield self._op.SetCheckpoint(self._client,
                                         {'remove': list(self._remove_ids)})
        else:
            self._remove_ids = set(self._op.checkpoint['remove'])
Ejemplo n.º 2
0
    def _Check(self):
        """Gathers pre-mutation information:
       1. Queries for follower.

       Validates the following:
       1. Permission to remove viewpoint.

    Returns True if all checks succeeded and operation execution should continue, or False
    if the operation should end immediately.
    """
        # Get the follower object, check permissions, and determine if it's already been removed.
        self._follower = yield gen.Task(Follower.Query,
                                        self._client,
                                        self._user_id,
                                        self._viewpoint_id,
                                        None,
                                        must_exist=False)

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

        if self._op.checkpoint is None:
            # The operation should not proceed if the viewpoint is already removed.
            if self._follower.IsRemoved():
                raise gen.Return(False)

            # Set checkpoint so that IsRemoved will not be checked on restart (since it will be updated
            # to True as part of this operation).
            yield self._op.SetCheckpoint(self._client, {'state': 'remove'})

        raise gen.Return(True)
Ejemplo n.º 3
0
    def _Check(self):
        """Gathers pre-mutation information:
       1. Queries for follower.
       2. Queries for viewpoint.

       Validates the following:
       1. Permission to update follower metadata.
       2. Certain labels cannot be set.
    """
        self._follower = yield gen.Task(Follower.Query,
                                        self._client,
                                        self._user_id,
                                        self._viewpoint_id,
                                        None,
                                        must_exist=False)
        if self._follower is None:
            raise PermissionError(
                'User %d does not have permission to update follower "%s", or it does not exist.'
                % (self._user_id, self._viewpoint_id))

        self._viewpoint = yield gen.Task(Viewpoint.Query, self._client,
                                         self._viewpoint_id, None)

        if 'labels' in self._foll_dict:
            self._follower.SetLabels(self._foll_dict['labels'])
Ejemplo n.º 4
0
def ValidatePassword(client, user_id, password, salt, expected_hash):
    """Hashes the given user's password using the given salt, and validates that it matches the
  expected hash. Also ensures that the maximum incorrect guess count has not been exceeded.
  Raises a PermissionError if validation fails.
  """
    actual_hash = HashPassword(password, salt)

    # Limit the number of incorrect password guesses.
    guess_id = Guess.ConstructGuessId('pw', user_id)
    if not (yield Guess.CheckGuessLimit(client, guess_id,
                                        _MAX_PASSWORD_GUESSES)):
        raise PermissionError(TOO_MANY_GUESSES_ERROR)

    # If password does not match, increase incorrect guess count and raise error.
    if not web._time_independent_equals(actual_hash, expected_hash):
        yield Guess.ReportIncorrectGuess(client, guess_id)
        raise PermissionError(_PASSWORD_MISMATCH)
Ejemplo n.º 5
0
    def post(self):
        """POST is used when authenticating via the mobile application."""
        # Validate the request.
        yield gen.Task(self._StartJSONRequest,
                       'merge_token',
                       self.request,
                       json_schema.MERGE_TOKEN_REQUEST,
                       migrators=_REQUEST_MIGRATORS)

        # Validate the identity key.
        identity_key = self._request_message.dict['identity']
        AuthViewfinderHandler._ValidateIdentityKey(identity_key)

        # Require target merge account to be logged in, so that we can get target user name, id, and device type.
        context = ViewfinderContext.current()
        if context.user is None:
            # This case should never happen in the mobile or web clients, since they will not offer
            # the option to merge if the user is not already logged in. But it could happen with a
            # direct API call.
            raise PermissionError(MERGE_REQUIRES_LOGIN)

        identity = yield gen.Task(Identity.Query,
                                  self._client,
                                  identity_key,
                                  None,
                                  must_exist=False)
        if identity is not None and identity.user_id is not None:
            # If "error_if_linked" is true, raise an error, since the identity is already linked to a user.
            if self._request_message.dict.get('error_if_linked', False):
                raise PermissionError(
                    ALREADY_LINKED,
                    account=Identity.GetDescription(identity_key))

        # Send the email or SMS message in order to verify that the user controls it.
        yield VerifyIdBaseHandler.SendVerifyIdMessage(
            self._client,
            'merge_token',
            use_short_token=self._UseShortToken(),
            is_mobile_app=context.IsMobileClient(),
            identity_key=identity_key,
            user_id=context.user.user_id,
            user_name=context.user.name)

        self._FinishAuthViewfinder(identity_key)
Ejemplo n.º 6
0
  def _Check(self):
    """Gathers pre-mutation information:
       1. Queries for existing followers and comment.
       2. Checkpoints list of followers that need to be revived.

       Validates the following:
       1. Checks for maximum comment size.
       2. Permission to add a comment to the viewpoint.
    """
    # Check that the size of the comment message isn't too large.
    message_byte_size = len(escape.utf8(self._cm_dict['message']))
    if message_byte_size > Comment.COMMENT_SIZE_LIMIT_BYTES:
      logging.warning('User %d attempted to exceed message size limit ( %d / %d ) on comment "%s", viewpoint "%s"' %
                      (self._user_id, message_byte_size, Comment.COMMENT_SIZE_LIMIT_BYTES,
                       self._comment_id, self._viewpoint_id))
      raise LimitExceededError('Comment "%s" is too long.' % self._comment_id)

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

    # Check for permission to add a comment to the viewpoint.
    owner_follower = [follower for follower in self._followers if follower.user_id == self._user_id]
    if not owner_follower or not owner_follower[0].CanContribute():
      raise PermissionError('User %d does not have permission to add comments to viewpoint "%s".' %
                            (self._user_id, self._viewpoint_id))

    # Start populating the checkpoint if this the first time the operation has been run.
    if self._op.checkpoint is None:
      # If comment 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.
      comment = yield gen.Task(Comment.Query,
                               self._client,
                               self._viewpoint_id,
                               self._comment_id,
                               None,
                               must_exist=False)
      if comment is not None:
        logging.warning('comment "%s" already exists', self._comment_id)
        raise gen.Return(False)

      # 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)

      # 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}
      yield self._op.SetCheckpoint(self._client, checkpoint)
    else:
      # Restore state from checkpoint.
      self._revive_follower_ids = self._op.checkpoint['revive']

    raise gen.Return(True)
Ejemplo n.º 7
0
    def _Check(self):
        """Gathers pre-mutation information:
       1. Queries for existing followers.
       2. Checkpoints list of followers that need to be revived.

       Validates the following:
       1. Permission to update episode metadata.
    """
        if self._episode.user_id != self._user_id:
            raise PermissionError(
                'User id of episode "%s" does not match requesting user.' %
                self._episode_id)

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

        # Check for permission to modify the viewpoint.
        owner_follower = [
            follower for follower in self._followers
            if follower.user_id == self._user_id
        ]
        if not owner_follower or not owner_follower[0].CanContribute():
            raise PermissionError(
                'User %d does not have permission to contribute to viewpoint "%s".'
                % (self._user_id, 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)

            # 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}
            yield self._op.SetCheckpoint(self._client, checkpoint)
        else:
            # Restore state from checkpoint.
            self._revive_follower_ids = self._op.checkpoint['revive']
Ejemplo n.º 8
0
    def VerifyAccessToken(self, client, access_token):
        """Verifies the correctness of the given access token, that was previously generated in
    response to a CreateAccessTokenURL call. Verification will fail if any of these conditions
    is false.

      1. The access token is expired.
      2. Too many incorrect attempts to guess the token have been made in the past.
      3. The access token does not match.
    """
        identity_type, identity_value = Identity.SplitKey(self.key)
        now = time.time()

        if identity_type == 'Email':
            error = ExpiredError(EXPIRED_EMAIL_LINK_ERROR)
        else:
            error = ExpiredError(EXPIRED_ACCESS_CODE_ERROR)

        if self.authority != 'Viewfinder':
            # The most likely case here is that the user clicked an old link in their inbox. In the interim since
            # receiving the link, they may have logged in with Google, which would update the authority to Google.
            # In this case, the ExpiredError is an appropriate error message since the link is expired.
            logging.warning(
                'the authority is not "Viewfinder" for identity "%s"',
                self.key)
            raise error

        if now >= self.expires:
            # Either the access token has expired, or has already been used up.
            logging.warning('the access token has expired for identity "%s"',
                            self.key)
            raise error

        # Fail if too many incorrect guesses have been made.
        guess_id = self._ConstructAccessTokenGuessId(identity_type,
                                                     self.user_id)
        if not (yield Guess.CheckGuessLimit(client, guess_id,
                                            Identity._MAX_GUESSES)):
            logging.warning(
                'too many access token guesses have been made for identity "%s"',
                self.key)
            raise TooManyGuessesError(TOO_MANY_GUESSES_ERROR)

        # Increment incorrect guess account and raise permission error if the access code did not match.
        if not web._time_independent_equals(self.access_token, access_token):
            logging.warning(
                'the access token "%s" does not match for identity "%s"',
                access_token, self.key)
            yield Guess.ReportIncorrectGuess(client, guess_id)
            raise PermissionError(INCORRECT_ACCESS_CODE,
                                  identity_value=Identity.GetDescription(
                                      self.key))
Ejemplo n.º 9
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))
Ejemplo n.º 10
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)
Ejemplo n.º 11
0
def ValidateUserPassword(client, user, password):
    """Validates that the user's password matches the given password and that the maximum
  incorrect guess count has not been reached. Raises a PermissionError if it does not match.
  """
    assert user is not None, 'user should exist in login case'
    if user.pwd_hash is None:
        raise PermissionError(_NO_PASSWORD_SET)

    # Salt must already exist.
    assert user.salt, user
    user_salt = user.salt.Decrypt()
    user_pwd_hash = user.pwd_hash.Decrypt()

    yield ValidatePassword(client, user.user_id, password, user_salt,
                           user_pwd_hash)
Ejemplo n.º 12
0
  def SetLabels(self, new_labels):
    """Sets the labels attribute on the follower. This must be done with care in order to
    avoid security bugs such as allowing users to give themselves admin permissions, or
    allowing users to accidentally remove their right to see the viewpoint, or allowing a
    viewpoint to be removed without updating quota.

    TODO(Andy): Eventually we'll want more full-featured control over
                permissions.
    """
    new_labels = set(new_labels)
    new_unsettable_labels = new_labels.intersection(Follower.UNSETTABLE_LABLES)
    existing_labels = set(self.labels)
    existing_unsettable_labels = existing_labels.intersection(Follower.UNSETTABLE_LABLES)
    if new_unsettable_labels != existing_unsettable_labels:
      raise PermissionError('Permission and removed labels cannot be updated on the follower.')
    self.labels = new_labels
Ejemplo n.º 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)
Ejemplo n.º 14
0
 def _BuggyOpMethod(client, callback):
   self._method_count += 1
   # PermissionError is one of the exceptions which qualifies as abortable.
   raise PermissionError('Not Authorized')
Ejemplo n.º 15
0
    def _GatherUnshares(self, episode, posts):
        """Recursively accumulates the set of viewpoints, episodes, and posts that are affected
    by an unshare operation rooted at the specified episodes and posts. Adds the unshare
    information to "unshares_dict" in the format described in the UnshareOperation._Unshare
    docstring.

    Also gets list of ids of posts that have already been removed. Accounting will not be
    decremented again for these posts, since remove_photos already did it.

    This method does not mutate the database; it just gathers the information necessary to
    do so. Because episodes can be shared multiple times, and can even circle back to a
    previous viewpoint, it is necessary to traverse the entire sharing tree before concluding
    that all episodes to unshare have been discovered. If too many episodes have been
    traversed, raises a PermissionError. This protects the server from unshares of photos
    that have gone massively viral.
    """
        @gen.coroutine
        def _ProcessChildEpisode(child_episode, photo_ids_to_unshare):
            """For each child episode, query for the set of posts to unshare from each (might be a
      subset of parent posts). Recurse into the child episode to gather more unshares.
      """
            post_keys = [
                DBKey(child_episode.episode_id, ph_id)
                for ph_id in photo_ids_to_unshare
            ]
            child_posts = yield gen.Task(Post.BatchQuery,
                                         self._client,
                                         post_keys,
                                         None,
                                         must_exist=False)
            yield gen.Task(self._GatherUnshares, child_episode, child_posts)

        # Check whether episode traversal limit has been exceeded.
        self._num_unshares += 1
        if self._num_unshares > UnshareOperation._UNSHARE_LIMIT:
            raise PermissionError(
                'These photos cannot be unshared because they have already been shared too widely.'
            )

        # Get posts that were previously gathered.
        vp_dict = self._unshares_dict.get(episode.viewpoint_id, None)
        if vp_dict is not None and episode.episode_id in vp_dict:
            photo_ids_to_unshare = OrderedDict.fromkeys(
                vp_dict[episode.episode_id])
        else:
            photo_ids_to_unshare = OrderedDict()

        # Add photo ids to unshare. Don't include posts that have already been unshared, but do
        # include posts that have been removed (so that we add the UNSHARED label).
        for post in posts:
            if post is not None and not post.IsUnshared():
                photo_ids_to_unshare[post.photo_id] = None
                if post.IsRemoved():
                    # Post has already been removed, so accounting should not be adjusted again for it.
                    self._already_removed_ids.add(
                        Post.ConstructPostId(post.episode_id, post.photo_id))

        if len(photo_ids_to_unshare) > 0:
            self._unshares_dict.setdefault(
                episode.viewpoint_id,
                {})[episode.episode_id] = list(photo_ids_to_unshare)

        # Recursively descend into children of the episode, looking for additional branches of the sharing tree.
        # Use secondary index to find children of "episode".
        query_expr = ('episode.parent_ep_id={id}', {'id': episode.episode_id})
        child_episodes = yield gen.Task(Episode.IndexQuery, self._client,
                                        query_expr, None)
        yield [
            _ProcessChildEpisode(child_episode, photo_ids_to_unshare)
            for child_episode in child_episodes
        ]
Ejemplo n.º 16
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)
Ejemplo n.º 17
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', {})
Ejemplo n.º 18
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.
       3. Checkpoints list of contacts that need to be made prospective users.
       4. Checkpoints list of contacts that are already following the viewpoint.

       Validates the following:
       1. Max follower limit.
       2. Permission to add followers.
    """
        # 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)

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

        # Start populating the checkpoint if this the first time the operation has been run.
        if self._op.checkpoint is None:
            # Get all existing followers.
            self._existing_followers, _ = yield gen.Task(
                Viewpoint.QueryFollowers,
                self._client,
                self._viewpoint_id,
                limit=Viewpoint.MAX_FOLLOWERS)

            # Get list of followers which have removed themselves from the viewpoint and will need to be revived.
            self._revive_follower_ids = self._GetRevivableFollowers(
                self._existing_followers)

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

            # Set checkpoint.
            # Existing followers, followers to revive, and list of contacts need to be check-pointed
            # because these sets are changed in the UPDATE phase. If we fail after UPDATE, but before
            # NOTIFY, we would not send correct notifications on retry.
            checkpoint = {
                'existing':
                [follower.user_id for follower in self._existing_followers],
                'revive':
                self._revive_follower_ids,
                'contacts':
                self._contact_ids
            }
            yield self._op.SetCheckpoint(self._client, checkpoint)
        else:
            # Restore state from checkpoint.
            follower_keys = [
                DBKey(follower_id, self._viewpoint_id)
                for follower_id in self._op.checkpoint['existing']
            ]
            self._existing_followers = yield gen.Task(Follower.BatchQuery,
                                                      self._client,
                                                      follower_keys, None)
            self._revive_follower_ids = self._op.checkpoint['revive']
            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
        ]

        # Check if we're about to exceed follower limit on this viewpoint.
        if len(self._existing_followers) + len(
                self._contact_dicts) > Viewpoint.MAX_FOLLOWERS:
            raise LimitExceededError(
                'User %d attempted to exceed follower limit on viewpoint "%s" by adding %d followers.'
                %
                (self._user_id, self._viewpoint_id, len(self._contact_dicts)))
Ejemplo n.º 19
0
    def _PrepareAuthUser(self, user_dict, ident_dict, device_dict):
        """Validates incoming user, identity, and device information in preparation for login,
    register, or link action. Derives user id and name and sets them into the user dict.
    """
        # Create json_attrs from the user_dict returned by the auth service.
        ident_dict['json_attrs'] = user_dict

        # Check whether identity is already created.
        identity = yield gen.Task(Identity.Query,
                                  self._client,
                                  ident_dict['key'],
                                  None,
                                  must_exist=False)

        # Ensure that user id and device id are allocated.
        current_user = self.get_current_user()

        # Find or allocate the user id.
        if self._action in ['login', 'login_reset']:
            # Require identity to already be linked to an account.
            if identity is not None and identity.user_id is not None:
                user = yield gen.Task(User.Query,
                                      self._client,
                                      identity.user_id,
                                      None,
                                      must_exist=False)
            else:
                user = None

            if user is None:
                raise PermissionError(NO_USER_ACCOUNT,
                                      account=Identity.GetDescription(
                                          ident_dict['key']))

            if not user.IsRegistered():
                # Cannot log into an unregistered account.
                raise PermissionError(LOGIN_REQUIRES_REGISTER)

            user_dict['user_id'] = identity.user_id
        elif self._action == 'register':
            if identity is not None and identity.user_id is not None:
                # Identity should already be bound to a user, so only proceed if registering a prospective user.
                user = yield gen.Task(User.Query,
                                      self._client,
                                      identity.user_id,
                                      None,
                                      must_exist=False)
                if user is None or user.IsRegistered():
                    # User can be None if there's a DB corruption, or if it's still in the process of
                    # creation. Treat this case the same as if the user exists but is already registered.
                    raise PermissionError(ALREADY_REGISTERED,
                                          account=Identity.GetDescription(
                                              identity.key))

                user_dict['user_id'] = user.user_id
            else:
                # Construct a prospective user with newly allocated user id and web device id.
                user_id, webapp_dev_id = yield User.AllocateUserAndWebDeviceIds(
                    self._client)
                user_dict['user_id'] = user_id

                request = {
                    'headers': {
                        'synchronous': True
                    },
                    'user_id': user_id,
                    'webapp_dev_id': webapp_dev_id,
                    'identity_key': ident_dict['key'],
                    'reason': 'register'
                }
                yield gen.Task(Operation.CreateAndExecute, self._client,
                               user_id, webapp_dev_id,
                               'CreateProspectiveOperation.Execute', request)

                user = yield gen.Task(User.Query, self._client, user_id, None)
                identity = yield gen.Task(Identity.Query, self._client,
                                          ident_dict['key'], None)

            if options.options.freeze_new_accounts:
                raise web.HTTPError(403, _FREEZE_NEW_ACCOUNTS_MESSAGE)
        else:
            assert self._action == 'link', self._action
            if current_user is None:
                # This case should never happen in the mobile or web clients, since they will not offer
                # the option to link if the user is not already logged in. But it could happen with a
                # direct API call.
                raise PermissionError(MERGE_REQUIRES_LOGIN)

            if not current_user.IsRegistered():
                raise web.HTTPError(403, _CANNOT_LINK_TO_PROSPECTIVE)

            if identity is not None and identity.user_id is not None and current_user.user_id != identity.user_id:
                raise PermissionError(ALREADY_LINKED,
                                      account=Identity.GetDescription(
                                          ident_dict['key']))

            # Ensure that the new identity is created.
            if identity is None:
                identity = Identity.CreateFromKeywords(key=ident_dict['key'])
                yield gen.Task(identity.Update, self._client)

            user = current_user
            user_dict['user_id'] = current_user.user_id

        assert user, user_dict
        assert identity, ident_dict

        if device_dict is not None:
            if 'device_id' in device_dict:
                # If device_id was specified, it must be owned by the calling user.
                if 'user_id' in user_dict:
                    # Raise error if the device specified in the device dict is not owned by the calling user.
                    device = yield gen.Task(Device.Query,
                                            self._client,
                                            user_dict['user_id'],
                                            device_dict['device_id'],
                                            None,
                                            must_exist=False)
                    if device is None:
                        raise web.HTTPError(
                            403, 'user %d does not own device %d' %
                            (user_dict['user_id'], device_dict['device_id']))
                else:
                    logging.warning(
                        'device_id cannot be set when user does not yet exist: %s'
                        % device_dict)
                    raise web.HTTPError(403,
                                        _CANNOT_SET_DEVICE_FOR_USER_MESSAGE)

        raise gen.Return(user)
Ejemplo n.º 20
0
    def _Check(self):
        """Gathers pre-mutation information:
       1. Queries for existing followers and viewpoint.
       2. Checkpoints list of followers that need to have REMOVED label added.

       Validates the following:
       1. Viewpoint exists and is not a default viewpoint.
       2. Permission to modify viewpoint.
       3. Permission to remove the requested followers.
    """
        # Get all existing followers.
        self._followers, _ = yield gen.Task(Viewpoint.QueryFollowers,
                                            self._client,
                                            self._viewpoint_id,
                                            limit=Viewpoint.MAX_FOLLOWERS)

        # Get the viewpoint to be modified, along with the follower that is removing the users.
        # This state will not be changed by remove followers, and so doesn't need to be part of
        # the checkpoint.
        self._viewpoint, self._removing_follower = yield gen.Task(
            Viewpoint.QueryWithFollower, self._client, self._user_id,
            self._viewpoint_id)

        # Raise error if viewpoint is not found.
        if self._viewpoint is None:
            raise NotFoundError(VIEWPOINT_NOT_FOUND,
                                viewpoint_id=self._viewpoint_id)

        # Don't allow removal of followers from a default viewpoint.
        if self._viewpoint.IsDefault():
            raise PermissionError(CANNOT_REMOVE_DEFAULT_FOLLOWER)

        # Check permission to remove followers from the viewpoint.
        if self._removing_follower is None or not self._removing_follower.CanContribute(
        ):
            raise PermissionError(CANNOT_REMOVE_FOLLOWERS,
                                  user_id=self._user_id,
                                  viewpoint_id=self._viewpoint.viewpoint_id)

        # Check permission to remove each of the followers.
        for follower in self._followers:
            # Only consider followers to be removed.
            if follower.user_id not in self._remove_id_set:
                continue

            # User can always remove himself from the viewpoint.
            if follower.user_id == self._user_id:
                continue

            # User can only remove other user if he originally added that user.
            if follower.adding_user_id != self._user_id:
                raise PermissionError(
                    CANNOT_REMOVE_THIS_FOLLOWER,
                    remove_id=follower.user_id,
                    viewpoint_id=self._viewpoint.viewpoint_id)

            # Follower can only be removed if he was added less than 7 days ago.
            if util.GetCurrentTimestamp(
            ) - follower.timestamp > RemoveFollowersOperation.MAX_REMOVE_PERIOD:
                raise PermissionError(
                    CANNOT_REMOVE_OLD_FOLLOWER,
                    remove_id=follower.user_id,
                    viewpoint_id=self._viewpoint.viewpoint_id)

        # Get followers to make un-revivable.
        self._unrevivable_followers = [
            follower for follower in self._followers
            if follower.user_id in self._remove_id_set
            and not follower.IsUnrevivable()
        ]

        # Start populating the checkpoint if this the first time the operation has been run.
        if self._op.checkpoint is None:
            # Trim down remove set to include only those which are not already removed. Note that
            # some of the discarded followers still need to be made un-revivable.
            for follower in self._followers:
                if follower.IsRemoved():
                    self._remove_id_set.discard(follower.user_id)

            # Set checkpoint.
            # The list of followers that need to be removed from the viewpoint need to be check-pointed
            # because it is changed in the UPDATE phase. If we fail after UPDATE, but before NOTIFY,
            # we would not send correct notifications on retry.
            checkpoint = {'remove': list(self._remove_id_set)}
            yield self._op.SetCheckpoint(self._client, checkpoint)
        else:
            # Restore state from checkpoint.
            self._remove_id_set = set(self._op.checkpoint['remove'])
Ejemplo n.º 21
0
    def _Check(self):
        """Gathers pre-mutation information:
       1. Existing viewpoint and owner follower.
       2. Followers of the existing viewpoint.
       3. List of requested episodes and photos to share.
       4. Checkpoints list of episode and post ids that need to be (re)created.
       5. Checkpoints list of followers that need to be revived.
       6. Checkpoints boolean indicating whether cover photo needs to be set.

       Validates the following:
       1. Permissions to share from source episodes.
       2. Permission to share into existing viewpoint.
    """
        self._viewpoint, self._follower = yield gen.Task(
            Viewpoint.QueryWithFollower, self._client, self._user_id,
            self._viewpoint_id)

        # Checks permission to share into viewpoint.
        if self._follower is None or not self._follower.CanContribute():
            raise PermissionError(
                'User %d does not have permission to contribute to viewpoint "%s".'
                % (self._user_id, self._viewpoint_id))
        assert self._viewpoint is not None, self._viewpoint_id

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

        # Validate source episodes and posts and save the list for possible later use.
        self._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,
            self._source_ep_posts_list, target_ep_ids)

        # Start populating the checkpoint if this the first time the operation has been run.
        if self._op.checkpoint is None:
            # Get subset of target episodes and posts that need to be shared.
            self._new_ids = yield self._CheckCopyTargets(
                'share', self._client, self._user_id, self._viewpoint_id,
                self._new_ep_dicts)

            # 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)

            # Check whether a cover photo needs to be set on the viewpoint.
            self._need_cover_photo = not self._viewpoint.IsDefault(
            ) and not self._viewpoint.IsCoverPhotoSet()

            # Set checkpoint.
            # List of new episode/post ids and followers to revive 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),
                'revive': self._revive_follower_ids,
                'cover': self._need_cover_photo
            }
            yield self._op.SetCheckpoint(self._client, checkpoint)
        else:
            # Restore state from checkpoint.
            self._new_ids = set(self._op.checkpoint['new'])
            self._revive_follower_ids = self._op.checkpoint['revive']
            self._need_cover_photo = self._op.checkpoint['cover']

        raise gen.Return(True)