Example #1
0
 def _MakeContactDict(contact):
     """Create a contact dict from the contact object plus its referenced identity object.
 """
     identity_dict = dict()
     for identity_key in contact.identities:
         identity = validator.GetModelObject(Identity,
                                             identity_key,
                                             must_exist=False)
         identity_dict[identity_key] = identity
     contact_dict = {
         'contact_id': contact.contact_id,
         'contact_source': contact.contact_source
     }
     util.SetIfNotNone(contact_dict, 'name', contact.name)
     util.SetIfNotNone(contact_dict, 'given_name', contact.given_name)
     util.SetIfNotNone(contact_dict, 'family_name', contact.family_name)
     util.SetIfNotNone(contact_dict, 'rank', contact.rank)
     if contact.labels is not None and len(contact.labels) > 0:
         contact_dict['labels'] = list(contact.labels)
     identities_list = []
     if contact.identities_properties is not None:
         for identity_properties in contact.identities_properties:
             identity_key = identity_properties[0]
             properties = {'identity': identity_key}
             util.SetIfNotNone(properties, 'description',
                               identity_properties[1])
             if identity_dict[Identity.Canonicalize(identity_key)] is None:
                 user_id = None
             else:
                 user_id = identity_dict[Identity.Canonicalize(
                     identity_key)].user_id
             util.SetIfNotNone(properties, 'user_id', user_id)
             identities_list.append(properties)
         contact_dict['identities'] = identities_list
     return contact_dict
Example #2
0
    def testPhoneNumbers(self):
        """Test validation of phone numbers."""
        # United States.
        self.assertEqual(Identity.CanonicalizePhone('+14251234567'),
                         '+14251234567')

        # Malaysia.
        self.assertEqual(Identity.CanonicalizePhone('+60321345678'),
                         '+60321345678')

        # Great Britain.
        self.assertEqual(Identity.CanonicalizePhone('+442083661177'),
                         '+442083661177')

        # China.
        self.assertEqual(Identity.CanonicalizePhone('+861082301234'),
                         '+861082301234')

        self.assertRaises(InvalidRequestError, Identity.CanonicalizePhone,
                          None)
        self.assertRaises(InvalidRequestError, Identity.CanonicalizePhone, '')
        self.assertRaises(InvalidRequestError, Identity.CanonicalizePhone,
                          '14251234567')
        self.assertRaises(InvalidRequestError, Identity.CanonicalizePhone, '+')
        self.assertRaises(InvalidRequestError, Identity.CanonicalizePhone,
                          '+abc')
Example #3
0
 def _FinishAuthViewfinder(self, identity_key):
     """Finishes the Viewfinder auth response, passing back the number of digits used in the
 access token.
 """
     identity_type, identity_value = Identity.SplitKey(identity_key)
     num_digits, good_for = Identity.GetAccessTokenSettings(
         identity_type, self._UseShortToken())
     self._FinishJSONRequest(None, {'token_digits': num_digits},
                             json_schema.AUTH_VIEWFINDER_RESPONSE)
Example #4
0
 def _ValidateIdentityKey(cls, identity_key):
     """Validates that the identity key is in canonical format, and that it's either an Email
 or a Phone identity. Returns a tuple containing: (identity_type, identity_value).
 """
     Identity.ValidateKey(identity_key)
     identity_type, identity_value = Identity.SplitKey(identity_key)
     if identity_type not in ['Email', 'Phone']:
         raise web.HTTPError(400, _IDENTITY_NOT_SUPPORTED % identity_key)
     return (identity_type, identity_value)
Example #5
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 #6
0
        def _VerifyResponse(response):
            """Verify successful registration. Query the identity and
      contacts and verify against the actual test data in facebook.
      """
            self.assertEqual(response.code, 200)
            cookie = self._tester.GetCookieFromResponse(response)
            user_dict = self._tester.DecodeUserCookie(cookie)
            response_dict = json.loads(response.body)

            self.assertTrue('user_id' in user_dict)
            self.assertTrue('device_id' in user_dict)
            self.assertEqual(user_dict['device_id'],
                             response_dict['device_id'])

            with util.DictBarrier(partial(_VerifyAccountStatus, cookie)) as b:
                identity_key = 'FacebookGraph:%s' % users[0]['id']
                Identity.Query(self._client,
                               hash_key=identity_key,
                               col_names=None,
                               callback=b.Callback('identity'))
                User.Query(self._client,
                           hash_key=user_dict['user_id'],
                           col_names=None,
                           callback=b.Callback('user'))
                Device.Query(self._client,
                             hash_key=user_dict['user_id'],
                             range_key=user_dict['device_id'],
                             col_names=None,
                             callback=b.Callback('device'))
