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
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')
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)
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)
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))
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'))
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)
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)
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))
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
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)
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))
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 }
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)
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
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)
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))
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)
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)
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)
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))
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)
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())
def SendVerifyIdMessage(cls, client, action, use_short_token, is_mobile_app, identity_key, user_id, user_name, **kwargs): """Sends a verification email or SMS message to the given identity. This message may directly contain an access code (e.g. if an SMS is sent), or it may contain a ShortURL link to a page which reveals the access code (e.g. if email was triggered by the mobile app). Or it may contain a link to a page which confirms the user's password and redirects them to the web site (e.g. if email was triggered by the web site). """ # Ensure that identity exists. identity = yield gen.Task(Identity.Query, client, identity_key, None, must_exist=False) if identity is None: identity = Identity.CreateFromKeywords(key=identity_key) yield gen.Task(identity.Update, client) identity_type, identity_value = Identity.SplitKey(identity.key) message_type = 'emails' if identity_type == 'Email' else 'messages' # Throttle the rate at which email/SMS messages can be sent to this identity. The updated # count will be saved by CreateAccessTokenURL. auth_throttle = identity.auth_throttle or {} per_min_dict, is_throttled = util.ThrottleRate( auth_throttle.get('per_min', None), VerifyIdBaseHandler._MAX_MESSAGES_PER_MIN, constants.SECONDS_PER_MINUTE) if is_throttled: # Bug 485: Silently do not send the email if throttled. We don't want to give user error # if they exit out of confirm code screen, then re-create account, etc. return per_day_dict, is_throttled = util.ThrottleRate( auth_throttle.get('per_day', None), VerifyIdBaseHandler._MAX_MESSAGES_PER_DAY, constants.SECONDS_PER_DAY) if is_throttled: raise InvalidRequestError(TOO_MANY_MESSAGES_DAY, message_type=message_type, identity_value=Identity.GetDescription( identity.key)) identity.auth_throttle = { 'per_min': per_min_dict, 'per_day': per_day_dict } # Create a ShortURL link that will supply the access token to the user when clicked. # Use a URL path like "idm/*" for the mobile app, and "idw/*" for the web. encoded_user_id = base64hex.B64HexEncode( util.EncodeVarLengthNumber(user_id), padding=False) group_id = '%s/%s' % ('idm' if is_mobile_app else 'idw', encoded_user_id) short_url = yield gen.Task(identity.CreateAccessTokenURL, client, group_id, use_short_token=use_short_token, action=action, identity_key=identity.key, user_name=user_name, **kwargs) # Send email/SMS in order to verify that the user controls the identity. if identity_type == 'Email': args = VerifyIdBaseHandler._GetAuthEmail(client, action, use_short_token, user_name, identity, short_url) yield gen.Task(EmailManager.Instance().SendEmail, description=action, **args) else: args = VerifyIdBaseHandler._GetAccessTokenSms(identity) yield gen.Task(SMSManager.Instance().SendSMS, description=action, **args) # In dev servers, display a popup with the generated code (OS X 10.9-only). if (options.options.localdb and platform.system() == 'Darwin' and platform.mac_ver()[0] == '10.9'): subprocess.call([ 'osascript', '-e', 'display notification "%s" with title "Viewfinder"' % identity.access_token ])
def _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()
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)
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)
def _VisitIdentity(identity_key): """Unlink this identity from the user.""" yield Identity.UnlinkIdentityOperation(client, user_id, identity_key.hash_key)