Ejemplo n.º 1
0
def get_audited_groups(session):
    # type: (Session) -> List[Group]
    """Returns all audited enabled groups.

    At present, this is not cached at all and returns the full list of
    groups from the database each time it's called.

    Args:
        session (Session): Session to load data on.

    Returns:
        a list of all enabled and audited Group objects in the database
    """
    audited_groups = []
    graph = Graph()
    for group in get_all_groups(session):
        try:
            group_md = graph.get_group_details(group.name)
        except NoSuchGroup:
            # Very new group with no metadata yet, or it has been disabled and
            # excluded from in-memory cache.
            continue

        if group_md.get('audited', False):
            audited_groups.append(group)

    return audited_groups
Ejemplo n.º 2
0
def get_all_groups_by_user(session, user):
    # type: (Session, User) -> List[Tuple[Group, int]]
    """Return groups a given user is a member of along with the user's role.

    This includes groups inherited from other groups, unlike get_groups_by_user.
    """
    from grouper.graph import Graph

    grps = Graph().get_user_details(username=user.name)["groups"]
    groups = session.query(Group).filter(Group.name.in_(grps.keys())).all()
    return [(group, grps[group.name]["role"]) for group in groups]
Ejemplo n.º 3
0
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"
Ejemplo n.º 4
0
def user_is_auditor(username):
    """Check if a user is an auditor

    This is defined as the user having the audit permission.

    Args:
        username (str): The account name to check.

    Returns:
        bool: True/False.
    """
    graph = Graph()
    user_md = graph.get_user_details(username)
    for perm in user_md["permissions"]:
        if perm["permission"] == PERMISSION_AUDITOR:
            return True
    return False
Ejemplo n.º 5
0
def assert_can_join(group, user_or_group, role="member"):
    # type: (Group, Union[Group, User], str) -> bool
    """Enforce audit rules on joining a group

    This applies the auditing rules to determine whether or not a given user can join the given
    group with the given role.

    Args:
        group (models.Group): The group to test against.
        user (models.User): The user attempting to join.
        role (str): The role being tested.

    Raises:
        UserNotAuditor: If a user is found that violates the audit training policy, then this
            exception is raised.

    Returns:
        bool: True if the user should be allowed per policy, else it will raise as above.
    """
    # By definition, any user can join as a member to any group.
    if user_or_group.type == "User" and role == "member":
        return True

    # Else, we have to check if the group is audited. If not, anybody can join.
    graph = Graph()
    group_md = graph.get_group_details(group.name)
    if not group_md["audited"]:
        return True

    # Audited group. Easy case, let's see if we're checking a user. If so, the user must be
    # considered an auditor.
    if user_or_group.type == "User":
        if user_is_auditor(user_or_group.name):
            return True
        raise UserNotAuditor(
            "User {} lacks the auditing permission ('{}') so may only have the "
            "'member' role in this audited group.".format(user_or_group.name, PERMISSION_AUDITOR)
        )

    # No, this is a group-joining-group case. In this situation we must walk the entire group
    # subtree and ensure that all owners/np-owners/managers are considered auditors. This data
    # is contained in the group metadetails, which contains all eventual members.
    #
    # We have to fetch each group's details individually though to figure out what someone's role
    # is in that particular group.
    return assert_controllers_are_auditors(user_or_group)
Ejemplo n.º 6
0
def assert_controllers_are_auditors(group):
    # type: (Group) -> bool
    """Return whether not all owners/np-owners/managers in a group (and below) are auditors

    This is used to ensure that all of the people who can control a group
    (owners, np-owners, managers) and all subgroups (all the way down the tree)
    have audit permissions.

    Raises:
        UserNotAuditor: If a user is found that violates the audit training policy, then this
            exception is raised.

    Returns:
        bool: True if the tree is completely controlled by auditors, else it will raise as above.
    """
    graph = Graph()
    checked = set()  # type: Set[str]
    queue = [group.name]
    while queue:
        cur_group = queue.pop()
        if cur_group in checked:
            continue
        details = graph.get_group_details(cur_group)
        for chk_user, info in iteritems(details["users"]):
            if chk_user in checked:
                continue
            # Only examine direct members of this group, because then the role is accurate.
            if info["distance"] == 1:
                if info["rolename"] == "member":
                    continue
                if user_is_auditor(chk_user):
                    checked.add(chk_user)
                else:
                    raise UserNotAuditor(
                        "User {} has role '{}' in the group {} but lacks the auditing "
                        "permission ('{}').".format(
                            chk_user, info["rolename"], cur_group, PERMISSION_AUDITOR
                        )
                    )
        # Now put subgroups into the queue to examine.
        for chk_group, info in iteritems(details["subgroups"]):
            if info["distance"] == 1:
                queue.append(chk_group)

    # If we didn't raise, we're valid.
    return True