Example #7
0
    def Transform(self, client, contact, callback):
        from viewfinder.backend.db.contact import Contact
        from viewfinder.backend.db.identity import Identity
        contact_dict = contact._asdict()
        # During this upgrade assume that any email identities came from GMail and any facebook identities came from
        #   Facebook.  At this time, there shouldn't be any identities that start with anything else.
        assert contact.identity.startswith(
            'Email:') or contact.identity.startswith('FacebookGraph:'), contact
        contact_dict[
            'contact_source'] = Contact.GMAIL if contact.identity.startswith(
                'Email:') else Contact.FACEBOOK
        contact_dict['identities_properties'] = [
            (Identity.Canonicalize(contact.identity), None)
        ]
        contact_dict['timestamp'] = util.GetCurrentTimestamp()
        # Let Contact.CreateFromKeywords calculate a new sort_key.
        contact_dict.pop('sort_key')
        # Let Contact.CreateFromKeywords determine value for identities column.
        contact_dict.pop('identities')
        # Contact.CreateFromKeywords() will calculate sort_key, contact_id, and identities columns.
        new_contact = Contact.CreateFromKeywords(**contact_dict)

        self._LogUpdate(new_contact)

        if Version._mutate_items:
            yield gen.Task(new_contact.Update, client)
            yield gen.Task(contact.Delete, client)

        callback(new_contact)
Example #8
0
    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)
Example #9
0
    def _CreateViewpointURL(cls,
                            client,
                            recipient_user,
                            identity_key,
                            viewpoint,
                            use_short_domain=False):
        """Creates a Short URL which links to a conversation on the website.

    If "use_short_domain" is true, then return a URL that uses the short domain, along with
    a shorter group_id prefix that will get re-mapped by ShortDomainRedirectHandler.
    """
        # Create ShortURL that sets prospective user cookie and then redirects to the conversation.
        short_url = yield Identity.CreateInvitationURL(
            client,
            recipient_user.user_id,
            identity_key,
            viewpoint.viewpoint_id,
            default_url='/view#conv/%s' % viewpoint.viewpoint_id)

        # ShortURL's can use either the regular domain or the short domain. For SMS messages, we
        # typically use the short domain. For email, we typically use the regular domain.
        if use_short_domain:
            assert short_url.group_id.startswith('pr/'), short_url
            raise gen.Return('https://%s/p%s%s' %
                             (options.options.short_domain,
                              short_url.group_id[3:], short_url.random_key))

        raise gen.Return(
            'https://%s/%s%s' %
            (options.options.domain, short_url.group_id, short_url.random_key))
Example #10
0
  def CreateContactDict(cls, user_id, identities_properties, timestamp, contact_source, **kwargs):
    """Creates a dict with all properties needed for a contact.
    The identities_properties parameter is a list of tuples where each tuple is:
      (identity_key, description_string).  Description string is for 'work', 'mobile', 'home', etc...
      designation and may be None.
    This includes calculation of the contact_id and sort_key from timestamp, contact_source, and other attributes.
    Returns: contact dictionary.
    """
    from viewfinder.backend.db.identity import Identity

    contact_dict = {'user_id': user_id,
                    'timestamp': timestamp,
                    'contact_source': contact_source}

    if Contact.REMOVED not in kwargs.get('labels', []):
      # identities is the unique set of canonicalized identities associated with this contact.
      contact_dict['identities'] = {Identity.Canonicalize(identity_properties[0])
                                    for identity_properties in identities_properties}
      contact_dict['identities_properties'] = identities_properties

    contact_dict.update(kwargs)
    if 'contact_id' not in contact_dict:
      contact_dict['contact_id'] = Contact.CalculateContactId(contact_dict)
    if 'sort_key' not in contact_dict:
      contact_dict['sort_key'] = Contact.CreateSortKey(contact_dict['contact_id'], timestamp)

    return contact_dict
