def start_server(args, settings, plugins): # type: (Namespace, ApiSettings, PluginProxy) -> None log_level = logging.getLevelName(logging.getLogger().level) logging.info("begin. log_level={}".format(log_level)) assert not (settings.debug and settings.num_processes > 1 ), "debug mode does not support multiple processes" # setup database logging.debug("configure database session") if args.database_url: settings.database = args.database_url Session.configure(bind=get_db_engine(settings.database)) with closing(Session()) as session: graph = Graph() graph.update_from_db(session) refresher = DbRefreshThread(settings, plugins, graph, settings.refresh_interval) refresher.daemon = True refresher.start() usecase_factory = create_graph_usecase_factory(settings, plugins, graph=graph) application = create_api_application(graph, settings, plugins, usecase_factory) if args.listen_stdin: logging.info("Starting application server on stdin") server = HTTPServer(application) s = socket.socket(fileno=sys.stdin.fileno()) s.setblocking(False) s.listen() server.add_sockets([s]) else: address = args.address or settings.address port = args.port or settings.port logging.info("Starting application server on %s:%d", address, port) server = HTTPServer(application) server.bind(port, address=address) server.start(settings.num_processes) try: IOLoop.current().start() except KeyboardInterrupt: IOLoop.current().stop() finally: print("Bye")
def start_server(args, sentry_client): # type: (Namespace, SentryProxy) -> None log_level = logging.getLevelName(logging.getLogger().level) logging.info("begin. log_level={}".format(log_level)) assert not (settings.debug and settings.num_processes > 1 ), "debug mode does not support multiple processes" try: initialize_plugins(settings.plugin_dirs, settings.plugin_module_paths, "grouper_api") except PluginsDirectoryDoesNotExist as e: logging.fatal("Plugin directory does not exist: {}".format(e)) sys.exit(1) # setup database logging.debug("configure database session") database_url = args.database_url or get_database_url(settings) Session.configure(bind=get_db_engine(database_url)) settings.start_config_thread(args.config, "api") with closing(Session()) as session: graph = Graph() graph.update_from_db(session) refresher = DbRefreshThread(settings, graph, settings.refresh_interval, sentry_client) refresher.daemon = True refresher.start() usecase_factory = create_graph_usecase_factory(settings, graph=graph) application = create_api_application(graph, settings, usecase_factory) address = args.address or settings.address port = args.port or settings.port logging.info("Starting application server on port %d", port) server = tornado.httpserver.HTTPServer(application) server.bind(port, address=address) server.start(settings.num_processes) stats.set_defaults() try: tornado.ioloop.IOLoop.instance().start() except KeyboardInterrupt: tornado.ioloop.IOLoop.instance().stop() finally: print("Bye")
def start_server(args, sentry_client): # type: (argparse.Namespace, SentryProxy) -> None log_level = logging.getLevelName(logging.getLogger().level) logging.info("begin. log_level={}".format(log_level)) assert not (settings.debug and settings.num_processes > 1), \ "debug mode does not support multiple processes" try: initialize_plugins(settings.plugin_dirs, settings.plugin_module_paths, "grouper_api") except PluginsDirectoryDoesNotExist as e: logging.fatal("Plugin directory does not exist: {}".format(e)) sys.exit(1) # setup database logging.debug("configure database session") database_url = args.database_url or get_database_url(settings) Session.configure(bind=get_db_engine(database_url)) settings.start_config_thread(args.config, "api") with closing(Session()) as session: graph = Graph() graph.update_from_db(session) refresher = DbRefreshThread(settings, graph, settings.refresh_interval, sentry_client) refresher.daemon = True refresher.start() application = get_application(graph, settings, sentry_client) address = args.address or settings.address port = args.port or settings.port logging.info("Starting application server on port %d", port) server = tornado.httpserver.HTTPServer(application) server.bind(port, address=address) server.start(settings.num_processes) stats.set_defaults() try: tornado.ioloop.IOLoop.instance().start() except KeyboardInterrupt: tornado.ioloop.IOLoop.instance().stop() finally: print "Bye"
def promote_nonauditors(self, session): # type: (Session) -> None """Checks all enabled audited groups and ensures that all approvers for that group have the PERMISSION_AUDITOR permission. All non-auditor approvers of audited groups will be promoted to be auditors, i.e., added to the auditors group. Args: session (Session): database session """ graph = Graph() # Hack to ensure the graph is loaded before we access it graph.update_from_db(session) # map from user object to names of audited groups in which # user is a nonauditor approver nonauditor_approver_to_groups = defaultdict(set) # type: Dict[User, Set[str]] user_is_auditor = {} # type: Dict[str, bool] for group_tuple in graph.get_groups(audited=True, directly_audited=False): group_md = graph.get_group_details(group_tuple.name, expose_aliases=False) for username, user_md in iteritems(group_md["users"]): if username not in user_is_auditor: user_perms = graph.get_user_details(username)["permissions"] user_is_auditor[username] = any( [p["permission"] == PERMISSION_AUDITOR for p in user_perms] ) if user_is_auditor[username]: # user is already auditor so can skip continue if user_md["role"] in APPROVER_ROLE_INDICES: # non-auditor approver. BAD! nonauditor_approver_to_groups[username].add(group_tuple.name) if nonauditor_approver_to_groups: auditors_group = get_auditors_group(self.settings, session) for username, group_names in iteritems(nonauditor_approver_to_groups): reason = "auto-added due to having approver role(s) in group(s): {}".format( ", ".join(group_names) ) user = User.get(session, name=username) assert user auditors_group.add_member(user, user, reason, status="actioned") notify_nonauditor_promoted( self.settings, session, user, auditors_group, group_names ) session.commit()
def expire_nonauditors(self, session): """Checks all enabled audited groups and ensures that all approvers for that group have the PERMISSION_AUDITOR permission. All approvers of audited groups that aren't auditors have their membership in the audited group set to expire settings.nonauditor_expiration_days days in the future. Args: session (Session): database session """ now = datetime.utcnow() graph = Graph() exp_days = timedelta(days=settings.nonauditor_expiration_days) # Hack to ensure the graph is loaded before we access it graph.update_from_db(session) # TODO(tyleromeara): replace with graph call for group in get_audited_groups(session): members = group.my_members() # Go through every member of the group and set them to expire if they are an approver # but not an auditor for (type_, member), edge in members.iteritems(): # Auditing is already inherited, so we don't need to handle that here if type_ == "Group": continue member = User.get(session, name=member) member_is_approver = user_role_index( member, members) in APPROVER_ROLE_INDICIES member_is_auditor = user_has_permission( session, member, PERMISSION_AUDITOR) if not member_is_approver or member_is_auditor: continue edge = GroupEdge.get(session, id=edge.edge_id) if edge.expiration and edge.expiration < now + exp_days: continue exp = now + exp_days exp = exp.date() edge.apply_changes_dict({ "expiration": "{}/{}/{}".format(exp.month, exp.day, exp.year) }) edge.add(session) notify_nonauditor_flagged(settings, session, edge) session.commit()
def expire_nonauditors(self, session): # type: (Session) -> None """Checks all enabled audited groups and ensures that all approvers for that group have the PERMISSION_AUDITOR permission. All approvers of audited groups that aren't auditors have their membership in the audited group set to expire settings.nonauditor_expiration_days days in the future. Args: session (Session): database session """ now = datetime.utcnow() graph = Graph() exp_days = timedelta(days=self.settings.nonauditor_expiration_days) # Hack to ensure the graph is loaded before we access it graph.update_from_db(session) # TODO(tyleromeara): replace with graph call for group in get_audited_groups(session): members = group.my_members() # Go through every member of the group and set them to expire if they are an approver # but not an auditor for (type_, member), edge in members.iteritems(): # Auditing is already inherited, so we don't need to handle that here if type_ == "Group": continue member = User.get(session, name=member) member_is_approver = user_role_index(member, members) in APPROVER_ROLE_INDICES member_is_auditor = user_has_permission(session, member, PERMISSION_AUDITOR) if not member_is_approver or member_is_auditor: continue edge = GroupEdge.get(session, id=edge.edge_id) if edge.expiration and edge.expiration < now + exp_days: continue exp = (now + exp_days).date() edge.apply_changes( {"expiration": "{}/{}/{}".format(exp.month, exp.day, exp.year)} ) edge.add(session) notify_nonauditor_flagged(self.settings, session, edge) session.commit()
def start_server(args, settings, sentry_client): # type: (Namespace, FrontendSettings, SentryProxy) -> None log_level = logging.getLevelName(logging.getLogger().level) logging.info("begin. log_level={}".format(log_level)) assert not ( settings.debug and settings.num_processes > 1 ), "debug mode does not support multiple processes" try: plugins = PluginProxy.load_plugins(settings, "grouper-fe") set_global_plugin_proxy(plugins) except PluginsDirectoryDoesNotExist as e: logging.fatal("Plugin directory does not exist: {}".format(e)) sys.exit(1) # setup database logging.debug("configure database session") if args.database_url: settings.database = args.database_url Session.configure(bind=get_db_engine(settings.database)) application = create_fe_application(settings, args.deployment_name) ssl_context = plugins.get_ssl_context() if args.listen_stdin: logging.info( "Starting application server with %d processes on stdin", settings.num_processes ) server = HTTPServer(application, ssl_options=ssl_context) if PY2: s = socket.fromfd(sys.stdin.fileno(), socket.AF_INET, socket.SOCK_STREAM) s.setblocking(False) s.listen(5) else: s = socket.socket(fileno=sys.stdin.fileno()) s.setblocking(False) s.listen() server.add_sockets([s]) else: address = args.address or settings.address port = args.port or settings.port logging.info( "Starting application server with %d processes on %s:%d", settings.num_processes, address, port, ) server = HTTPServer(application, ssl_options=ssl_context) server.bind(port, address=address) # When using multiple processes, the forking happens here server.start(settings.num_processes) stats.set_defaults() # Create the Graph and start the graph update thread post fork to ensure each process gets # updated. with closing(Session()) as session: graph = Graph() graph.update_from_db(session) refresher = DbRefreshThread(settings, graph, settings.refresh_interval, sentry_client) refresher.daemon = True refresher.start() try: IOLoop.current().start() except KeyboardInterrupt: IOLoop.current().stop() finally: print("Bye")
class GrouperHandler(RequestHandler): def initialize(self): self.session = self.application.my_settings.get("db_session")() self.graph = Graph() if self.get_argument("_profile", False): self.perf_collector = Collector() self.perf_trace_uuid = str(uuid4()) self.perf_collector.start() else: self.perf_collector = None self.perf_trace_uuid = None self._request_start_time = datetime.utcnow() stats.incr("requests") stats.incr("requests_{}".format(self.__class__.__name__)) def write_error(self, status_code, **kwargs): """Override for custom error page.""" if status_code >= 500 and status_code < 600: template = self.application.my_settings["template_env"].get_template("errors/5xx.html") self.write(template.render({"is_active": self.is_active})) else: template = self.application.my_settings["template_env"].get_template("errors/generic.html") self.write( template.render( { "status_code": status_code, "message": self._reason, "is_active": self.is_active, "trace_uuid": self.perf_trace_uuid, } ) ) self.finish() def is_refresh(self): # type: () -> bool """Indicates whether the refresh argument for this handler has been set to yes. This is used to force a refresh of the cached graph so that we don't show inconsistent state to the user. Returns: a boolean indicating whether this handler should refresh the graph """ return self.get_argument("refresh", "no").lower() == "yes" # The refresh argument can be added to any page. If the handler for that # route calls this function, it will sync its graph from the database if # requested. def handle_refresh(self): if self.is_refresh(): self.graph.update_from_db(self.session) def redirect(self, url, *args, **kwargs): if self.is_refresh(): url = urlparse.urljoin(url, "?refresh=yes") return super(GrouperHandler, self).redirect(url, *args, **kwargs) def get_current_user(self): username = self.request.headers.get(settings.user_auth_header) if not username: return # Users must be fully qualified if not re.match("^{}$".format(USERNAME_VALIDATION), username): raise InvalidUser() try: user, created = User.get_or_create(self.session, username=username) if created: logging.info("Created new user %s", username) self.session.commit() # Because the graph doesn't initialize until the updates table # is populated, we need to refresh the graph here in case this # is the first update. self.graph.update_from_db(self.session) except sqlalchemy.exc.OperationalError: # Failed to connect to database or create user, try to reconfigure the db. This invokes # the fetcher to try to see if our URL string has changed. Session.configure(bind=get_db_engine(get_database_url(settings))) raise DatabaseFailure() return user def prepare(self): if not self.current_user or not self.current_user.enabled: self.forbidden() self.finish() return def on_finish(self): if self.perf_collector: self.perf_collector.stop() perf_profile.record_trace(self.session, self.perf_collector, self.perf_trace_uuid) self.session.close() # log request duration duration = datetime.utcnow() - self._request_start_time duration_ms = int(duration.total_seconds() * 1000) stats.incr("duration_ms", duration_ms) stats.incr("duration_ms_{}".format(self.__class__.__name__), duration_ms) # log response status code response_status = self.get_status() stats.incr("response_status_{}".format(response_status)) stats.incr("response_status_{}_{}".format(self.__class__.__name__, response_status)) def update_qs(self, **kwargs): qs = self.request.arguments.copy() qs.update(kwargs) return "?" + urllib.urlencode(qs, True) def is_active(self, test_path): path = self.request.path if path == test_path: return "active" return "" def get_template_namespace(self): namespace = super(GrouperHandler, self).get_template_namespace() namespace.update( { "update_qs": self.update_qs, "is_active": self.is_active, "perf_trace_uuid": self.perf_trace_uuid, "xsrf_form": self.xsrf_form_html, "alerts": [], } ) return namespace def render_template(self, template_name, **kwargs): template = self.application.my_settings["template_env"].get_template(template_name) content = template.render(kwargs) return content def render(self, template_name, **kwargs): context = {} context.update(self.get_template_namespace()) context.update(kwargs) self.write(self.render_template(template_name, **context)) def get_form_alerts(self, errors): alerts = [] for field, field_errors in errors.items(): for error in field_errors: alerts.append(Alert("danger", error, field)) return alerts def raise_and_log_exception(self, exc): try: raise exc except Exception: self.log_exception(*sys.exc_info()) def log_message(self, message, **kwargs): if self.captureMessage: self.captureMessage(message, **kwargs) else: logging.info("{}, kwargs={}".format(message, kwargs)) # TODO(gary): Add json error responses. def badrequest(self, format_type=None): self.set_status(400) self.raise_and_log_exception(tornado.web.HTTPError(400)) self.render("errors/badrequest.html") def forbidden(self, format_type=None): self.set_status(403) self.raise_and_log_exception(tornado.web.HTTPError(403)) self.render("errors/forbidden.html") def notfound(self, format_type=None): self.set_status(404) self.raise_and_log_exception(tornado.web.HTTPError(404)) self.render("errors/notfound.html") def get_sentry_user_info(self): user = self.get_current_user() return {"username": user.name}
def start_server(args, settings, sentry_client): # type: (Namespace, FrontendSettings, SentryProxy) -> None log_level = logging.getLevelName(logging.getLogger().level) logging.info("begin. log_level={}".format(log_level)) assert not (settings.debug and settings.num_processes > 1 ), "debug mode does not support multiple processes" try: plugins = PluginProxy.load_plugins(settings, "grouper-fe") set_global_plugin_proxy(plugins) except PluginsDirectoryDoesNotExist as e: logging.fatal("Plugin directory does not exist: {}".format(e)) sys.exit(1) # setup database logging.debug("configure database session") if args.database_url: settings.database = args.database_url Session.configure(bind=get_db_engine(settings.database)) application = create_fe_application(settings, args.deployment_name) ssl_context = plugins.get_ssl_context() if args.listen_stdin: logging.info("Starting application server with %d processes on stdin", settings.num_processes) server = HTTPServer(application, ssl_options=ssl_context) if PY2: s = socket.fromfd(sys.stdin.fileno(), socket.AF_INET, socket.SOCK_STREAM) s.setblocking(False) s.listen(5) else: s = socket.socket(fileno=sys.stdin.fileno()) s.setblocking(False) s.listen() server.add_sockets([s]) else: address = args.address or settings.address port = args.port or settings.port logging.info( "Starting application server with %d processes on %s:%d", settings.num_processes, address, port, ) server = HTTPServer(application, ssl_options=ssl_context) server.bind(port, address=address) # When using multiple processes, the forking happens here server.start(settings.num_processes) stats.set_defaults() # Create the Graph and start the graph update thread post fork to ensure each process gets # updated. with closing(Session()) as session: graph = Graph() graph.update_from_db(session) refresher = DbRefreshThread(settings, graph, settings.refresh_interval, sentry_client) refresher.daemon = True refresher.start() try: IOLoop.current().start() except KeyboardInterrupt: IOLoop.current().stop() finally: print("Bye")
def graph(session): graph = Graph() graph.update_from_db(session) return graph
class GrouperHandler(RequestHandler): def initialize(self): self.session = self.application.my_settings.get("db_session")() self.graph = Graph() if self.get_argument("_profile", False): self.perf_collector = Collector() self.perf_trace_uuid = str(uuid4()) self.perf_collector.start() else: self.perf_collector = None self.perf_trace_uuid = None self._request_start_time = datetime.utcnow() stats.incr("requests") stats.incr("requests_{}".format(self.__class__.__name__)) def write_error(self, status_code, **kwargs): """Override for custom error page.""" if status_code >= 500 and status_code < 600: template = self.application.my_settings[ "template_env"].get_template("errors/5xx.html") self.write(template.render({"is_active": self.is_active})) else: template = self.application.my_settings[ "template_env"].get_template("errors/generic.html") self.write( template.render({ "status_code": status_code, "message": self._reason, "is_active": self.is_active, "trace_uuid": self.perf_trace_uuid, })) self.finish() def is_refresh(self): # type: () -> bool """Indicates whether the refresh argument for this handler has been set to yes. This is used to force a refresh of the cached graph so that we don't show inconsistent state to the user. Returns: a boolean indicating whether this handler should refresh the graph """ return self.get_argument("refresh", "no").lower() == "yes" # The refresh argument can be added to any page. If the handler for that # route calls this function, it will sync its graph from the database if # requested. def handle_refresh(self): if self.is_refresh(): self.graph.update_from_db(self.session) def redirect(self, url, *args, **kwargs): if self.is_refresh(): url = urlparse.urljoin(url, "?refresh=yes") self.set_alerts(kwargs.pop("alerts", [])) return super(GrouperHandler, self).redirect(url, *args, **kwargs) def get_current_user(self): username = self.request.headers.get(settings.user_auth_header) if not username: return # Users must be fully qualified if not re.match("^{}$".format(USERNAME_VALIDATION), username): raise InvalidUser() try: user, created = User.get_or_create(self.session, username=username) if created: logging.info("Created new user %s", username) self.session.commit() # Because the graph doesn't initialize until the updates table # is populated, we need to refresh the graph here in case this # is the first update. self.graph.update_from_db(self.session) except sqlalchemy.exc.OperationalError: # Failed to connect to database or create user, try to reconfigure the db. This invokes # the fetcher to try to see if our URL string has changed. Session.configure(bind=get_db_engine(get_database_url(settings))) raise DatabaseFailure() return user def prepare(self): if not self.current_user or not self.current_user.enabled: self.forbidden() self.finish() return def on_finish(self): if self.perf_collector: self.perf_collector.stop() perf_profile.record_trace(self.session, self.perf_collector, self.perf_trace_uuid) self.session.close() # log request duration duration = datetime.utcnow() - self._request_start_time duration_ms = int(duration.total_seconds() * 1000) stats.incr("duration_ms", duration_ms) stats.incr("duration_ms_{}".format(self.__class__.__name__), duration_ms) # log response status code response_status = self.get_status() stats.incr("response_status_{}".format(response_status)) stats.incr("response_status_{}_{}".format(self.__class__.__name__, response_status)) def update_qs(self, **kwargs): qs = self.request.arguments.copy() qs.update(kwargs) return "?" + urllib.urlencode(qs, True) def is_active(self, test_path): path = self.request.path if path == test_path: return "active" return "" def get_template_namespace(self): namespace = super(GrouperHandler, self).get_template_namespace() namespace.update({ "update_qs": self.update_qs, "is_active": self.is_active, "perf_trace_uuid": self.perf_trace_uuid, "xsrf_form": self.xsrf_form_html, "alerts": self.get_alerts(), }) return namespace def render_template(self, template_name, **kwargs): template = self.application.my_settings["template_env"].get_template( template_name) content = template.render(kwargs) return content def render(self, template_name, **kwargs): defaults = self.get_template_namespace() context = {} context.update(defaults) context.update(kwargs) # Merge alerts context["alerts"] = defaults.get("alerts", []) + kwargs.get( "alerts", []) self.write(self.render_template(template_name, **context)) def set_alerts(self, alerts): # type: (List[Alert]) -> None if len(alerts) > 0: self.set_cookie("_alerts", _serialize_alerts(alerts)) else: self.clear_cookie("_alerts") def get_alerts(self): # type: () -> List[Alert] serialized_alerts = self.get_cookie("_alerts", default="[]") alerts = _deserialize_alerts(serialized_alerts) self.clear_cookie("_alerts") return alerts def get_form_alerts(self, errors): alerts = [] for field, field_errors in errors.items(): for error in field_errors: alerts.append(Alert("danger", error, field)) return alerts def raise_and_log_exception(self, exc): try: raise exc except Exception: self.log_exception(*sys.exc_info()) def log_message(self, message, **kwargs): if self.captureMessage: self.captureMessage(message, **kwargs) else: logging.info("{}, kwargs={}".format(message, kwargs)) # TODO(gary): Add json error responses. def badrequest(self, format_type=None): self.set_status(400) self.raise_and_log_exception(tornado.web.HTTPError(400)) self.render("errors/badrequest.html") def forbidden(self, format_type=None): self.set_status(403) self.raise_and_log_exception(tornado.web.HTTPError(403)) self.render("errors/forbidden.html") def notfound(self, format_type=None): self.set_status(404) self.raise_and_log_exception(tornado.web.HTTPError(404)) self.render("errors/notfound.html") def get_sentry_user_info(self): user = self.get_current_user() return { 'username': user.username, }
class GrouperHandler(SentryHandler): def initialize(self, *args, **kwargs): # type: (*Any, **Any) -> None self.graph = Graph() self.session = self.settings["session"]() # type: Session self.template_engine = self.settings["template_engine"] # type: FrontendTemplateEngine self.plugins = get_plugin_proxy() session_factory = SingletonSessionFactory(self.session) self.usecase_factory = create_graph_usecase_factory( settings(), self.plugins, session_factory ) if self.get_argument("_profile", False): self.perf_collector = Collector() self.perf_trace_uuid = str(uuid4()) # type: Optional[str] self.perf_collector.start() else: self.perf_collector = None self.perf_trace_uuid = None self._request_start_time = datetime.utcnow() stats.log_rate("requests", 1) stats.log_rate("requests_{}".format(self.__class__.__name__), 1) def set_default_headers(self): # type: () -> None self.set_header("Content-Security-Policy", self.settings["template_engine"].csp_header()) self.set_header("Referrer-Policy", "same-origin") def write_error(self, status_code, **kwargs): # type: (int, **Any) -> None """Override for custom error page.""" message = kwargs.get("message", "Unknown error") if status_code >= 500 and status_code < 600: template = self.template_engine.get_template("errors/5xx.html") self.write( template.render({"is_active": self.is_active, "static_url": self.static_url}) ) else: template = self.template_engine.get_template("errors/generic.html") self.write( template.render( { "status_code": status_code, "message": message, "is_active": self.is_active, "trace_uuid": self.perf_trace_uuid, "static_url": self.static_url, } ) ) self.finish() def is_refresh(self): # type: () -> bool """Indicates whether the refresh argument for this handler has been set to yes. This is used to force a refresh of the cached graph so that we don't show inconsistent state to the user. Returns: a boolean indicating whether this handler should refresh the graph """ return self.get_argument("refresh", "no").lower() == "yes" # The refresh argument can be added to any page. If the handler for that # route calls this function, it will sync its graph from the database if # requested. def handle_refresh(self): # type: () -> None if self.is_refresh(): self.graph.update_from_db(self.session) def redirect(self, url, *args, **kwargs): # type: (str, *Any, **Any) -> None if self.is_refresh(): url = urljoin(url, "?refresh=yes") alerts = kwargs.pop("alerts", []) # type: List[Alert] self.set_alerts(alerts) super(GrouperHandler, self).redirect(url, *args, **kwargs) def get_or_create_user(self, username): # type: (str) -> Optional[User] """Retrieve or create the User object for the authenticated user. This is done in a separate method called by prepare instead of in the magic Tornado get_current_user method because exceptions thrown by the latter are caught by Tornado and not propagated to the caller, and we want to use exceptions to handle invalid users and then return an error page in prepare. """ if not username: return None # Users must be fully qualified if not re.match("^{}$".format(USERNAME_VALIDATION), username): raise InvalidUser("{} does not match {}".format(username, USERNAME_VALIDATION)) # User must exist in the database and be active try: user, created = User.get_or_create(self.session, username=username) if created: logging.info("Created new user %s", username) self.session.commit() # Because the graph doesn't initialize until the updates table # is populated, we need to refresh the graph here in case this # is the first update. self.graph.update_from_db(self.session) except sqlalchemy.exc.OperationalError: # Failed to connect to database or create user, try to reconfigure the db. This invokes # the fetcher to try to see if our URL string has changed. Session.configure(bind=get_db_engine(settings().database)) raise DatabaseFailure() # service accounts are, by definition, not interactive users if user.is_service_account: raise InvalidUser("{} is a service account".format(username)) return user def prepare(self): # type: () -> None username = self.request.headers.get(settings().user_auth_header) try: user = self.get_or_create_user(username) except InvalidUser as e: self.baduser(str(e)) self.finish() return if user and user.enabled: self.current_user = user else: self.baduser("{} is not an active account".format(username)) self.finish() def on_finish(self): # type: () -> None if self.perf_collector: self.perf_collector.stop() record_trace(self.session, self.perf_collector, self.perf_trace_uuid) self.session.close() # log request duration duration = datetime.utcnow() - self._request_start_time duration_ms = int(duration.total_seconds() * 1000) stats.log_rate("duration_ms", duration_ms) stats.log_rate("duration_ms_{}".format(self.__class__.__name__), duration_ms) # log response status code response_status = self.get_status() stats.log_rate("response_status_{}".format(response_status), 1) stats.log_rate("response_status_{}_{}".format(self.__class__.__name__, response_status), 1) def update_qs(self, **kwargs): # type: (**Any) -> str qs = self.request.arguments.copy() qs.update(kwargs) return "?" + urlencode(sorted(qs.items()), True) def is_active(self, test_path): # type: (str) -> str path = self.request.path if path == test_path: return "active" return "" def get_template_namespace(self): # type: () -> Dict[str, Any] namespace = super(GrouperHandler, self).get_template_namespace() namespace.update( { "update_qs": self.update_qs, "is_active": self.is_active, "perf_trace_uuid": self.perf_trace_uuid, "xsrf_form": self.xsrf_form_html, "alerts": self.get_alerts(), "static_url": self.static_url, } ) return namespace def render_template(self, template_name, **kwargs): # type: (str, **Any) -> Text template = self.template_engine.get_template(template_name) content = template.render(kwargs) return content def render(self, template_name, **kwargs): # type: (str, **Any) -> None defaults = self.get_template_namespace() context = {} context.update(defaults) context.update(kwargs) # Merge alerts context["alerts"] = [] context["alerts"].extend(defaults.get("alerts", [])) context["alerts"].extend(kwargs.get("alerts", [])) self.write(self.render_template(template_name, **context)) def set_alerts(self, alerts): # type: (List[Alert]) -> None if len(alerts) > 0: self.set_cookie("_alerts", _serialize_alerts(alerts)) else: self.clear_cookie("_alerts") def get_alerts(self): # type: () -> List[Alert] serialized_alerts = self.get_cookie("_alerts", default="[]") alerts = _deserialize_alerts(serialized_alerts) self.clear_cookie("_alerts") return alerts def get_form_alerts(self, errors): # type: (Dict[str, List[str]]) -> List[Alert] alerts = [] for field, field_errors in iteritems(errors): for error in field_errors: alerts.append(Alert("danger", error, field)) return alerts def raise_and_log_exception(self, exc): # type: (Exception) -> None try: raise exc except Exception: self.log_exception(*sys.exc_info()) def log_message(self, message, **kwargs): # type: (str, **Any) -> None if getattr(self, "captureMessage", None): self.captureMessage(message, **kwargs) else: logging.info("{}, kwargs={}".format(message, kwargs)) def badrequest(self): # type: () -> None self.set_status(400) self.raise_and_log_exception(tornado.web.HTTPError(400)) self.render("errors/badrequest.html") def baduser(self, message): # type: (str) -> None self.set_status(403) self.raise_and_log_exception(tornado.web.HTTPError(403)) how_to_get_help = settings().how_to_get_help self.render("errors/baduser.html", message=message, how_to_get_help=how_to_get_help) def forbidden(self): # type: () -> None self.set_status(403) self.raise_and_log_exception(tornado.web.HTTPError(403)) self.render("errors/forbidden.html", how_to_get_help=settings().how_to_get_help) def notfound(self): # type: () -> None self.set_status(404) self.raise_and_log_exception(tornado.web.HTTPError(404)) self.render("errors/notfound.html") def get_sentry_user_info(self): # type: () -> Dict[str, Optional[str]] if self.current_user: return {"username": self.current_user.username} else: return {"username": None}
class GrouperHandler(RequestHandler): def initialize(self, *args: Any, **kwargs: Any) -> None: self.graph = Graph() self.session = self.settings["session"]() # type: Session self.template_engine = self.settings[ "template_engine"] # type: FrontendTemplateEngine self.plugins = get_plugin_proxy() session_factory = SingletonSessionFactory(self.session) self.usecase_factory = create_graph_usecase_factory( settings(), self.plugins, session_factory) if self.get_argument("_profile", False): self.perf_collector = Collector() self.perf_trace_uuid = str(uuid4()) # type: Optional[str] self.perf_collector.start() else: self.perf_collector = None self.perf_trace_uuid = None self._request_start_time = datetime.utcnow() def set_default_headers(self) -> None: self.set_header("Content-Security-Policy", self.settings["template_engine"].csp_header()) self.set_header("Referrer-Policy", "same-origin") def log_exception( self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], exc_tb: Optional[TracebackType], ) -> None: if isinstance(exc_value, HTTPError): status_code = exc_value.status_code else: status_code = 500 self.plugins.log_exception(self.request, status_code, exc_type, exc_value, exc_tb) super().log_exception(exc_type, exc_value, exc_tb) def write_error(self, status_code: int, **kwargs: Any) -> None: """Override for custom error page.""" message = kwargs.get("message", "Unknown error") if status_code >= 500 and status_code < 600: template = self.template_engine.get_template("errors/5xx.html") self.write( template.render({ "is_active": self.is_active, "static_url": self.static_url })) else: template = self.template_engine.get_template("errors/generic.html") self.write( template.render({ "status_code": status_code, "message": message, "is_active": self.is_active, "trace_uuid": self.perf_trace_uuid, "static_url": self.static_url, })) self.finish() def is_refresh(self) -> bool: """Indicates whether the refresh argument for this handler has been set to yes. This is used to force a refresh of the cached graph so that we don't show inconsistent state to the user. Returns: a boolean indicating whether this handler should refresh the graph """ return self.get_argument("refresh", "no").lower() == "yes" # The refresh argument can be added to any page. If the handler for that # route calls this function, it will sync its graph from the database if # requested. def handle_refresh(self) -> None: if self.is_refresh(): self.graph.update_from_db(self.session) def redirect(self, url: str, *args: Any, **kwargs: Any) -> None: if self.is_refresh(): url = urljoin(url, "?refresh=yes") alerts = kwargs.pop("alerts", []) # type: List[Alert] self.set_alerts(alerts) super().redirect(url, *args, **kwargs) def get_or_create_user(self, username: str) -> Optional[User]: """Retrieve or create the User object for the authenticated user. This is done in a separate method called by prepare instead of in the magic Tornado get_current_user method because exceptions thrown by the latter are caught by Tornado and not propagated to the caller, and we want to use exceptions to handle invalid users and then return an error page in prepare. """ if not username: return None # Users must be fully qualified if not re.match("^{}$".format(USERNAME_VALIDATION), username): raise InvalidUser("{} does not match {}".format( username, USERNAME_VALIDATION)) # User must exist in the database and be active user, created = User.get_or_create(self.session, username=username) if created: logging.info("Created new user %s", username) self.session.commit() # Because the graph doesn't initialize until the updates table is populated, we need to # refresh the graph here in case this is the first update. self.graph.update_from_db(self.session) # service accounts are, by definition, not interactive users if user.is_service_account: raise InvalidUser("{} is a service account".format(username)) return user def get_path_argument(self, name: str) -> str: """Get a URL path argument. Parallel to get_request_argument() and get_body_argument(), this uses path_kwargs to find an argument to the handler, undo any URL quoting, and return it. Use this uniformly instead of kwargs for all handler get() and post() methods to handle escaping properly. """ value: str = self.path_kwargs[name] return unquote(value) def prepare(self) -> None: username = self.request.headers.get(settings().user_auth_header) try: user = self.get_or_create_user(username) except InvalidUser as e: self.baduser(str(e)) self.finish() return if user and user.enabled: self.current_user = user else: self.baduser("{} is not an active account".format(username)) self.finish() def on_finish(self) -> None: if self.perf_collector: self.perf_collector.stop() record_trace(self.session, self.perf_collector, self.perf_trace_uuid) self.session.close() handler = self.__class__.__name__ duration_ms = int( (datetime.utcnow() - self._request_start_time).total_seconds() * 1000) response_status = self.get_status() self.plugins.log_request(handler, response_status, duration_ms) def update_qs(self, **kwargs: Any) -> str: qs = self.request.arguments.copy() qs.update(kwargs) return "?" + urlencode(sorted(qs.items()), True) def is_active(self, test_path: str) -> str: path = self.request.path if path == test_path: return "active" return "" def get_template_namespace(self) -> Dict[str, Any]: namespace = super().get_template_namespace() namespace.update({ "alerts": self.get_alerts(), "is_active": self.is_active, "static_url": self.static_url, "perf_trace_uuid": self.perf_trace_uuid, "update_qs": self.update_qs, "xsrf_form": self.xsrf_form_html, }) return namespace def render_template(self, template_name: str, **kwargs: Any) -> str: template = self.template_engine.get_template(template_name) content = template.render(kwargs) return content def render(self, template_name: str, **kwargs: Any) -> None: defaults = self.get_template_namespace() context = {} context.update(defaults) context.update(kwargs) # Merge alerts context["alerts"] = [] context["alerts"].extend(defaults.get("alerts", [])) context["alerts"].extend(kwargs.get("alerts", [])) self.write(self.render_template(template_name, **context)) def render_template_class( self, template: BaseTemplate, alerts: Optional[List[Alert]] = None) -> Future[None]: return self.finish(template.render(self, alerts)) def set_alerts(self, alerts: Sequence[Alert]) -> None: if len(alerts) > 0: self.set_cookie("_alerts", _serialize_alerts(alerts)) else: self.clear_cookie("_alerts") def get_alerts(self) -> List[Alert]: serialized_alerts = self.get_cookie("_alerts", default="[]") alerts = _deserialize_alerts(serialized_alerts) self.clear_cookie("_alerts") return alerts def get_form_alerts(self, errors: Dict[str, List[str]]) -> List[Alert]: alerts = [] for field, field_errors in errors.items(): for error in field_errors: alerts.append(Alert("danger", error, field)) return alerts def raise_and_log_exception(self, exc: Exception) -> None: try: raise exc except Exception: self.log_exception(*sys.exc_info()) def log_message(self, message: str, **kwargs: Any) -> None: logging.info("{}, kwargs={}".format(message, kwargs)) def badrequest(self) -> None: self.set_status(400) self.raise_and_log_exception(HTTPError(400)) self.render("errors/badrequest.html") def baduser(self, message) -> None: self.set_status(403) self.raise_and_log_exception(HTTPError(403)) how_to_get_help = settings().how_to_get_help self.render("errors/baduser.html", message=message, how_to_get_help=how_to_get_help) def forbidden(self) -> None: self.set_status(403) self.raise_and_log_exception(HTTPError(403)) self.render("errors/forbidden.html", how_to_get_help=settings().how_to_get_help) def notfound(self) -> None: self.set_status(404) self.raise_and_log_exception(HTTPError(404)) self.render("errors/notfound.html")