def build_link_header(self, results, order): n_cur = getattr(results, 'next_cursor', None) p_cur = getattr(results, 'previous_cursor', None) l_cur = getattr(results, 'last_cursor', None) if not n_cur and not p_cur: # These results don't have cursors, so don't use a link header. return None first_url = util.set_query_parameters( self.request.path_qs, cursor=None) previous_url = util.set_query_parameters( self.request.path_qs, cursor=p_cur.urlsafe()) next_url = util.set_query_parameters( self.request.path_qs, cursor=n_cur.urlsafe()) if l_cur: # SQL models _can_ tell us a cursor for the last page. last_url = util.set_query_parameters( self.request.path_qs, cursor=l_cur.urlsafe()) else: # The Datastore usually can't tell us a cursor for the last # page of an index. Cheat by taking the first page of a # reverse-ordered query. last_url = util.set_query_parameters( self.request.path_qs, cursor=None, order=reverse_order_str(order)) links = [ '<{}>;rel=self'.format(self.request.path_qs), '<{}>;rel=first'.format(first_url), '<{}>;rel=previous'.format(previous_url), '<{}>;rel=next'.format(next_url), '<{}>;rel=last'.format(last_url), ] return ','.join(links)
def outer(self, *args, **kwargs): ## BEFORE GET ## # Is the user returning from a google authentication page? If so, # examine the credentials in their cookie and attempt to log them # in. logging.info(u"MetaView Outer: ".format(self.request)) if (self.request.get('google_login') == 'true'): if self.get_current_user(): # Don't try to log someone in if they already are, just # clear the URL param. refresh_url = util.set_query_parameters( self.request.url, google_login='', ) self.redirect(refresh_url) else: # This will set up a redirect, so make sure to return # afterwards. self.handle_google_response() return ## INHERITING GET HANDLER RUNS HERE ## return_value = method(self, *args, **kwargs) ## AFTER GET ## # nothing here... yet... return return_value
def test_get_pdf_token_allowed(self): """You don't need to be logged in at all, if your token is right.""" (other, teammate, contact, captain, super_admin, team, classroom, report_dict) = self.test_post_team_pdf() # Put in a classroom report for the same team so we know we can # correctly select the team-level one. classReport = Report.create( team_id=team.uid, classroom_id=classroom.uid, filename='classroom.pdf', gcs_path='/mybucket/upload/classroom.pdf', size=1000000, content_type='application/pdf', ) classReport.put() path = '/api/teams/{team_id}/reports/{filename}'.format( team_id=team.uid, filename=report_dict['filename'], ) endpoint_str = util.get_endpoint_str(method='GET', path=path) jwt = jwt_helper.encode({'allowed_endpoints': [endpoint_str]}), url = util.set_query_parameters(path, token=jwt) response = self.testapp.get(url) # asserts 200 self.assert_pdf_response(report_dict, response)
def dispatch(self): if self.datastore_connected(): # Call the overridden dispatch(), which has the effect of running # the get() or post() etc. of the inheriting class. BaseHandler.dispatch(self) else: # Sometimes the datastore doesn't respond. I really don't know why. # Wait a bit and try again. attempts = int(self.request.get('connection_attempts', 0)) + 1 if attempts <= 10: time.sleep(1) self.redirect( util.set_query_parameters(self.request.url, connection_attempts=attempts)) else: logging.error('Could not connect to datastore after 10 tries.') # Since this is a view, it's important to not display weird # devvy text to the user. The least harmful thing I can come # up with is showing them the home page. jinja_environment = jinja2.Environment( autoescape=True, loader=jinja2.FileSystemLoader('templates'), ) self.response.write( jinja_environment.get_template('home.html').render())
def http_not_found(self, **kwargs): """Respond with a 404. Example use: class Foo(ViewHandler): def get(self): return self.http_not_found() """ # default parameters that all views get user = self.get_current_user() # Sets up the google sign in link, used in modal on all pages, which # must include a special flag to alert this handler that google # credentials are present in the cookie. It should also incorporate any # redirect already set in the URL. redirect = str(self.request.get('redirect')) or self.request.url google_redirect = util.set_query_parameters(redirect, google_login='******') google_login_url = app_engine_users.create_login_url(google_redirect) kwargs['user'] = user kwargs['google_login_url'] = google_login_url kwargs['hosting_domain'] = os.environ['HOSTING_DOMAIN'] kwargs['share_url'] = self.request.url kwargs['google_client_id'] = config.google_client_id # Determine which Facebook app depending on environment kwargs['localhost'] = False if util.is_localhost(): kwargs['localhost'] = True # Fetch all themes and topics for navigation courses = self.api.get('Theme') if courses: # fetch topics for each theme course_topic_ids = [ id for course in courses for id in course.topics ] course_topics = self.api.get_by_id(course_topic_ids) # associate topics with appropriate courses for course in courses: course.associate_topics(course_topics) # Special case for "Teachers" kit if course.name == 'Growth Mindset for Teachers': kwargs['teacher_topics'] = course.topics_list kwargs['courses'] = courses self.error(404) jinja_environment = self.get_jinja_environment() template = jinja_environment.get_template('404.html') self.response.write(template.render(kwargs))
def handle_google_response(self): """Figure out the results of the user's interaction with google. Attempt to login a/o register, then refresh to clear temporary url parameters. """ logging.info("Handling a google login response.") error_code = None response = self.authenticate('google') logging.info(u"Response is: {}".format(response)) if isinstance(response, User): user = response logging.info(u"User {} found, logging them in.".format(user.email)) elif (('email_exists' in response) or (response == 'credentials_missing')): # Provide the error code to the template so the UI can advise # the user. error_code = response elif response == 'credentials_invalid': logging.info("There's no record of this google user, registering.") response = self.register('google') if isinstance(response, User): user = response logging.info(u"Registered {}.".format(user.email)) else: # This will get written into the template, and the UI can # display an appropriate message. error_code = response logging.info("Error in auto-registering google user.") # Now that google's response has been handled, refresh the # request. This will create one of two behaviors: # * If the user was correctly logged in a/o registered, they get # the requested page, ready to use, no complications, no params. # * If there was an error, an error code is available about why, # and the url fragment/hash will trigger the login modal so a # message can be displayed. params = {'google_login': ''} # means remove this parameter new_fragment = '' # means remove hash/fragment if error_code: logging.info("Error code: {}.".format(error_code)) params['google_login_error'] = error_code new_fragment = 'login' refresh_url = util.set_query_parameters(self.request.url, new_fragment=new_fragment, **params) self.redirect(refresh_url)
def participation_query_url(cycle, classrooms): if util.is_localhost(): protocol = 'http' neptune_domain = 'localhost:8080' else: protocol = 'https' neptune_domain = os.environ['NEPTUNE_DOMAIN'] url = '{protocol}://{domain}/api/project_cohorts/participation'.format( protocol=protocol, domain=neptune_domain, ) return util.set_query_parameters( url, uid=[c.url_code for c in classrooms], start=cycle.start_date.strftime(config.iso_datetime_format), end=cycle.end_date.strftime(config.iso_datetime_format), )
def report_link(self, report): parent_kind = SqlModel.get_url_kind(report.parent_id) short_id = SqlModel.convert_uid(report.parent_id) if report.gcs_path: platform = 'triton' prefix = '' view_path = '/api/{parent_kind}/{id}/reports/{filename}'.format( parent_kind=parent_kind, id=short_id, filename=report.filename, ) elif report.dataset_id: platform = 'neptune' prefix = '{protocol}://{domain}'.format( protocol='http' if util.is_localhost() else 'https', domain=('localhost:8888' if util.is_localhost() else os.environ['NEPTUNE_DOMAIN']), ) view_path = '/datasets/{ds_id}/{template}/{filename}'.format( ds_id=SqlModel.convert_uid(report.dataset_id), # short form template=report.template, filename=report.filename, ) # Permit report clients to query some data about participation. parent_path = '/api/{parent_kind}/{id}'.format(parent_kind=parent_kind, id=short_id) data_path = '/api/{parent_kind}/{id}/report_data'.format( parent_kind=parent_kind, id=short_id) link_jwt = jwt_helper.encode( { 'allowed_endpoints': [ self.get_endpoint_str(platform=platform, path=view_path), self.get_endpoint_str(platform='triton', path=parent_path), self.get_endpoint_str(platform='triton', path=data_path), ] }, expiration_minutes=(30 * 24 * 60), # thirty days ) return util.set_query_parameters(prefix + view_path, token=link_jwt)
def dispatch(self): if self.datastore_connected(): # Call the overridden dispatch(), which has the effect of running # the get() or post() etc. of the inheriting class. BaseHandler.dispatch(self) else: # Sometimes the datastore doesn't respond. I really don't know why. # Wait a bit and try again. attempts = int(self.request.get('connection_attempts', 0)) + 1 if attempts <= 10: time.sleep(1) self.redirect( util.set_query_parameters(self.request.url, connection_attempts=attempts)) else: logging.error('Could not connect to datastore after 10 tries.') # This is an api call, so the most appropriate response is # devvy JSON text. It prompts devs as to what has happened and # possibly how to fix it. The success: false bit will tell any # javascript using the api to retry or show an appropriate # message to the user. self.response.headers['Content-Type'] = 'application/json; ' \ 'charset=utf-8' self.response.write( json.dumps({ 'success': False, 'message': "Intermittent error. Please try again.", 'dev_message': ("This error occurs when pegasus cannot find a " "particular entity, the unique DatastoreConnection " "entity. It occurs intermittently under normal " "operations for unknown reasons. However, it may occur if " "no one has yet created the entity in question in the " "first place. If you think this may be the case, visit " "/initialize_connection as an app admin."), }))
def write(self, template_filename, template_path='templates', **kwargs): util.profiler.add_event("Begin ViewHandler:write") jinja_environment = self.get_jinja_environment(template_path) # Jinja environment filters: @jinja2.evalcontextfilter def jinja_json_filter(eval_context, value): """Seralize value as JSON and mark as safe for jinja.""" return jinja2.Markup(json.dumps(value)) jinja_environment.filters['to_json'] = jinja_json_filter def nl2br(value): """Replace new lines with <br> for html view""" return value.replace('\n', '<br>\n') jinja_environment.filters['nl2br'] = nl2br def format_datetime(value): # Formats datetime as Ex: "January 9, 2015" return '{dt:%B} {dt.day}, {dt.year}'.format(dt=value) jinja_environment.filters['datetime'] = format_datetime def format_ampescape(value): return value.replace('&', '%26') jinja_environment.filters['ampescape'] = format_ampescape def format_filetype(value): if value.split('/')[0] in ['application']: if value.split('/')[1] in ['pdf']: formatted_type = 'pdf file' elif value.split('/')[1].find('wordprocessing') > -1: formatted_type = 'word document' elif value.split('/')[1].find('presentation') > -1: formatted_type = 'presentation' else: formatted_type = 'document' elif value.split('/')[0] in ['image']: formatted_type = 'image file' else: formatted_type = value.split('/')[0] return formatted_type jinja_environment.filters['filetype'] = format_filetype util.profiler.add_event("Begin ViewHandler:add_jinja_filters") user = self.get_current_user() util.profiler.add_event("Begin ViewHandler:get_current_user()") # Only get sign in links if no user is present if user is None: # Sets up the google sign in link, used in modal on all pages, # which must include a special flag to alert this handler that # google credentials are present in the cookie. It should also # incorporate any redirect already set in the URL. redirect = str(self.request.get('redirect')) or self.request.url google_redirect = util.set_query_parameters(redirect, google_login='******') google_login_url = app_engine_users.create_login_url( google_redirect) else: google_login_url = '' util.profiler.add_event("Begin ViewHandler:get_login_redirects") # default parameters that all views get kwargs['user'] = user kwargs['google_login_url'] = google_login_url kwargs['hosting_domain'] = os.environ['HOSTING_DOMAIN'] kwargs['share_url'] = self.request.url kwargs['google_client_id'] = config.google_client_id util.profiler.add_event("Begin ViewHandler:set_user_params") # Determine which Facebook app depending on environment kwargs['localhost'] = False if util.is_localhost(): kwargs['localhost'] = True util.profiler.add_event("Begin ViewHandler:start_fetching_themes") # Fetch all themes and topics for navigation courses = self.api.get('Theme') if courses: # Fetch all topics for courses course_topic_ids = [ id for course in courses for id in course.topics ] course_topics = self.api.get_by_id(course_topic_ids) # Associate topics with appropriate courses for course in courses: course.associate_topics(course_topics) # Special case for "Teachers" kit if course.name == 'Growth Mindset for Teachers': # IDK WHAT THIS IS kwargs['teacher_topics'] = course.topics_list kwargs['courses'] = courses util.profiler.add_event("Begin ViewHandler:finish_fetching_themes") logging.info(util.profiler) # Try to load the requested template. If it doesn't exist, replace # it with a 404. try: template = jinja_environment.get_template(template_filename) except jinja2.exceptions.TemplateNotFound: logging.error("TemplateNotFound: {}".format(template_filename)) return self.http_not_found() # logging.info('kwargs={}', kwargs['book']) # Render the template with data and write it to the HTTP response. self.response.write(template.render(kwargs))
def get(self, cohort_code, token): logging.info("Redirecting student '{}' from cohort '{}'." .format(token, cohort_code)) api = Api() redirect = unique_link = anonymous_link = security_token = None # Attempt to look up the token, which may fail and return None. redirect = unique_link = api.get_redirect(cohort_code, token) if unique_link: logging.info("Token maps to link: {}".format(unique_link)) else: # Token not found; use the cohort's anonymous link instead. redirect = anonymous_link = api.get_anonymous_link(cohort_code) if not anonymous_link: # This happens when the request cohort doesn't exist. return self.http_not_found() logging.info("No mapping found. Using anonymous link {}" .format(anonymous_link)) # We have an anonymous link to use. Now include the (username) # token so we know who this is, and security token so we can later # detect if people mess with the URL parameters. # Make sure that the token is at least a string, not None. token = str(token) if isinstance(token, basestring) else '' security_token = self.hash(token + self.security_token_salt) redirect = util.set_query_parameters( redirect, token=token, security_token=security_token) # Both kinds of redirect, using unique or anonymous links, need the # cohort code. Also, pass through any request params that may be # present, as long as they don't conflict with what we need. reserved_params = ['debug', 'cohort_code', 'token', 'security_token'] GET_params = {k: v for k, v in self.request.GET.items() if k not in reserved_params} logging.info("Passing through params: {}".format(GET_params)) redirect = util.set_query_parameters( redirect, cohort_code=cohort_code, **GET_params) logging.info("Final redirct URL is: {}".format(redirect)) if self.request.get('debug'): logging.info("Debug mode on. NOT redirecting.") self.write( 'redirector.html', cohort_code=cohort_code, token=token, security_token=security_token, unique_link=unique_link, anonymous_link=anonymous_link, redirect=redirect, ) else: logging.info("Redirecting to {}".format(redirect)) # Links come back from the db as unicode, and this needs str. self.redirect(str(redirect))
class User(DatastoreModel): """Neptune users.""" # tl;dr: values forced to lower case before storage here AND in # uniqueness_key()! # # Emails are stored in two places and must match across them to make # sure we don't get duplicate users: in User.email and the key name of # the corresponding Unique entity (see uniqueness_key()). And because # email addresses are effectively case insensitive while our databases # are case sensistive, force them all to lower case before storage. # Because people _do_ vary their capitalization between sessions. See # #387. email = ndb.StringProperty(required=True, validator=lambda prop, value: value.lower()) name = ndb.StringProperty() role = ndb.StringProperty() phone_number = ndb.StringProperty() hashed_password = ndb.StringProperty() google_id = ndb.StringProperty() # user type can be: super_admin, program_admin, user, public user_type = ndb.StringProperty(default='user') # notification option has two possible keys: # { # "email": ("yes"|"no"), # "sms": ("yes"|"no") # } notification_option_json = ndb.TextProperty(default=r'{}') owned_organizations = ndb.StringProperty(repeated=True) assc_organizations = ndb.StringProperty(repeated=True) owned_programs = ndb.StringProperty(repeated=True) owned_projects = ndb.StringProperty(repeated=True) assc_projects = ndb.StringProperty(repeated=True) owned_data_tables = ndb.StringProperty(repeated=True) owned_data_requests = ndb.StringProperty(repeated=True) last_login = ndb.DateTimeProperty() # App Engine can only run pure-python external libraries, and so we can't get # a native (C-based) implementation of bcrypt. Pure python implementations are # so slow that [the feasible number of rounds is insecure][1]. This uses the # [algorithm recommended by passlib][2]. # [1]: http://stackoverflow.com/questions/7027196/how-can-i-use-bcrypt-scrypt-on-appengine-for-python # [2]: https://pythonhosted.org/passlib/new_app_quickstart.html#sha512-crypt password_hashing_context = CryptContext( schemes=['sha512_crypt', 'sha256_crypt'], default='sha512_crypt', all__vary_rounds=0.1, # Can change hashing rounds here. 656,000 is the default. # sha512_crypt__default_rounds=656000, # sha256_crypt__default_rounds=656000, ) json_props = ['notification_option_json'] @property def super_admin(self): return self.user_type == 'super_admin' @property def non_admin(self): # Matches either value while we transition. See #985. return self.user_type in ('org_admin', 'user') @property def notification_option(self): return (json.loads(self.notification_option_json) if self.notification_option_json else None) @notification_option.setter def notification_option(self, obj): self.notification_option_json = json.dumps(obj) return obj @classmethod def create(klass, **kwargs): # Create Unique entity based on email, allowing strongly consistent # prevention of duplicates. is_unique_email = Unique.create(User.uniqueness_key(kwargs['email'])) if not is_unique_email: raise DuplicateUser( "There is already a user with email {}.".format( kwargs['email'])) return super(klass, klass).create(**kwargs) @classmethod def create_public(klass): return super(klass, klass).create( id='public', name='public', email='public', user_type='public', ) @classmethod def uniqueness_key(klass, email): # See #387. return u'User.email:{}'.format(email.lower()) @classmethod def email_exists(klass, email): """Test if this email has been registered, idempotent.""" return Unique.get_by_id(User.uniqueness_key(email)) is not None @classmethod def get_by_auth(klass, auth_type, auth_id): # All stored emails are in lower case. If we hope to find them, need # to lower case the search param. See #387. if auth_type == 'email': auth_id = auth_id.lower() matches = User.get(order='created', **{auth_type: auth_id}) if len(matches) == 0: return None elif len(matches) == 1: return matches[0] elif len(matches) > 1: logging.error( u"More than one user matches auth info: {}, {}.".format( auth_type, auth_id)) # We'll let the function pass on and take the first of multiple # duplicate users, which will be the earliest-created one. return matches[0] @classmethod def property_types(klass): """Overrides DatastoreModel. Prevents hashed_password from being set.""" props = super(klass, klass).property_types() props.pop('hashed_password', None) return props @classmethod def example_params(klass): name = ''.join(random.choice(string.ascii_uppercase) for c in range(3)) return { 'name': name, 'email': name + '@example.com', 'phone_number': '+1 (555) 555-5555', 'hashed_password': '******', 'user_type': random.choice(['user', 'program_admin', 'super_admin']), } @classmethod def hash_password(klass, password): if re.match(config.password_pattern, password) is None: raise BadPassword(u'Bad password: {}'.format(password)) return klass.password_hashing_context.encrypt(password) @classmethod def verify_password(klass, password, hashed_password): return (klass.password_hashing_context.verify( password, hashed_password) if hashed_password else False) def __nonzero__(self): return False if getattr(self, 'user_type', None) == 'public' else True def before_put(self, *args, **kwargs): if self.user_type == 'public': raise Exception("Public user cannot be saved.") def to_client_dict(self, **kwargs): """Overrides DatastoreModel, modifies behavior of hashed_password. Change hashed_password to a boolean so client can detect if a user hasn't set their password yet. Also prevent hash from be unsafely exposed. """ output = super(User, self).to_client_dict() output['hashed_password'] = bool(self.hashed_password) return output def create_reset_link(self, domain, token, continue_url='', case=''): """Create the kind of jwt-based set password link used by Triton. Args: domain: str, beginning with protocol, designed this way to make it easier to switch btwn localhost on http and deployed on https. continue_url: str, page should support redirecting user to this url after successful submission case: str, either 'reset' or 'invitation', aids the UI in displaying helpful text based on why the user has arrived. """ return util.set_query_parameters( '{}/set_password/{}'.format(domain, token), continue_url=continue_url, case=case, )