Example #11
0
def _TestAuthGoogleUser(action,
                        tester,
                        user_dict,
                        device_dict=None,
                        user_cookie=None):
    """Called by the ServiceTester in order to test login/google, link/google, and
  register/google calls.
  """
    ident_dict = {
        'key': 'Email:%s' % Identity.CanonicalizeEmail(user_dict['email']),
        'authority': 'Google',
        'refresh_token': 'refresh_token',
        'access_token': 'access_token',
        'expires': util._TEST_TIME + 3600
    }
    if device_dict:
        device_dict.pop('device_uuid', None)
        device_dict.pop('test_udid', None)

    # Mock responses from Google.
    with mock.patch('tornado.httpclient.AsyncHTTPClient',
                    MockAsyncHTTPClient()) as mock_client:
        # Response to request for access token.
        auth_test._AddMockJSONResponse(
            mock_client, r'https://accounts.google.com/o/oauth2/token', {
                'access_token': ident_dict['access_token'],
                'token_type': 'Bearer',
                'expires_in': ident_dict['expires'] - util._TEST_TIME,
                'id_token': 'id_token',
                'refresh_token': ident_dict['refresh_token']
            })

        # Response to request for user info.
        auth_test._AddMockJSONResponse(
            mock_client, r'https://www.googleapis.com/oauth2/v1/userinfo\?',
            user_dict)

        # Response to request for people (i.e. contacts).
        auth_test._AddMockJSONResponse(
            mock_client,
            r'https://www.google.com/m8/feeds/contacts/default/full', {
                'feed': {
                    'entry': [],
                    'openSearch$startIndex': {
                        '$t': '1'
                    },
                    'openSearch$totalResults': {
                        '$t': '0'
                    }
                }
            })

        response = auth_test._AuthFacebookOrGoogleUser(tester, action,
                                                       user_dict, ident_dict,
                                                       device_dict,
                                                       user_cookie)
        return auth_test._ValidateAuthUser(tester, action, user_dict,
                                           ident_dict, device_dict,
                                           user_cookie, response)
Example #12
0
 def testRepr(self):
     """Test conversion of Identity objects to strings."""
     ident = Identity.CreateFromKeywords(key='Email:[email protected]',
                                         access_token='access_token1',
                                         refresh_token='refresh_token1')
     self.assertIn('*****@*****.**', repr(ident))
     self.assertIn('scrubbed', repr(ident))
     self.assertNotIn('access_token1', repr(ident))
     self.assertNotIn('refresh_token1', repr(ident))
Example #13
0
 def _GetAccessTokenSms(cls, identity):
     """Returns a dict of parameters that will be passed to SMSManager.SendSMS in order to
 text an access token to a user who is verifying his/her account.
 """
     identity_type, identity_value = Identity.SplitKey(identity.key)
     return {
         'number': identity_value,
         'text': 'Viewfinder code: %s' % identity.access_token
     }
Example #14
0
def _Unlink(callback):
    assert options.options.identity, 'must specify --identity'
    client = DBClient.Instance()

    def _OnQueryIdentity(ident):
        assert ident.authority, 'unauthenticated identity has no associated contacts: %r' % ident
        assert ident.access_token, 'identity has no access token: %r' % ident
        ident.FetchContacts(client, callback)

    Identity.Query(client, options.options.identity, None, _OnQueryIdentity)