Ejemplo n.º 7
0
def assert_can_join(group, user_or_group, role="member"):
    """Enforce audit rules on joining a group

    This applies the auditing rules to determine whether or not a given user can join the given
    group with the given role.

    Args:
        group (models.Group): The group to test against.
        user (models.User): The user attempting to join.
        role (str): The role being tested.

    Raises:
        UserNotAuditor: If a user is found that violates the audit training policy, then this
            exception is raised.

    Returns:
        bool: True if the user should be allowed per policy, else it will raise as above.
    """
    # By definition, any user can join as a member to any group.
    if user_or_group.type == "User" and role == "member":
        return True

    # Else, we have to check if the group is audited. If not, anybody can join.
    graph = Graph()
    group_md = graph.get_group_details(group.name)
    if not group_md["audited"]:
        return True

    # Audited group. Easy case, let's see if we're checking a user. If so, the user must be
    # considered an auditor.
    if user_or_group.type == "User":
        if user_is_auditor(user_or_group.name):
            return True
        raise UserNotAuditor(
            "User {} lacks auditing permission, so may only have the member role.".format(
                user_or_group.name))

    # No, this is a group-joining-group case. In this situation we must walk the entire group
    # subtree and ensure that all owners/np-owners/managers are considered auditors. This data
    # is contained in the group metadetails, which contains all eventual members.
    #
    # We have to fetch each group's details individually though to figure out what someone's role
    # is in that particular group.
    return assert_controllers_are_auditors(user_or_group)
Ejemplo n.º 8
0
def assert_controllers_are_auditors(group):
    """Return whether not all owners/np-owners/managers in a group (and below) are auditors

    This is used to ensure that all of the people who can control a group
    (owners, np-owners, managers) and all subgroups (all the way down the tree)
    have audit permissions.

    Raises:
        UserNotAuditor: If a user is found that violates the audit training policy, then this
            exception is raised.

    Returns:
        bool: True if the tree is completely controlled by auditors, else it will raise as above.
    """
    graph = Graph()
    checked, queue = set(), [group.name]
    while queue:
        cur_group = queue.pop()
        if cur_group in checked:
            continue
        details = graph.get_group_details(cur_group)
        for chk_user, info in iteritems(details["users"]):
            if chk_user in checked:
                continue
            # Only examine direct members of this group, because then the role is accurate.
            if info["distance"] == 1:
                if info["rolename"] == "member":
                    continue
                if user_is_auditor(chk_user):
                    checked.add(chk_user)
                else:
                    raise UserNotAuditor(
                        "User {} has role '{}' in the group {} but lacks the auditing "
                        "permission ('{}').".format(
                            chk_user, info["rolename"], cur_group, PERMISSION_AUDITOR
                        )
                    )
        # Now put subgroups into the queue to examine.
        for chk_group, info in iteritems(details["subgroups"]):
            if info["distance"] == 1:
                queue.append(chk_group)

    # If we didn't raise, we're valid.
    return True
Ejemplo n.º 9
0
    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()
Ejemplo n.º 10
0
    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()
Ejemplo n.º 11
0
    def initialize(self, *args, **kwargs):
        # type: (*Any, **Any) -> None
        self.graph = Graph()
        self.session = kwargs["session"]()  # type: Session
        self.template_env = kwargs["template_env"]  # type: Environment
        self.usecase_factory = kwargs[
            "usecase_factory"]  # type: UseCaseFactory

        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)
