def _MakeUsageDict(): """Lookup the user's accounting entries and build a USAGE_METADATA dict.""" # Make sure that the accounting model is up-to-date. validator.ValidateAccounting() user_hash = '%s:%d' % (Accounting.USER_SIZE, user_id) def _AccountingAsDict(sort_key): act = validator.GetModelObject(Accounting, DBKey(user_hash, sort_key), must_exist=False) if act is None: return None act_dict = act._asdict() # GetModelObject just returns everything, with no possibility of selecting the columns. act_dict.pop('hash_key', None) act_dict.pop('sort_key', None) act_dict.pop('op_ids', None) return act_dict usage_dict = {} util.SetIfNotNone(usage_dict, 'owned_by', _AccountingAsDict(Accounting.OWNED_BY)) util.SetIfNotNone(usage_dict, 'shared_by', _AccountingAsDict(Accounting.SHARED_BY)) util.SetIfNotNone(usage_dict, 'visible_to', _AccountingAsDict(Accounting.VISIBLE_TO)) if len(usage_dict.keys()) > 0: return usage_dict return None
def _StartAuthViewfinder(self, action): """Validates the request and prepares to authenticate by creating user, identity, and device dicts. Returns a tuple of (user_dict, ident_dict, device_dict). """ # Validate the request. if action == 'register': schema = json_schema.REGISTER_VIEWFINDER_REQUEST elif action == 'login': schema = json_schema.LOGIN_VIEWFINDER_REQUEST else: schema = json_schema.AUTH_VIEWFINDER_REQUEST # Strip out names in all but register case for older clients. if action == 'register': migrators = _REQUEST_MIGRATORS else: migrators = _REQUEST_MIGRATORS + [message.SUPPRESS_AUTH_NAME] yield gen.Task(self._StartJSONRequest, action, self.request, schema, migrators=migrators) auth_info_dict = self._request_message.dict['auth_info'] # Validate the identity key. identity_key = auth_info_dict['identity'] identity_type, identity_value = AuthViewfinderHandler._ValidateIdentityKey( identity_key) # Create identity dict. ident_dict = {'key': identity_key, 'authority': 'Viewfinder'} # Create user dict. user_dict = {identity_type.lower(): identity_value} if action == 'register': user_dict['name'] = auth_info_dict['name'] util.SetIfNotNone(user_dict, 'given_name', auth_info_dict.get('given_name', None)) util.SetIfNotNone(user_dict, 'family_name', auth_info_dict.get('family_name', None)) # If password is specified, compute hash and generate salt (if not already generated). password = self._request_message.dict['auth_info'].get( 'password', None) if password is not None: # Generate password hash and salt. pwd_hash, salt = password_util.GeneratePasswordHash(password) user_dict['pwd_hash'] = pwd_hash user_dict['salt'] = salt # Create device_dict. device_dict = self._request_message.dict.get('device', None) # Validate input and fill out the dicts. yield self._PrepareAuthUser(user_dict, ident_dict, device_dict) raise gen.Return((user_dict, ident_dict, device_dict))
def _handle_request_exception(self, e): """Handles presentation of an exception condition to the user, either as an HTML error page, or as a JSON error response. """ try: status, message = www_util.HTTPInfoFromException(e) self.set_status(status) # Write JSON error response if this was a user-level interactive request, otherwise # write an HTML error response. if self._IsInteractiveRequest(): title = 'Unknown Error' if status == 500: logging.error('failure processing %s' % getattr(self, 'api_name', None), exc_info=sys.exc_info()) message = 'We\'re sorry but an unforeseen error occurred; please try again later.' else: logging.warning('[%s] %s' % (type(e).__name__, message)) if status in _ERROR_MAP: title = _ERROR_MAP[status] self.render('info.html', title=title, message=message, button_url='/', button_text='home') else: if status == 500: logging.error('failure processing %s:\n%s', escape.utf8(getattr(self, 'api_name', 'N/A')), escape.utf8(self.request.body), exc_info=sys.exc_info()) else: logging.warning('[%s] %s' % (type(e).__name__, message)) error_dict = { 'error': { 'message': message if message else 'Unknown error.' } } util.SetIfNotNone(error_dict['error'], 'method', getattr(self, 'api_name', None)) util.SetIfNotNone(error_dict['error'], 'id', getattr(e, 'id', None)) validictory.validate(error_dict, json_schema.ERROR_RESPONSE) self.write(error_dict) self.finish() except Exception: # This is the exception handler of last resort - if we don't finish the request here, nothing will, # and it will just leak and time out on the client. logging.exception( 'exception in BaseHandler._handle_request_exception') self.set_status(500) self.finish() return True
def MakeMetadataDict(self): """Projects all follower attributes that the follower himself can see.""" foll_dict = {'follower_id': self.user_id} util.SetIfNotNone(foll_dict, 'adding_user_id', self.adding_user_id) util.SetIfNotNone(foll_dict, 'viewed_seq', self.viewed_seq) if self.labels is not None: # Normalize labels property for easier testing. foll_dict['labels'] = sorted(self.labels) return foll_dict
def MakeFriendMetadataDict(self): """Projects a subset of the follower attributes that should be provided to another user that is on the same viewpoint as this follower. """ foll_dict = {'follower_id': self.user_id} util.SetIfNotNone(foll_dict, 'adding_user_id', self.adding_user_id) util.SetIfNotNone(foll_dict, 'follower_timestamp', self.timestamp) if self.IsUnrevivable(): # Only project labels if the follower has left the viewpoint entirely. foll_dict['labels'] = [Follower.REMOVED, Follower.UNREVIVABLE] return foll_dict
def _CreateRegisterRequest(device_dict=None, auth_info_dict=None, synchronous=True, version=message.MAX_SUPPORTED_MESSAGE_VERSION): """Returns a new AUTH_REQUEST dict that has been populated with information from the specified dicts. """ request_dict = {'headers': {'version': version}} util.SetIfNotNone(request_dict, 'device', device_dict) util.SetIfNotNone(request_dict, 'auth_info', auth_info_dict) if synchronous: request_dict['headers']['synchronous'] = True return request_dict
def _Update(self): """Updates the database: 1. Creates episode if it did not exist, or sets episode's location/placemark. 2. Creates posts that did not previously exist. 3. Creates photos that did not previously exist. 4. Updates photo MD5 values if they were given in a re-upload. """ # Set episode location/placemark. if self._set_location or self._set_placemark: for ph_dict in self._ph_dicts: if 'location' not in self._ep_dict and 'location' in ph_dict: self._ep_dict['location'] = ph_dict['location'] if 'placemark' not in self._ep_dict and 'placemark' in ph_dict: self._ep_dict['placemark'] = ph_dict['placemark'] # Create new episode if it did not exist at the beginning of the operation. if self._episode_id in self._new_ids: yield gen.Task(Episode.CreateNew, self._client, **self._ep_dict) # Update existing episode's location/placemark. elif self._set_location or self._set_placemark: yield gen.Task(self._episode.UpdateExisting, self._client, location=self._ep_dict.get('location', None), placemark=self._ep_dict.get('placemark', None)) # Create posts and photos that did not exist at the beginning of the operation. tasks = [] for ph_dict in self._ph_dicts: # Only create post, user_photo and photo if photo did not exist at the beginning of the operation. if ph_dict['photo_id'] in self._new_ids: # Create user photo record if asset keys were specified. asset_keys = ph_dict.pop('asset_keys', None) if asset_keys is not None: tasks.append(UserPhoto.CreateNew(self._client, user_id=self._user.user_id, photo_id=ph_dict['photo_id'], asset_keys=asset_keys)) tasks.append(Photo.CreateNew(self._client, **ph_dict)) tasks.append(Post.CreateNew(self._client, episode_id=self._episode_id, photo_id=ph_dict['photo_id'])) else: # Update the photo if any MD5 attributes need to be overwritten. This is allowed if the photo image # has not yet been uploaded. This can happen if the MD5 value has changed on the client due to an IOS # upgrade. md5_dict = {'photo_id': ph_dict['photo_id']} util.SetIfNotNone(md5_dict, 'tn_md5', ph_dict['tn_md5']) util.SetIfNotNone(md5_dict, 'med_md5', ph_dict['med_md5']) util.SetIfNotNone(md5_dict, 'full_md5', ph_dict['full_md5']) util.SetIfNotNone(md5_dict, 'orig_md5', ph_dict['orig_md5']) if md5_dict: yield Photo.UpdateExisting(self._client, **md5_dict) yield tasks
def _MakeMetadataDict(friend_user, forward_friend, reverse_friend): user_dict = {'user_id': friend_user.user_id} # Certain labels are visible even to non-friends. labels = list( friend_user.labels.intersection([User.REGISTERED, User.TERMINATED])) # User profile attributes should only be visible to those who consider caller a friend. if reverse_friend is not None: util.SetIfNotNone(user_dict, 'name', friend_user.name) util.SetIfNotNone(user_dict, 'given_name', friend_user.given_name) util.SetIfNotNone(user_dict, 'family_name', friend_user.family_name) util.SetIfNotNone(user_dict, 'email', friend_user.email) util.SetIfNotNone(user_dict, 'picture', friend_user.picture) util.SetIfNotNone(user_dict, 'merged_with', friend_user.merged_with) labels.append('friend') user_dict['labels'] = labels if friend_user.user_id == user_id: # Subscriptions don't currently use the model so we can't access them here, # but since most tests don't have subscriptions we just turn off validation # in the ones that do. user_dict['private'] = {'subscriptions': [], 'user_identities': []} if friend_user.pwd_hash is None: user_dict['private']['no_password'] = True db_key = DBKey('us:%d' % user_id, AccountSettings.GROUP_NAME) settings = validator.GetModelObject(AccountSettings, db_key, must_exist=False) if settings is not None: settings_dict = user_dict['private'].setdefault( 'account_settings', {}) util.SetIfNotNone(settings_dict, 'email_alerts', settings.email_alerts) util.SetIfNotNone(settings_dict, 'sms_alerts', settings.sms_alerts) util.SetIfNotEmpty(settings_dict, 'storage_options', list(settings.storage_options)) predicate = lambda ident: ident.user_id == user_id for expected_ident in validator.QueryModelObjects( Identity, predicate=predicate): ident_dict = {'identity': expected_ident.key} if expected_ident.authority is not None: ident_dict['authority'] = expected_ident.authority user_dict['private']['user_identities'].append(ident_dict) # Add attributes assigned to the friend by the user himself (such as nickname). if forward_friend is not None: util.SetIfNotNone(user_dict, 'nickname', forward_friend.nickname) return user_dict
def MakeUserMetadataDict(self, client, viewer_user_id, forward_friend, reverse_friend, callback): """Projects a subset of the user attributes that can be provided to the viewing user (using the same schema as the query_users service method). The 'forward_friend' is viewer_user_id => friend_user_id, and the 'reverse_friend' is the reverse. This user's profile information will only be provided to the viewer if the viewer is a reverse friend (i.e. user considers the viewer a friend). The 'private' block will be returned only if viewer_user_id == self.user_id. """ user_dict = {'user_id': self.user_id} # First, populate basic user data, but only if the user considers the viewer a friend. if reverse_friend is not None: for attr_name in User._USER_FRIEND_ATTRIBUTES: util.SetIfNotNone(user_dict, attr_name, getattr(self, attr_name, None)) user_dict['labels'] = self.MakeLabelList(True) else: # Set labels which are visible to non-friends. user_dict['labels'] = self.MakeLabelList(False) # Now project friend attributes. for attr_name in Friend.FRIEND_ATTRIBUTES: util.SetIfNotNone(user_dict, attr_name, getattr(forward_friend, attr_name, None)) # Now fill out private attributes if this user is also the viewing user. if viewer_user_id == self.user_id: user_dict['private'] = {} if self.pwd_hash is None: user_dict['private']['no_password'] = True subs, settings = yield [gen.Task(Subscription.QueryByUser, client, user_id=self.user_id), gen.Task(AccountSettings.QueryByUser, client, self.user_id, None, must_exist=False)] sub_dicts = [sub.MakeMetadataDict() for sub in subs] user_dict['private']['subscriptions'] = sub_dicts if settings is not None: user_dict['private']['account_settings'] = settings.MakeMetadataDict() def _MakeIdentityDict(ident): i_dict = {'identity': ident.key} if ident.authority is not None: i_dict['authority'] = ident.authority return i_dict query_expr = 'identity.user_id=%d' % self.user_id identities = yield gen.Task(Identity.IndexQuery, client, query_expr, ['key', 'authority']) user_dict['private']['user_identities'] = [_MakeIdentityDict(ident) for ident in identities] callback(user_dict)
def ValidateFollower(self, user_id, viewpoint_id, labels, last_updated, timestamp=None, adding_user_id=None, viewed_seq=None): """Validates that Follower and Followed records have been created or updated in the database for user "user_id" and viewpoint "viewpoint_id". Returns the follower. """ follower_dict = {'user_id': user_id, 'viewpoint_id': viewpoint_id, 'labels': labels} util.SetIfNotNone(follower_dict, 'timestamp', timestamp) util.SetIfNotNone(follower_dict, 'adding_user_id', adding_user_id) util.SetIfNotNone(follower_dict, 'viewed_seq', viewed_seq) follower = self.ValidateUpdateDBObject(Follower, **follower_dict) self._ValidateUpdateFollowed(user_id, viewpoint_id, None, last_updated) return follower
def MakeMetadataDict(self): """Project a subset of subscription attributes that can be provided to the user.""" sub_dict = {} for attr_name in Subscription._JSON_ATTRIBUTES: util.SetIfNotNone(sub_dict, attr_name, getattr(self, attr_name, None)) if self.extra_info: sub_dict['extra_info'] = deepcopy(self.extra_info) return sub_dict
def CreateUserCookieDict(self, user_id, device_id, user_name=None, viewpoint_id=None, confirm_time=None, is_session_cookie=None): """Creates a user cookie dict from the given arguments.""" user_dict = { 'user_id': user_id, 'device_id': device_id, 'server_version': self.settings['server_version'] } util.SetIfNotNone(user_dict, 'name', user_name) util.SetIfNotNone(user_dict, 'viewpoint_id', viewpoint_id) util.SetIfNotNone(user_dict, 'confirm_time', confirm_time) util.SetIfNotNone(user_dict, 'is_session_cookie', is_session_cookie) return user_dict
def MakeMetadataDict(self): """Project a subset of account settings attributes that can be provided to the user.""" settings_dict = {} for attr_name in AccountSettings._JSON_ATTRIBUTES: value = getattr(self, attr_name, None) if isinstance(value, frozenset): util.SetIfNotEmpty(settings_dict, attr_name, list(value)) else: util.SetIfNotNone(settings_dict, attr_name, value) return settings_dict
def GenerateUploadUrl(self, key, content_type=None, content_md5=None, expires_in=constants.SECONDS_PER_DAY, max_bytes=5 << 20): """Generates a URL for a PUT request to allow a client to store the specified key directly to S3 from a browser or mobile client. 'max_bytes' limits the upload file size to prevent D.O.S. attacks. TODO(andy) max_bytes is not currently enforced, need to fix this. """ headers = {} util.SetIfNotNone(headers, 'Content-Type', content_type) util.SetIfNotNone(headers, 'Content-MD5', content_md5) return self._s3_conn.generate_url(expires_in, 'PUT', self._bucket_name, key, headers=headers or None)
def _MakeNotificationDict(notification): """Create a viewpoint dict from the followed object plus its referenced viewpoint object. """ notification_dict = { 'notification_id': notification.notification_id, 'name': notification.name, 'sender_id': notification.sender_id, 'timestamp': notification.timestamp } util.SetIfNotNone(notification_dict, 'op_id', notification.op_id) if notification.update_seq is not None or notification.viewed_seq is not None: vp_dict = notification_dict.setdefault('inline', {}).setdefault( 'viewpoint', {}) vp_dict['viewpoint_id'] = notification.viewpoint_id util.SetIfNotNone(vp_dict, 'update_seq', notification.update_seq) util.SetIfNotNone(vp_dict, 'viewed_seq', notification.viewed_seq) if notification.activity_id is not None: viewpoint_id = notification.viewpoint_id activity = validator.GetModelObject( Activity, DBKey(viewpoint_id, notification.activity_id)) activity_dict = activity.MakeMetadataDict() notification_dict.setdefault('inline', {})['activity'] = activity_dict if activity.name == 'post_comment' and notification.invalidate is None: comment_id = activity_dict['post_comment']['comment_id'] comment = validator.GetModelObject( Comment, DBKey(viewpoint_id, comment_id)) notification_dict['inline']['comment'] = comment._asdict() invalidate = notification.GetInvalidate() if invalidate is not None: invalidate.pop('headers') notification_dict['invalidate'] = invalidate return notification_dict
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 _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 GenerateUrl(self, key, method='GET', cache_control=None, expires_in=constants.SECONDS_PER_DAY, content_type=None): """Generates a URL that can be used retrieve the specified key. If 'cache_control' is given, it will result in a Cache-Control header being added to any S3 responses. The expires_in parameter specifies how long (in seconds) the URL is valid for. content-type forces the content-type of the downloaded file. eg: use text/plain for logs. """ response_headers = {} util.SetIfNotNone(response_headers, 'response-cache-control', cache_control) util.SetIfNotNone(response_headers, 'response-content-type', content_type) return self._s3_conn.generate_url(expires_in, method, self._bucket_name, key, response_headers=response_headers or None)
def SendEmailToUser(template, user): assert user.email is not None, user unsubscribe_cookie = User.CreateUnsubscribeCookie( user.user_id, AccountSettings.MARKETING) unsubscribe_url = 'https://%s/unsubscribe?%s' % ( options.domain, urlencode(dict(cookie=unsubscribe_cookie))) # Create arguments for the email template. fmt_args = { 'first_name': user.given_name, 'unsubscribe_url': unsubscribe_url } # Create arguments for the email. args = { 'from': EmailManager.Instance().GetInfoAddress(), 'fromname': 'Viewfinder', 'to': user.email, 'subject': options.email_subject } util.SetIfNotNone(args, 'toname', user.name) args['html'] = template.generate(is_html=True, **fmt_args) args['text'] = template.generate(is_html=False, **fmt_args) print 'Sending marketing email to %s (%s) (#%d)' % (user.email, user.name, user.user_id) if options.test_mode: global _is_first_email if _is_first_email: print args['html'] _is_first_email = False else: # Remove extra whitespace in the HTML (seems to help it avoid Gmail spam filter). args['html'] = escape.squeeze(args['html']) yield gen.Task(EmailManager.Instance().SendEmail, description='marketing email', **args)
def _TestGenerateMergeToken(self, identity_key, user_cookie, error_if_linked=None): """Invokes the merge_token auth API that triggers the email of a Viewfinder access token. Validates that an identity was created. Returns a source_identity_dict that can be passed directly to merge_accounts. """ url = self._tester.GetUrl('/merge_token/viewfinder') request_dict = { 'headers': { 'version': message.MAX_SUPPORTED_MESSAGE_VERSION, 'synchronous': True }, 'identity': identity_key } util.SetIfNotNone(request_dict, 'error_if_linked', error_if_linked) auth_test._SendAuthRequest(self._tester, url, 'POST', user_cookie=user_cookie, request_dict=request_dict) identity = self._RunAsync(Identity.Query, self._client, identity_key, None) # Validate the identity. self._validator.ValidateUpdateDBObject( Identity, key=identity_key, authority='Viewfinder', user_id=identity.user_id, access_token=identity.access_token, expires=identity.expires) return { 'identity': identity.key, 'access_token': identity.access_token }
def _Check(self): """Gathers pre-mutation information: 1. Queries for existing followers and viewpoint. 2. Checkpoints list of followers that need to be revived. Validates the following: 1. Permission to update viewpoint metadata. """ # Get the viewpoint to be modified, along with the follower that is adding the additional users. # This state will not be changed by add followers, and so doesn't need to be part of the checkpoint. self._viewpoint, self._follower = yield gen.Task( Viewpoint.QueryWithFollower, self._client, self._user_id, self._viewpoint_id) if self._viewpoint is None: raise InvalidRequestError( 'Viewpoint "%s" does not exist and so cannot be updated.' % (self._viewpoint_id)) if self._follower is None or not self._follower.CanContribute(): raise PermissionError( 'User %d does not have permission to update viewpoint "%s".' % (self._user_id, self._viewpoint_id)) # Get all existing followers. self._followers, _ = yield gen.Task(Viewpoint.QueryFollowers, self._client, self._viewpoint_id, limit=Viewpoint.MAX_FOLLOWERS) # Check that cover photo exists in this viewpoint. cover_photo = self._vp_dict.get('cover_photo', None) if cover_photo is not None: if self._viewpoint.IsDefault(): # cover_photo isn't supported creating default viewpoint. raise InvalidRequestError( 'A cover photo cannot be set on your library.') cover_photo_episode, cover_photo_post = yield [ gen.Task(Episode.Query, self._client, cover_photo['episode_id'], None, must_exist=False), gen.Task(Post.Query, self._client, cover_photo['episode_id'], cover_photo['photo_id'], None, must_exist=False) ] if cover_photo_post is None: raise InvalidRequestError( 'The requested cover photo does not exist.') if cover_photo_post.IsUnshared(): raise PermissionError( 'The requested cover photo has been unshared.') if cover_photo_episode.viewpoint_id != self._viewpoint_id: raise InvalidRequestError( 'The requested cover photo is not in viewpoint "%s".' % self._viewpoint_id) # Start populating the checkpoint if this the first time the operation has been run. if self._op.checkpoint is None: # Get list of followers which have removed themselves from the viewpoint and will need to be revived. self._revive_follower_ids = self._GetRevivableFollowers( self._followers) # Get previous values of title and cover_photo, if they are being updated. self._prev_values = {} if 'title' in self._vp_dict: util.SetIfNotNone(self._prev_values, 'prev_title', self._viewpoint.title) if 'cover_photo' in self._vp_dict: util.SetIfNotNone(self._prev_values, 'prev_cover_photo', self._viewpoint.cover_photo) # Set checkpoint. # Followers to revive need to be check-pointed because they are changed in the UPDATE phase. # If we fail after UPDATE, but before NOTIFY, we would not send correct notifications on retry. checkpoint = {'revive': self._revive_follower_ids} util.SetIfNotEmpty(checkpoint, 'prev', self._prev_values) yield self._op.SetCheckpoint(self._client, checkpoint) else: # Restore state from checkpoint. self._revive_follower_ids = self._op.checkpoint['revive'] self._prev_values = self._op.checkpoint.get('prev', {})
def _AuthUser(self, user_dict, ident_dict, device_dict, confirmed=False): """Called when a requester has been authenticated as a Viewfinder user by a trusted authority that provides additional information about the user, such as name, email, gender, etc. At this point, we can trust that the identity key provided in "ident_dict" is controlled by the calling user. Completes the authentication action in two steps: first, makes sure the user id and device id are retrieved or allocated as necessary; second, starts a user registration operation and returns a login cookie. If "confirmed" is True, then the "confirm_time" field in the user cookie is set, indicating the time at which the user confirmed their control of the identity via email or SMS. This type of cookie is necessary to perform certain high-privilege operations, such as updating the password. Registration is synchronous, meaning that the caller will wait until it is complete. This ensures that when the caller tries to login immediately following this call, the new user will be created and ready. """ before_user = yield gen.Task(self._PrepareAuthUser, user_dict, ident_dict, device_dict) # Map auth attribute names to those used by User schema and exclude any attributes that are not yet stored # in the User table. scratch_user_dict = {'user_id': user_dict['user_id']} for k, v in user_dict.items(): user_key = AuthHandler._AUTH_ATTRIBUTE_MAP.get(k, None) if user_key is not None: if getattr(before_user, user_key) is None: scratch_user_dict[user_key] = v # Set facebook email if it has not yet been set. if user_key == 'email' and ident_dict[ 'authority'] == 'Facebook': if getattr(before_user, 'facebook_email') is None: scratch_user_dict['facebook_email'] = v # If the device id is not present, then allocate it now. if device_dict is not None and 'device_id' not in device_dict: device_dict['device_id'] = yield gen.Task(Device._allocator.NextId, self._client) # Make synchronous request to ensure user is fully created before returning. request = { 'headers': { 'synchronous': True }, 'user_dict': scratch_user_dict, 'ident_dict': ident_dict, 'device_dict': device_dict } op = yield gen.Task( Operation.CreateAndExecute, self._client, user_dict['user_id'], device_dict['device_id'] if device_dict is not None else before_user.webapp_dev_id, 'RegisterUserOperation.Execute', request) if self._action == 'link': # Now make asynchronous request (or synchronous if requested by client) to fetch contacts. # Fetching contacts can take a long time, so best to do this in the background by default. request = { 'key': ident_dict['key'], 'user_id': user_dict['user_id'] } if self._IsInteractiveRequest( ) or self._request_message.dict['headers'].get( 'synchronous', False): request['headers'] = {'synchronous': True} op = yield gen.Task( Operation.CreateAndExecute, self._client, user_dict['user_id'], device_dict['device_id'] if device_dict is not None else before_user.webapp_dev_id, 'FetchContactsOperation.Execute', request) # Get the user that was registered by the operation. after_user = yield gen.Task(User.Query, self._client, user_dict['user_id'], None) # If the identity was confirmed via email/SMS, set the cookie "confirm_time", which allows # the cookie to authorize higher privilege operations, such as setting the user password. confirm_time = util.GetCurrentTimestamp() if confirmed else None # Create the user cookie dict that will be returned to the caller. device_id = after_user.webapp_dev_id if device_dict is None else device_dict[ 'device_id'] user_cookie_dict = self.CreateUserCookieDict(after_user.user_id, device_id, after_user.name, confirm_time=confirm_time) # Sets the user cookie and finishes the request. if self._IsInteractiveRequest(): self.SetUserCookie(user_cookie_dict) self._FinishInteractiveRequest() else: response_dict = {'user_id': user_dict['user_id']} if device_dict is not None: response_dict['device_id'] = device_dict['device_id'] use_session_cookie = self._request_message.dict.get( 'use_session_cookie', None) util.SetIfNotNone(user_cookie_dict, 'is_session_cookie', use_session_cookie) self.SetUserCookie(user_cookie_dict) self._FinishJSONRequest(op, response_dict, json_schema.AUTH_RESPONSE)
def _TestUpdateViewpoint(tester, user_cookie, request_dict): """Called by the ServiceTester in order to test update_viewpoint service API call. """ from viewfinder.backend.www.test.update_follower_test import _ValidateUpdateFollower validator = tester.validator user_id, device_id = tester.GetIdsFromCookie(user_cookie) request_dict = deepcopy(request_dict) viewpoint_id = request_dict['viewpoint_id'] # Send update_viewpoint request. actual_dict = tester.SendRequest('update_viewpoint', user_cookie, request_dict) op_dict = tester._DeriveNotificationOpDict(user_id, device_id, request_dict) # Get previous values for title and cover_photo. viewpoint = validator.GetModelObject(Viewpoint, viewpoint_id) prev_title = viewpoint.title prev_cover_photo = viewpoint.cover_photo # Validate case where only follower attributes are specified (uses update_follower code path). follower_columns = set([ 'user_id', 'viewpoint_id', 'labels', 'viewed_seq', 'activity', 'headers' ]) if all(attr in follower_columns for attr in request_dict.keys()): # Validate Follower object. foll_dict = {'viewpoint_id': request_dict['viewpoint_id']} util.SetIfNotNone(foll_dict, 'labels', request_dict.pop('labels', None)) util.SetIfNotNone(foll_dict, 'viewed_seq', request_dict.pop('viewed_seq', None)) _ValidateUpdateFollower(tester, user_cookie, op_dict, foll_dict) else: # Validate Viewpoint object. vp_dict = deepcopy(request_dict) vp_dict.pop('headers', None) vp_dict.pop('labels', None) vp_dict.pop('viewed_seq', None) vp_dict.pop('activity', None) viewpoint = validator.ValidateUpdateDBObject(Viewpoint, **vp_dict) # Need to revive before validating the follower, below. This also happens in # validator.ValidateFollowerNotifications(), below. validator.ValidateReviveRemovedFollowers(viewpoint_id, op_dict) # Validate activity and notifications for the update. invalidate = { 'viewpoints': [{ 'viewpoint_id': viewpoint_id, 'get_attributes': True }] } # Validate notifications to followers. activity_dict = { 'name': 'update_viewpoint', 'activity_id': request_dict['activity']['activity_id'], 'timestamp': request_dict['activity']['timestamp'], 'viewpoint_id': viewpoint_id } if 'title' in vp_dict and prev_title is not None: util.SetIfNotNone(activity_dict, 'prev_title', prev_title) if 'cover_photo' in vp_dict and prev_cover_photo is not None: util.SetIfNotNone(activity_dict, 'prev_cover_photo', prev_cover_photo) validator.ValidateFollowerNotifications(viewpoint_id, activity_dict, op_dict, invalidate) tester._CompareResponseDicts('update_viewpoint', user_id, request_dict, {}, actual_dict) return actual_dict