Example #15
0
    def _GetAuthEmail(cls, client, action, use_short_token, user_name,
                      identity, short_url):
        """Returns a dict of parameters that will be passed to EmailManager.SendEmail in order to
    email an access token to a user who is verifying his/her account.
    """
        action_info = VerifyIdBaseHandler.ACTION_MAP[action]
        identity_type, identity_value = Identity.SplitKey(identity.key)

        # Create arguments for the email.
        args = {
            'from': EmailManager.Instance().GetInfoAddress(),
            'fromname': 'Viewfinder',
            'to': identity_value
        }
        util.SetIfNotNone(args, 'toname', user_name)

        # Create arguments for the email template.
        fmt_args = {
            'user_name':
            user_name or identity_value,
            'user_email':
            identity_value,
            'url':
            'https://%s/%s%s' % (ServerEnvironment.GetHost(),
                                 short_url.group_id, short_url.random_key),
            'title':
            action_info.title,
            'use_short_token':
            use_short_token,
            'access_token':
            identity.access_token
        }

        # The email html format is designed to meet these requirements:
        #   1. It must be viewable on even the most primitive email html viewer. Avoid fancy CSS.
        #   2. It cannot contain any images. Some email systems (like Gmail) do not show images by default.
        #   3. It must be short and look good on an IPhone 4S screen. The action button should be visible
        #      without any scrolling necessary.
        resources_mgr = ResourcesManager.Instance()
        if use_short_token:
            args['subject'] = 'Viewfinder Code: %s' % identity.access_token
        else:
            args['subject'] = action_info.title
        args['html'] = resources_mgr.GenerateTemplate(
            action_info.email_template, is_html=True, **fmt_args)
        args['text'] = resources_mgr.GenerateTemplate(
            action_info.email_template, is_html=False, **fmt_args)

        # Remove extra whitespace in the HTML (seems to help it avoid Gmail spam filter).
        args['html'] = escape.squeeze(args['html'])

        return args
Example #16
0
    def testDerivedAttributes(self):
        """Test that the identity and identities attributes are being properly derived from the
    identities_properties attribute.
    """
        # Create a Peter contact for Spencer with multiple identical and nearly identical identities.
        spencer = self._user
        contact_identity_a = 'Email:[email protected]'
        contact_identity_b = 'Email:[email protected]'
        contact_identity_c = 'Email:[email protected]'
        timestamp = util.GetCurrentTimestamp()

        contact = Contact.CreateFromKeywords(spencer.user_id,
                                             [(contact_identity_a, None),
                                              (contact_identity_b, 'home'),
                                              (contact_identity_c, 'work')],
                                             timestamp,
                                             Contact.GMAIL,
                                             name='Peter Mattis',
                                             given_name='Peter',
                                             family_name='Mattis',
                                             rank=42)

        self.assertEqual(len(contact.identities_properties), 3)
        self.assertEqual(len(contact.identities), 2)
        self.assertFalse(contact_identity_a in contact.identities)
        self.assertFalse(contact_identity_b in contact.identities)
        self.assertFalse(contact_identity_c in contact.identities)
        self.assertTrue(
            Identity.Canonicalize(contact_identity_a) in contact.identities)
        self.assertTrue(
            Identity.Canonicalize(contact_identity_b) in contact.identities)
        self.assertTrue(
            Identity.Canonicalize(contact_identity_c) in contact.identities)
        self.assertTrue(
            [contact_identity_a, None] in contact.identities_properties)
        self.assertTrue(
            [contact_identity_b, 'home'] in contact.identities_properties)
        self.assertTrue(
            [contact_identity_c, 'work'] in contact.identities_properties)
Example #17
0
  def CreateProspective(cls, client, user_id, webapp_dev_id, identity_key, timestamp):
    """Creates a prospective user with the specified user id. web device id, and identity key.

    A prospective user is typically created when photos are shared with a contact that is not
    yet a Viewfinder user.

    Returns a tuple containing the user and identity.
    """
    from viewfinder.backend.db.viewpoint import Viewpoint

    identity_type, identity_value = Identity.SplitKey(identity_key)

    # Ensure that identity is created.
    identity = yield gen.Task(Identity.CreateProspective,
                              client,
                              identity_key,
                              user_id,
                              timestamp)

    # Create the default viewpoint.
    viewpoint = yield Viewpoint.CreateDefault(client, user_id, webapp_dev_id, timestamp)

    # By default, send alerts when a new conversation is started. Send email alerts if the
    # identity is email, or sms alerts if the identity is phone.
    email_alerts = AccountSettings.EMAIL_ON_SHARE_NEW if identity_type == 'Email' else AccountSettings.EMAIL_NONE
    sms_alerts = AccountSettings.SMS_ON_SHARE_NEW if identity_type == 'Phone' else AccountSettings.SMS_NONE
    settings = AccountSettings.CreateForUser(user_id,
                                             email_alerts=email_alerts,
                                             sms_alerts=sms_alerts,
                                             push_alerts=AccountSettings.PUSH_NONE)
    yield gen.Task(settings.Update, client)

    # Create a Friend relation (every user is friends with himself).
    friend = Friend.CreateFromKeywords(user_id=user_id, friend_id=user_id)
    yield gen.Task(friend.Update, client)

    # Create the prospective user.
    email = identity_value if identity_type == 'Email' else None
    phone = identity_value if identity_type == 'Phone' else None
    user = User.CreateFromKeywords(user_id=user_id,
                                   private_vp_id=viewpoint.viewpoint_id,
                                   webapp_dev_id=webapp_dev_id,
                                   email=email,
                                   phone=phone,
                                   asset_id_seq=User._RESERVED_ASSET_ID_COUNT,
                                   signing_key=secrets.CreateSigningKeyset('signing_key'))
    yield gen.Task(user.Update, client)

    raise gen.Return((user, identity))