Ejemplo n.º 12
0
def assert_can_join(group: Group, user_or_group: Union[Group, User], role: str = "member") -> None:
    """Enforce audit rules on joining a group

    This applies the auditing rules to determine whether or not a given user can join the given
    group with the given role.

    Args:
        group: The group to test against.
        user: The user attempting to join.
        role: The role being tested.

    Raises:
        UserNotAuditor: If a user is found that violates the audit training policy
    """
    # By definition, any user can join as a member to any group.
    if user_or_group.type == "User" and role == "member":
        return

    # Else, we have to check if the group is audited. If not, anybody can join.
    graph = Graph()
    group_md = graph.get_group_details(group.name)
    if not group_md["audited"]:
        return

    # Audited group. Easy case, let's see if we're checking a user. If so, the user must be
    # considered an auditor.
    if user_or_group.type == "User":
        if user_is_auditor(user_or_group.name):
            return
        raise UserNotAuditor(
            "User {} lacks the auditing permission ('{}') so may only have the "
            "'member' role in this audited group.".format(user_or_group.name, PERMISSION_AUDITOR)
        )

    # No, this is a group-joining-group case. In this situation we must walk the entire group
    # subtree and ensure that all owners/np-owners/managers are considered auditors. This data
    # is contained in the group metadetails, which contains all eventual members.
    #
    # We have to fetch each group's details individually though to figure out what someone's role
    # is in that particular group.
    assert_controllers_are_auditors(user_or_group)
Ejemplo n.º 13
0
def get_audited_groups(session):
    # type: (Session) -> List[Group]
    """Returns all audited enabled groups.

    At present, this is not cached at all and returns the full list of groups from the database
    each time it's called.
    """
    audited_groups = []
    graph = Graph()
    for group in get_all_groups(session):
        try:
            group_md = graph.get_group_details(group.name)
        except NoSuchGroup:
            # Very new group with no metadata yet, or it has been disabled and
            # excluded from in-memory cache.
            continue

        if group_md.get("audited", False):
            audited_groups.append(group)

    return audited_groups
Ejemplo n.º 14
0
    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()
Ejemplo n.º 15
0
def assert_controllers_are_auditors(group: Group) -> None:
    """Return whether not all owners/np-owners/managers in a group (and below) are auditors

    This is used to ensure that all of the people who can control a group (owners, np-owners,
    managers) and all subgroups (all the way down the tree) have audit permissions.

    Raises:
        UserNotAuditor: If a user is found that violates the audit training policy
    """
    graph = Graph()
    checked: Set[str] = set()
    queue = [group.name]
    while queue:
        cur_group = queue.pop()
        if cur_group in checked:
            continue
        checked.add(cur_group)
        details = graph.get_group_details(cur_group)
        for chk_user, info in details["users"].items():
            if chk_user in checked:
                continue
            # Only examine direct members of this group, because then the role is accurate.
            if info["distance"] == 1:
                if info["rolename"] == "member":
                    continue
                if user_is_auditor(chk_user):
                    checked.add(chk_user)
                else:
                    raise UserNotAuditor(
                        "User {} has role '{}' in the group {} but lacks the auditing "
                        "permission ('{}').".format(
                            chk_user, info["rolename"], cur_group, PERMISSION_AUDITOR
                        )
                    )
        # Now put subgroups into the queue to examine.
        for chk_group, info in details["subgroups"].items():
            if info["distance"] == 1:
                queue.append(chk_group)
Ejemplo n.º 16
0
    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.groupname,
                                               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.groupname)

        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()
Ejemplo n.º 17
0
    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)
        logging.error("initialized")
Ejemplo n.º 18
0
Archivo: util.py Proyecto: rra/grouper
    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__))
Ejemplo n.º 19
0
    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()
Ejemplo n.º 20
0
    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)
Ejemplo n.º 21
0
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")
Ejemplo n.º 22
0
Archivo: util.py Proyecto: rra/grouper
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}
Ejemplo n.º 23
0
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")
Ejemplo n.º 24
0
 def graph(self):
     # type: () -> GroupGraph
     if not self._graph:
         self._graph = Graph()
     return self._graph
Ejemplo n.º 25
0
def graph(session):
    graph = Graph()
    graph.update_from_db(session)
    return graph
Ejemplo n.º 26
0
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")
Ejemplo n.º 27
0
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,
        }
Ejemplo n.º 28
0
def graph(session):
    graph = Graph()
    graph.update_from_db(session)
    return graph
Ejemplo n.º 29
0
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}