Example #1
0
  def _Check(self):
    """Gathers pre-mutation information:
       1. Queries for the identity.

       Validates the following:
       1. Identity cannot be already linked to a different user.
    """
    self._identity = yield gen.Task(Identity.Query, self._client, self._source_identity_key, None, must_exist=False)
    if self._identity is None:
      self._identity = Identity.CreateFromKeywords(key=self._source_identity_key, authority='Viewfinder')

    if self._identity.user_id is not None and self._identity.user_id != self._target_user_id:
      raise PermissionError(ALREADY_LINKED, account=Identity.GetDescription(self._source_identity_key))
Example #2
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)
Example #3
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
            ])
Example #4
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)