Example #18
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 #19
0
    def _GetUserInfo(self, device_dict, refresh_token, response):
        """Parses the google access token from the JSON response body. Gets user data via OAUTH2
    with access token.
    """
        tokens = www_util.ParseJSONResponse(response)
        assert tokens, 'unable to fetch access token'
        access_token = tokens['access_token']
        expires = tokens['expires_in']
        if tokens.has_key('refresh_token') and not refresh_token:
            refresh_token = tokens['refresh_token']

        # Using the access token that was previously retrieved, request information about the
        # user that is logging in.
        assert access_token, 'no access token was provided'
        url = AuthGoogleHandler._OAUTH2_USERINFO_URL + '?' + urllib.urlencode(
            {'access_token': access_token})
        http_client = httpclient.AsyncHTTPClient()
        response = yield gen.Task(http_client.fetch, url)

        # Parse the user information from the JSON response body and invoke _OnAuthenticate to
        # register the user as a viewfinder account. Create user dict from Google's JSON response.
        user_dict = www_util.ParseJSONResponse(response)
        assert user_dict, 'unable to fetch user data'
        assert 'phone' not in user_dict, user_dict
        assert 'email' in user_dict, user_dict
        user_dict['email'] = Identity.CanonicalizeEmail(user_dict['email'])

        # Ensure that user email is verified, else we can't trust that the user really owns it.
        if not user_dict.get('verified_email', False):
            raise web.HTTPError(
                403, _CANNOT_USE_UNVERIFIED_EMAIL % user_dict['email'])

        # Create identity dict from Google's email field.
        ident_dict = {
            'key': 'Email:%s' % user_dict['email'],
            'authority': 'Google',
            'refresh_token': refresh_token,
            'access_token': access_token,
            'expires': util.GetCurrentTimestamp() + expires
        }

        self._AuthUser(user_dict, ident_dict, device_dict)
Example #20
0
  def ValidateCreateContact(self, user_id, identities_properties, timestamp, contact_source, **op_dict):
    """Validates creation of contact along with derived attributes.
    Returns created contact.
    """
    contact_dict = op_dict
    contact_dict['user_id'] = user_id
    contact_dict['timestamp'] = timestamp
    if identities_properties is not None:
      contact_dict['identities_properties'] = identities_properties
      contact_dict['identities'] = set([Identity.Canonicalize(identity_properties[0])
                                        for identity_properties in identities_properties])
    else:
      contact_dict['identities_properties'] = None
    contact_dict['contact_source'] = contact_source
    if 'contact_id' not in contact_dict:
      contact_dict['contact_id'] = Contact.CalculateContactId(contact_dict)
    if 'sort_key' not in contact_dict:
      contact_dict['sort_key'] = Contact.CreateSortKey(contact_dict['contact_id'], timestamp)

    return self.ValidateCreateDBObject(Contact, **contact_dict)
Example #21
0
def CountByIdentity(client, user_id, callback):
    query_str = 'identity.user_id=%d' % user_id
    # We only care about the identity type (the key).
    result = yield gen.Task(Identity.IndexQuery,
                            client,
                            query_str,
                            col_names=['key'])
    if len(result) == 0:
        callback(('NONE', 'NONE'))
        return

    type_count = Counter()
    for r in result:
        identity_type, value = Identity.SplitKey(r.key)
        type_count[identity_type[0]] += 1
    count_by_type = ''
    types = ''
    for k in sorted(type_count.keys()):
        count_by_type += '%s%d' % (k, type_count[k])
        types += k
    callback((count_by_type, types))
