Пример #1
0
 async def configure_tracing(self):
     self.tracer = ConsoleMeTracer()
     primary_span_name = "{0} {1}".format(self.request.method.upper(),
                                          self.request.path)
     tracer_tags = {
         "http.host": config.hostname,
         "http.method": self.request.method.upper(),
         "http.path": self.request.path,
         "ca": self.get_request_ip(),  # Client IP
         "http.url": self.request.full_url(),
     }
     tracer = await self.tracer.configure_tracing(primary_span_name,
                                                  tags=tracer_tags)
     if tracer:
         for k, v in tracer.headers.items():
             self.set_header(k, v)
Пример #2
0
class BaseHandler(tornado.web.RequestHandler):
    """Default BaseHandler."""
    def get_request_ip(self):
        trusted_remote_ip_header = config.get(
            "auth.remote_ip.trusted_remote_ip_header")
        if trusted_remote_ip_header:
            return self.request.headers[trusted_remote_ip_header].split(",")[0]
        return self.request.remote_ip

    def log_exception(self, *args, **kwargs):
        if args[0].__name__ == "SilentException":
            pass
        else:
            super(BaseHandler, self).log_exception(*args, **kwargs)

    def write_error(self, status_code: int, **kwargs: Any) -> None:
        if self.settings.get("serve_traceback") and "exc_info" in kwargs:
            # in debug mode, try to send a traceback
            self.set_header("Content-Type", "text/plain")
            for line in traceback.format_exception(*kwargs["exc_info"]):
                self.write(line)
            self.finish()
        else:
            self.finish(
                "<html><title>%(code)d: %(message)s</title>"
                "<body>%(code)d: %(message)s</body></html>" % {
                    "code":
                    status_code,
                    "message":
                    f"{self._reason} - {config.get('errors.custom_website_error_message', '')}",
                })

    def data_received(self, chunk):
        """Receives the data."""
        pass

    def initialize(self, **kwargs) -> None:
        self.kwargs = kwargs
        self.tracer = None
        self.responses = []
        super(BaseHandler, self).initialize()

    async def prepare(self) -> None:
        self.tracer = None
        await self.configure_tracing()

        if config.get("tornado.xsrf", True):
            cookie_kwargs = config.get("tornado.xsrf_cookie_kwargs", {})
            self.set_cookie(
                config.get("xsrf_cookie_name", "_xsrf"),
                self.xsrf_token,
                **cookie_kwargs,
            )
        self.request_uuid = str(uuid.uuid4())
        stats.timer("base_handler.incoming_request")
        return await self.authorization_flow()

    def write(self, chunk: Union[str, bytes, dict]) -> None:
        if config.get("_security_risk_full_debugging.enabled"):
            if not hasattr(self, "responses"):
                self.responses = []
            self.responses.append(chunk)
        super(BaseHandler, self).write(chunk)

    async def configure_tracing(self):
        self.tracer = ConsoleMeTracer()
        primary_span_name = "{0} {1}".format(self.request.method.upper(),
                                             self.request.path)
        tracer_tags = {
            "http.host": config.hostname,
            "http.method": self.request.method.upper(),
            "http.path": self.request.path,
            "ca": self.get_request_ip(),  # Client IP
            "http.url": self.request.full_url(),
        }
        tracer = await self.tracer.configure_tracing(primary_span_name,
                                                     tags=tracer_tags)
        if tracer:
            for k, v in tracer.headers.items():
                self.set_header(k, v)

    def on_finish(self) -> None:
        if hasattr(self, "tracer") and self.tracer:
            asyncio.ensure_future(
                self.tracer.set_additional_tags(
                    {"http.status_code": self.get_status()}))
            asyncio.ensure_future(self.tracer.finish_spans())
            asyncio.ensure_future(self.tracer.disable_tracing())

        if config.get("_security_risk_full_debugging.enabled"):
            responses = None
            if hasattr(self, "responses"):
                responses = self.responses
            request_details = {
                "path": self.request.path,
                "method": self.request.method,
                "body": self.request.body,
                "arguments": self.request.arguments,
                "body_arguments": self.request.body_arguments,
                "headers": dict(self.request.headers.items()),
                "query": self.request.query,
                "query_arguments": self.request.query_arguments,
                "uri": self.request.uri,
                "cookies": dict(self.request.cookies.items()),
                "response": responses,
            }
            with open(config.get("_security_risk_full_debugging.file"),
                      "a+") as f:
                f.write(json.dumps(request_details, reject_bytes=False))
        super(BaseHandler, self).on_finish()

    async def attempt_sso_authn(self) -> bool:
        """
        ConsoleMe's configuration allows authenticating users by user/password, SSO, or both.
        This function helps determine how ConsoleMe should authenticate a user. If user/password login is allowed,
        users will be redirected to ConsoleMe's login page (/login). If SSO is also allowed, the Login page will present
        a button allowing the user to sign in with SSO.

        If user/password login is enabled, we don't want to give users the extra step of having to visit the login page,
        so we just authenticate them through SSO directly.
         allow authenticating users by a combination of user/password and SSO. In this case, we need to tell
        Returns: boolean
        """
        if not config.get("auth.get_user_by_password", False):
            return True

        # force_use_sso indicates the user's intent to authenticate via SSO
        force_use_sso = self.request.arguments.get("use_sso", [False])[0]
        if force_use_sso:
            return True
        # It's a redirect from an SSO provider. Let it hit the SSO functionality
        if ("code" in self.request.query_arguments
                and "state" in self.request.query_arguments):
            return True
        if self.request.path == "/saml/acs":
            return True
        return False

    async def authorization_flow(self,
                                 user: str = None,
                                 console_only: bool = True,
                                 refresh_cache: bool = False) -> None:
        """Perform high level authorization flow."""
        self.eligible_roles = []
        self.eligible_accounts = []
        self.request_uuid = str(uuid.uuid4())
        refresh_cache = (self.request.arguments.get(
            "refresh_cache", [False])[0] or refresh_cache)
        attempt_sso_authn = await self.attempt_sso_authn()

        refreshed_user_roles_from_cache = False

        if not refresh_cache and config.get(
                "dynamic_config.role_cache.always_refresh_roles_cache", False):
            refresh_cache = True

        self.red = await RedisHandler().redis()
        self.ip = self.get_request_ip()
        self.user = user
        self.groups = None
        self.user_role_name = None
        self.auth_cookie_expiration = 0

        log_data = {
            "function": "Basehandler.authorization_flow",
            "ip": self.ip,
            "request_path": self.request.uri,
            "user-agent": self.request.headers.get("User-Agent"),
            "request_id": self.request_uuid,
            "message": "Incoming request",
        }

        log.debug(log_data)

        # Check to see if user has a valid auth cookie
        if config.get("auth_cookie_name", "consoleme_auth"):
            auth_cookie = self.get_cookie(
                config.get("auth_cookie_name", "consoleme_auth"))

            # Validate auth cookie and use it to retrieve group information
            if auth_cookie:
                res = await validate_and_return_jwt_token(auth_cookie)
                if res and isinstance(res, dict):
                    self.user = res.get("user")
                    self.groups = res.get("groups")
                    self.auth_cookie_expiration = res.get("exp")

        if not self.user:
            # Check for development mode and a configuration override that specify the user and their groups.
            if config.get("development") and config.get(
                    "_development_user_override"):
                self.user = config.get("_development_user_override")
            if config.get("development") and config.get(
                    "_development_groups_override"):
                self.groups = config.get("_development_groups_override")

        if not self.user:
            # SAML flow. If user has a JWT signed by ConsoleMe, and SAML is enabled in configuration, user will go
            # through this flow.

            if config.get("auth.get_user_by_saml",
                          False) and attempt_sso_authn:
                res = await authenticate_user_by_saml(self)
                if not res:
                    if (self.request.uri != "/saml/acs"
                            and not self.request.uri.startswith("/auth?")):
                        raise SilentException(
                            "Unable to authenticate the user by SAML. "
                            "Redirecting to authentication endpoint")
                    return

        if not self.user:
            if config.get("auth.get_user_by_oidc",
                          False) and attempt_sso_authn:
                res = await authenticate_user_by_oidc(self)
                if not res:
                    raise SilentException(
                        "Unable to authenticate the user by OIDC. "
                        "Redirecting to authentication endpoint")
                if res and isinstance(res, dict):
                    self.user = res.get("user")
                    self.groups = res.get("groups")

        if not self.user:
            if config.get("auth.get_user_by_aws_alb_auth", False):
                res = await authenticate_user_by_alb_auth(self)
                if not res:
                    raise Exception(
                        "Unable to authenticate the user by ALB Auth")
                if res and isinstance(res, dict):
                    self.user = res.get("user")
                    self.groups = res.get("groups")

        if not self.user:
            # Username/Password authn flow
            if config.get("auth.get_user_by_password", False):
                after_redirect_uri = self.request.arguments.get(
                    "redirect_url", [""])[0]
                if after_redirect_uri and isinstance(after_redirect_uri,
                                                     bytes):
                    after_redirect_uri = after_redirect_uri.decode("utf-8")
                self.set_status(403)
                self.write({
                    "type":
                    "redirect",
                    "redirect_url":
                    f"/login?redirect_after_auth={after_redirect_uri}",
                    "reason":
                    "unauthenticated",
                    "message":
                    "User is not authenticated. Redirect to authenticate",
                })
                await self.finish()
                raise SilentException(
                    "Redirecting user to authenticate by username/password.")

        if not self.user:
            try:
                # Get user. Config options can specify getting username from headers or
                # OIDC, but custom plugins are also allowed to override this.
                self.user = await auth.get_user(headers=self.request.headers)
                if not self.user:
                    raise NoUserException(
                        f"User not detected. Headers: {self.request.headers}")
                log_data["user"] = self.user
            except NoUserException:
                self.clear()
                self.set_status(403)

                stats.count(
                    "Basehandler.authorization_flow.no_user_detected",
                    tags={
                        "request_path": self.request.uri,
                        "ip": self.ip,
                        "user_agent": self.request.headers.get("User-Agent"),
                    },
                )
                log_data["message"] = "No user detected. Check configuration."
                log.error(log_data)
                await self.finish(log_data["message"])
                raise

        self.contractor = config.config_plugin().is_contractor(self.user)

        if config.get("auth.cache_user_info_server_side",
                      True) and not refresh_cache:
            try:
                cache_r = self.red.get(
                    f"USER-{self.user}-CONSOLE-{console_only}")
            except redis.exceptions.ConnectionError:
                cache_r = None
            if cache_r:
                log_data["message"] = "Loading from cache"
                log.debug(log_data)
                cache = json.loads(cache_r)
                self.groups = cache.get("groups")
                self.eligible_roles = cache.get("eligible_roles")
                self.eligible_accounts = cache.get("eligible_accounts")
                self.user_role_name = cache.get("user_role_name")
                refreshed_user_roles_from_cache = True

        try:
            if not self.groups:
                self.groups = await auth.get_groups(
                    self.user, headers=self.request.headers)
            if not self.groups:
                raise NoGroupsException(
                    f"Groups not detected. Headers: {self.request.headers}")

        except NoGroupsException:
            stats.count("Basehandler.authorization_flow.no_groups_detected")
            log_data["message"] = "No groups detected. Check configuration."
            log.error(log_data)

        # Set Per-User Role Name (This logic is not used in OSS deployment)
        if (config.get("user_roles.opt_in_group")
                and config.get("user_roles.opt_in_group") in self.groups):
            # Get or create user_role_name attribute
            self.user_role_name = await auth.get_or_create_user_role_name(
                self.user)

        self.eligible_roles = await group_mapping.get_eligible_roles(
            self.user,
            self.groups,
            self.user_role_name,
            console_only=console_only)

        if not self.eligible_roles:
            log_data[
                "message"] = "No eligible roles detected for user. But letting them continue"
            log.error(log_data)
        log_data["eligible_roles"] = len(self.eligible_roles)

        if not self.eligible_accounts:
            try:
                self.eligible_accounts = await group_mapping.get_eligible_accounts(
                    self.eligible_roles)
                log_data["eligible_accounts"] = len(self.eligible_accounts)
                log_data["message"] = "Successfully authorized user."
                log.debug(log_data)
            except Exception:
                stats.count("Basehandler.authorization_flow.exception")
                log.error(log_data, exc_info=True)
                raise
        if (config.get("auth.cache_user_info_server_side", True)
                and self.groups
                # Only set role cache if we didn't retrieve user's existing roles from cache
                and not refreshed_user_roles_from_cache):
            try:
                self.red.setex(
                    f"USER-{self.user}-CONSOLE-{console_only}",
                    config.get("dynamic_config.role_cache.cache_expiration",
                               60),
                    json.dumps({
                        "groups": self.groups,
                        "eligible_roles": self.eligible_roles,
                        "eligible_accounts": self.eligible_accounts,
                        "user_role_name": self.user_role_name,
                    }),
                )
            except redis.exceptions.ConnectionError:
                pass
        if (config.get("auth.set_auth_cookie")
                and config.get("auth_cookie_name", "consoleme_auth")
                and not self.get_cookie(
                    config.get("auth_cookie_name", "consoleme_auth"))):
            expiration = datetime.utcnow().replace(
                tzinfo=pytz.UTC) + timedelta(
                    minutes=config.get("jwt.expiration_minutes", 60))

            encoded_cookie = await generate_jwt_token(self.user,
                                                      self.groups,
                                                      exp=expiration)
            self.set_cookie(
                config.get("auth_cookie_name", "consoleme_auth"),
                encoded_cookie,
                expires=expiration,
                secure=config.get(
                    "auth.cookie.secure",
                    "https://" in config.get("url"),
                ),
                httponly=config.get("auth.cookie.httponly", True),
                samesite=config.get("auth.cookie.samesite", True),
            )
        if self.tracer:
            await self.tracer.set_additional_tags({"USER": self.user})