Example #22
0
    def _CreateContact(self,
                       identities_properties,
                       no_identity=False,
                       **kwargs):
        if not no_identity:
            for identity_properties in identities_properties:
                contact_user_id = identity_properties[2] if len(
                    identity_properties) > 2 else None
                self._UpdateOrAllocateDBObject(Identity,
                                               key=Identity.Canonicalize(
                                                   identity_properties[0]),
                                               user_id=contact_user_id)

        contact_identities_properties = []
        for identity_properties in identities_properties:
            contact_identities_properties.append(identity_properties[:2])

        contact_dict = Contact.CreateContactDict(
            self._user.user_id, contact_identities_properties, util._TEST_TIME,
            Contact.GMAIL, **kwargs)
        self._UpdateOrAllocateDBObject(Contact, **contact_dict)
Example #23
0
  def ValidateUpdateUser(self, name, op_dict, user_dict, ident_dict,
                         device_dict=None, is_prospective=False):
    """Validates that a user and identity have been created in the database
    if they did not already exist, or were updated if they did. If
    "device_dict" is defined, validates that a device was created or updated
    as well.
    """
    user_id = user_dict['user_id']

    # Validate creation of the default viewpoint, follower, and followed record.
    viewpoint_id = Viewpoint.ConstructViewpointId(user_dict['webapp_dev_id'], 0)
    viewpoint = self.GetModelObject(User, user_id, must_exist=False)
    if viewpoint is None:
      expected_viewpoint = self.ValidateCreateDBObject(Viewpoint,
                                                       viewpoint_id=viewpoint_id,
                                                       user_id=user_id,
                                                       timestamp=op_dict['op_timestamp'],
                                                       last_updated=op_dict['op_timestamp'],
                                                       type=Viewpoint.DEFAULT,
                                                       update_seq=0)

      labels = Follower.PERMISSION_LABELS + [Follower.PERSONAL]
      expected_follower = self.ValidateFollower(user_id=user_id,
                                                viewpoint_id=viewpoint_id,
                                                timestamp=op_dict['op_timestamp'],
                                                labels=labels,
                                                last_updated=op_dict['op_timestamp'],
                                                viewed_seq=0)

    # Validate User object.
    scratch_user_dict = deepcopy(user_dict)
    if ident_dict.get('authority', None) == 'Facebook' and user_dict.get('email', None):
      scratch_user_dict['facebook_email'] = user_dict['email']

    union_label = [] if is_prospective else [User.REGISTERED]
    existing_user = self.GetModelObject(User, user_id, must_exist=False)
    if existing_user is None:
      is_registering = False
      before_user_dict = None
      scratch_user_dict['private_vp_id'] = viewpoint_id
      scratch_user_dict['labels'] = union_label
    else:
      is_registering = not existing_user.IsRegistered()
      before_user_dict = existing_user._asdict()
      scratch_user_dict.update(before_user_dict)
      scratch_user_dict['labels'] = list(set(scratch_user_dict['labels']).union(union_label))

    expected_user = self.ValidateUpdateDBObject(User, **scratch_user_dict)

    # Validate AccountSettings object.
    settings = AccountSettings.CreateForUser(user_id)

    if device_dict is None:
      if self.GetModelObject(AccountSettings, settings.GetKey(), must_exist=False) is None:
        # First web device was registered, so validate that emails or sms messages are turned on.
        settings.push_alerts = AccountSettings.PUSH_NONE
        settings.email_alerts = AccountSettings.EMAIL_NONE
        settings.sms_alerts = AccountSettings.SMS_NONE

        identity_type, identity_value = Identity.SplitKey(ident_dict['key'])
        if identity_type == 'Email':
          settings.email_alerts = AccountSettings.EMAIL_ON_SHARE_NEW
        elif identity_type == 'Phone':
          settings.sms_alerts = AccountSettings.SMS_ON_SHARE_NEW
    else:
      if len(self.QueryModelObjects(Device, user_id)) == 0:
        # First mobile device was registered, so validate that emails and sms messages are
        # turned off and push alerts turned on.
        settings.push_alerts = AccountSettings.PUSH_ALL
        settings.email_alerts = AccountSettings.EMAIL_NONE
        settings.sms_alerts = AccountSettings.SMS_NONE

    self.ValidateUpdateDBObject(AccountSettings, **settings._asdict())

    # Validate Friend object.
    self.ValidateUpdateDBObject(Friend, user_id=user_id, friend_id=user_id)

    # Validate Identity object.
    existing_identity = self.GetModelObject(Identity, ident_dict['key'], must_exist=False)
    expected_ident = self.ValidateUpdateDBObject(Identity,
                                                 user_id=user_id,
                                                 **ident_dict)

    # Validate Device object.
    if device_dict is not None:
      update_dict = {'user_id': user_id,
                     'timestamp': util._TEST_TIME,
                     'last_access': util._TEST_TIME}
      if 'push_token' in device_dict:
        update_dict['alert_user_id'] = user_id
      update_dict.update(device_dict)
      expected_device = self.ValidateUpdateDBObject(Device, **update_dict)

      # Validate that any other devices with same push token have had their tokens revoked.
      if 'push_token' in device_dict:
        predicate = lambda d: d.device_id != expected_device.device_id and d.push_token == expected_device.push_token
        other_devices = self.QueryModelObjects(Device, predicate=predicate)
        for device in other_devices:
          self.ValidateUpdateDBObject(Device,
                                      user_id=device.user_id,
                                      device_id=device.device_id,
                                      push_token=None,
                                      alert_user_id=None)

    # Validate Contact objects.
    if existing_identity is None or is_registering:
      self.ValidateRewriteContacts(expected_ident.key, op_dict)

    # Validate contact notifications.
    self.ValidateContactNotifications(name, expected_ident.key, op_dict)

    # Validate Friend notifications.
    after_user_dict = self.GetModelObject(User, user_id)._asdict()
    if before_user_dict != after_user_dict and not is_prospective:
      invalidate = {'users': [user_id]}
      self.ValidateFriendNotifications('register friend', user_id, op_dict, invalidate)

    # Validate analytics entry for Register.
    if existing_user is None:
      # User is being created for the first time, it must have a CREATE_PROSPECTIVE analytics entry.
      analytics = Analytics.Create(entity='us:%d' % user_id,
                                   type=Analytics.USER_CREATE_PROSPECTIVE)
      self.ValidateCreateDBObject(Analytics, **analytics._asdict())

    if (not existing_user or is_registering) and not is_prospective:
      # User is being registered.
      analytics = Analytics.Create(entity='us:%d' % user_id,
                                   type=Analytics.USER_REGISTER)
      self.ValidateCreateDBObject(Analytics, **analytics._asdict())
Example #24
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 #25
0
    def _FetchGoogleContacts(self):
        """Do GMail specific data gathering and checking.
    Queries Google data API for contacts in JSON format.
    """
        # Track fetched contacts regardless of rank in order to dedup contacts retrieved from Google.
        assert self._identity.refresh_token is not None, self._identity

        if self._identity.expires and self._identity.expires < time.time():
            yield gen.Task(self._identity.RefreshGoogleAccessToken,
                           self._client)

        logging.info('fetching Google contacts for identity %r...' %
                     self._identity)
        http_client = httpclient.AsyncHTTPClient()
        # Google data API uses 1-based start index.
        start_index = 1
        retries = 0
        count = FetchContactsOperation._MAX_FETCH_COUNT
        while True:
            if retries >= FetchContactsOperation._MAX_FETCH_RETRIES:
                raise TooManyRetriesError(
                    'failed to fetch contacts %d times; aborting' % retries)
            logging.info('fetching next %d Google contacts for user %d' %
                         (count, self._user_id))
            url = FetchContactsOperation._GOOGLE_CONTACTS_URL + '?' + \
                urllib.urlencode({'max-results': count,
                                  'start-index': start_index,
                                  'alt': 'json'})
            response = yield gen.Task(http_client.fetch,
                                      url,
                                      method='GET',
                                      headers={
                                          'Authorization':
                                          'OAuth %s' %
                                          self._identity.access_token,
                                          'GData-Version':
                                          3.0
                                      })
            try:
                response_dict = www_util.ParseJSONResponse(response)['feed']
            except Exception as exc:
                logging.warning('failed to fetch Google contacts: %s' % exc)
                retries += 1
                continue

            # Temporarily log additional information to figure out why some responses don't seem to have "entry" fields.
            if 'entry' not in response_dict:
                logging.warning('Missing entry: %s' %
                                json.dumps(response_dict, indent=True))

            for c_dict in response_dict.get('entry', []):
                # Build identities_properties list from all emails/phone numbers associated with this contact.
                identities_properties = []
                # Process emails first so that if there are any emails, one of them will be first in the
                #   identities_properties list.  This will be *the* identity used for down-level client message
                #   migration.
                for email_info in c_dict.get('gd$email', []):
                    email = email_info.get('address', None)
                    if email is not None:
                        email_type = FetchContactsOperation._GOOGLE_TYPE_LOOKUP.get(
                            email_info.get('rel', None), None)
                        identity_properties = (
                            'Email:' + Identity.CanonicalizeEmail(email),
                            email_info.get('label', email_type))
                        if email_info.get('primary', False):
                            # Insert the primary email address at the head of the list.  Older clients will get this
                            #   as the only email address for this contact when they query_contacts.
                            identities_properties.insert(
                                0, identity_properties)
                        else:
                            identities_properties.append(identity_properties)
                for phone_info in c_dict.get('gd$phoneNumber', []):
                    # See RFC3966: "The tel URI for Telephone Numbers" for more information about this format.
                    #   It should be 'tel:' + E.164 format phone number.
                    phone = phone_info.get('uri', None)
                    if phone is not None and phone.startswith(
                            'tel:+') and Identity.CanCanonicalizePhone(
                                phone[4:]):
                        phone_type = FetchContactsOperation._GOOGLE_TYPE_LOOKUP.get(
                            phone_info.get('rel', None), None)
                        identities_properties.append(
                            ('Phone:' + Identity.CanonicalizePhone(phone[4:]),
                             phone_info.get('label', phone_type)))

                if len(identities_properties) == 0:
                    continue

                # Normalize name to None if empty.
                gd_name = c_dict.get('gd$name', None)
                if gd_name is not None:
                    names = {
                        'name':
                        gd_name.get('gd$fullName', {}).get('$t', None),
                        'given_name':
                        gd_name.get('gd$givenName', {}).get('$t', None),
                        'family_name':
                        gd_name.get('gd$familyName', {}).get('$t', None)
                    }
                else:
                    names = {
                        'name': None,
                        'given_name': None,
                        'family_name': None
                    }

                fetched_contact = Contact.CreateFromKeywords(
                    self._user_id,
                    identities_properties,
                    self._notify_timestamp,
                    Contact.GMAIL,
                    rank=None,
                    **names)
                self._fetched_contacts[
                    fetched_contact.contact_id] = fetched_contact

            # Prepare to fetch next batch.
            # Indexes are 1-based, so add 1 to max_index.
            if 'openSearch$totalResults' in response_dict:
                max_index = int(
                    response_dict['openSearch$totalResults']['$t']) + 1
            else:
                max_index = FetchContactsOperation._MAX_GOOGLE_CONTACTS + 1
            next_index = int(
                response_dict['openSearch$startIndex']['$t']) + len(
                    response_dict.get('entry', []))
            count = min(max_index - next_index,
                        FetchContactsOperation._MAX_FETCH_COUNT)
            if len(
                    self._fetched_contacts
            ) < FetchContactsOperation._MAX_GOOGLE_CONTACTS and count > 0:
                start_index = next_index
                retries = 0
                continue
            else:
                raise gen.Return()
Example #26
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)
Example #27
0
 def QueryIdentities(self, client, callback):
   """Queries the identities (if any) attached to this user and
   returns the list to the provided callback.
   """
   query_str = 'identity.user_id=%d' % self.user_id
   Identity.IndexQuery(client, query_str, col_names=None, callback=callback)
Example #28
0
 def _VisitIdentity(identity_key):
   """Unlink this identity from the user."""
   yield Identity.UnlinkIdentityOperation(client, user_id, identity_key.hash_key)