def test_execute_sql_statements_no_results_backend(
        self, mock_execute_sql_statement, mock_get_query
    ):
        sql = """
            -- comment
            SET @value = 42;
            SELECT @value AS foo;
            -- comment
        """
        mock_session = mock.MagicMock()
        mock_query = mock.MagicMock()
        mock_query.database.allow_run_async = True
        mock_cursor = mock.MagicMock()
        mock_query.database.get_sqla_engine.return_value.raw_connection.return_value.cursor.return_value = (
            mock_cursor
        )
        mock_query.database.db_engine_spec.run_multiple_statements_as_one = False
        mock_get_query.return_value = mock_query

        with pytest.raises(SupersetErrorException) as excinfo:
            execute_sql_statements(
                query_id=1,
                rendered_query=sql,
                return_results=True,
                store_results=False,
                session=mock_session,
                start_time=None,
                expand_data=False,
                log_params=None,
            )

        assert excinfo.value.error == SupersetError(
            message="Results backend is not configured.",
            error_type=SupersetErrorType.RESULTS_BACKEND_NOT_CONFIGURED_ERROR,
            level=ErrorLevel.ERROR,
            extra={
                "issue_codes": [
                    {
                        "code": 1021,
                        "message": (
                            "Issue 1021 - Results backend needed for asynchronous "
                            "queries is not configured."
                        ),
                    }
                ]
            },
        )
def check_ownership(obj: Any, raise_if_false: bool = True) -> bool:
    """Meant to be used in `pre_update` hooks on models to enforce ownership

    Admin have all access, and other users need to be referenced on either
    the created_by field that comes with the ``AuditMixin``, or in a field
    named ``owners`` which is expected to be a one-to-many with the User
    model. It is meant to be used in the ModelView's pre_update hook in
    which raising will abort the update.
    """
    if not obj:
        return False

    security_exception = SupersetSecurityException(
        SupersetError(
            error_type=SupersetErrorType.MISSING_OWNERSHIP_ERROR,
            message="You don't have the rights to alter [{}]".format(obj),
            level=ErrorLevel.ERROR,
        ))

    if g.user.is_anonymous:
        if raise_if_false:
            raise security_exception
        return False
    roles = [r.name for r in get_user_roles()]
    if "Admin" in roles:
        return True
    scoped_session = db.create_scoped_session()
    orig_obj = scoped_session.query(obj.__class__).filter_by(id=obj.id).first()

    # Making a list of owners that works across ORM models
    owners: List[User] = []
    if hasattr(orig_obj, "owners"):
        owners += orig_obj.owners
    if hasattr(orig_obj, "owner"):
        owners += [orig_obj.owner]
    if hasattr(orig_obj, "created_by"):
        owners += [orig_obj.created_by]

    owner_names = [o.username for o in owners if o]

    if g.user and hasattr(g.user,
                          "username") and g.user.username in owner_names:
        return True
    if raise_if_false:
        raise security_exception
    else:
        return False
Beispiel #3
0
    def validate_parameters(self) -> FlaskResponse:
        """validates database connection parameters
        ---
        post:
          description: >-
            Validates parameters used to connect to a database
          requestBody:
            description: DB-specific parameters
            required: true
            content:
              application/json:
                schema:
                  $ref: "#/components/schemas/DatabaseValidateParametersSchema"
          responses:
            200:
              description: Database Test Connection
              content:
                application/json:
                  schema:
                    type: object
                    properties:
                      message:
                        type: string
            400:
              $ref: '#/components/responses/400'
            422:
              $ref: '#/components/responses/422'
            500:
              $ref: '#/components/responses/500'
        """
        try:
            payload = DatabaseValidateParametersSchema().load(request.json)
        except ValidationError as ex:
            errors = [
                SupersetError(
                    message="\n".join(messages),
                    error_type=SupersetErrorType.INVALID_PAYLOAD_SCHEMA_ERROR,
                    level=ErrorLevel.ERROR,
                    extra={"invalid": [attribute]},
                )
                for attribute, messages in ex.messages.items()
            ]
            raise InvalidParametersError(errors) from ex

        command = ValidateDatabaseParametersCommand(g.user, payload)
        command.run()
        return self.response(200, message="OK")
Beispiel #4
0
    def get_datasource_access_error_object(  # pylint: disable=invalid-name
            self, datasource: "BaseDatasource") -> SupersetError:
        """
        Return the error object for the denied Superset datasource.

        :param datasource: The denied Superset datasource
        :returns: The error object
        """
        return SupersetError(
            error_type=SupersetErrorType.DATASOURCE_SECURITY_ACCESS_ERROR,
            message=self.get_datasource_access_error_msg(datasource),
            level=ErrorLevel.ERROR,
            extra={
                "link": self.get_datasource_access_link(datasource),
                "datasource": datasource.name,
            },
        )
    def test_test_connection_failed_invalid_hostname(
        self, mock_event_logger, mock_build_db
    ):
        """
        Database API: Test test connection failed due to invalid hostname
        """
        msg = 'psql: error: could not translate host name "locahost" to address: nodename nor servname provided, or not known'
        mock_build_db.return_value.set_sqlalchemy_uri.side_effect = DBAPIError(
            msg, None, None
        )
        mock_build_db.return_value.db_engine_spec.__name__ = "Some name"
        superset_error = SupersetError(
            message='Unable to resolve hostname "locahost".',
            error_type="TEST_CONNECTION_INVALID_HOSTNAME_ERROR",
            level="error",
            extra={
                "hostname": "locahost",
                "issue_codes": [
                    {
                        "code": 1007,
                        "message": (
                            "Issue 1007 - The hostname provided can't be resolved."
                        ),
                    }
                ],
            },
        )
        mock_build_db.return_value.db_engine_spec.extract_errors.return_value = [
            superset_error
        ]

        self.login("admin")
        data = {
            "sqlalchemy_uri": "postgres://*****:*****@locahost:12345/db",
            "database_name": "examples",
            "impersonate_user": False,
            "server_cert": None,
        }
        url = "api/v1/database/test_connection"
        rv = self.post_assert_metric(url, data, "test_connection")

        assert rv.status_code == 422
        assert rv.headers["Content-Type"] == "application/json; charset=utf-8"
        response = json.loads(rv.data.decode("utf-8"))
        expected_response = {"errors": [dataclasses.asdict(superset_error)]}
        assert response == expected_response
Beispiel #6
0
def show_http_exception(ex: HTTPException) -> FlaskResponse:
    logger.warning("HTTPException", exc_info=True)
    if ("text/html" in request.accept_mimetypes and not config["DEBUG"]
            and ex.code in {404, 500}):
        path = resource_filename("superset", f"static/assets/{ex.code}.html")
        return send_file(path, cache_timeout=0), ex.code

    return json_errors_response(
        errors=[
            SupersetError(
                message=utils.error_msg_from_exception(ex),
                error_type=SupersetErrorType.GENERIC_BACKEND_ERROR,
                level=ErrorLevel.ERROR,
            ),
        ],
        status=ex.code or 500,
    )
Beispiel #7
0
    def get_table_access_error_object(self,
                                      tables: Set["Table"]) -> SupersetError:
        """
        Return the error object for the denied SQL tables.

        :param tables: The set of denied SQL tables
        :returns: The error object
        """
        return SupersetError(
            error_type=SupersetErrorType.TABLE_SECURITY_ACCESS_ERROR,
            message=self.get_table_access_error_msg(tables),
            level=ErrorLevel.ERROR,
            extra={
                "link": self.get_table_access_link(tables),
                "tables": [str(table) for table in tables],
            },
        )
def show_command_errors(ex: CommandException) -> FlaskResponse:
    logger.warning(ex)
    if "text/html" in request.accept_mimetypes and not config["DEBUG"]:
        path = resource_filename("superset", "static/assets/500.html")
        return send_file(path, cache_timeout=0), 500

    extra = ex.normalized_messages() if isinstance(ex, CommandInvalidError) else {}
    return json_errors_response(
        errors=[
            SupersetError(
                message=ex.message,
                error_type=SupersetErrorType.GENERIC_COMMAND_ERROR,
                level=get_error_level_from_status_code(ex.status),
                extra=extra,
            ),
        ],
        status=ex.status,
    )
Beispiel #9
0
    def test_connection_superset_security_connection(self, mock_event_logger,
                                                     mock_get_sqla_engine):
        """Test to make sure event_logger is called when security
        connection exc is raised"""
        database = get_example_database()
        mock_get_sqla_engine.side_effect = SupersetSecurityException(
            SupersetError(error_type=500, message="test", level="info"))
        db_uri = database.sqlalchemy_uri_decrypted
        json_payload = {"sqlalchemy_uri": db_uri}
        command_without_db_name = TestConnectionDatabaseCommand(
            security_manager.find_user("admin"), json_payload)

        with pytest.raises(DatabaseSecurityUnsafeError) as excinfo:
            command_without_db_name.run()
            assert str(
                excinfo.value) == ("Stopped an unsafe database connection")

        mock_event_logger.assert_called()
    def execute(
        self,
        execution_context: SqlJsonExecutionContext,
        rendered_query: str,
        log_params: Optional[Dict[str, Any]],
    ) -> SqlJsonExecutionStatus:

        query_id = execution_context.query.id
        logger.info("Query %i: Running query on a Celery worker", query_id)
        try:
            task = self._get_sql_results_task.delay(  # type: ignore
                query_id,
                rendered_query,
                return_results=False,
                store_results=not execution_context.select_as_cta,
                username=get_username(),
                start_time=now_as_float(),
                expand_data=execution_context.expand_data,
                log_params=log_params,
            )
            try:
                task.forget()
            except NotImplementedError:
                logger.warning(
                    "Unable to forget Celery task as backend"
                    "does not support this operation"
                )
        except Exception as ex:
            logger.exception("Query %i: %s", query_id, str(ex))

            message = __("Failed to start remote query on a worker.")
            error = SupersetError(
                message=message,
                error_type=SupersetErrorType.ASYNC_WORKERS_ERROR,
                level=ErrorLevel.ERROR,
            )
            error_payload = dataclasses.asdict(error)
            query = execution_context.query
            query.set_extra_json_key("errors", [error_payload])
            query.status = QueryStatus.FAILED
            query.error_message = message
            raise SupersetErrorException(error) from ex
        self._query_dao.update_saved_query_exec_info(query_id)
        return SqlJsonExecutionStatus.QUERY_IS_RUNNING
Beispiel #11
0
 def test_extract_errors(self):
     msg = "403 POST https://bigquery.googleapis.com/bigquery/v2/projects/test-keel-310804/jobs?prettyPrint=false: Access Denied: Project User does not have bigquery.jobs.create permission in project profound-keel-310804"
     result = BigQueryEngineSpec.extract_errors(Exception(msg))
     assert result == [
         SupersetError(
             message=
             "We were unable to connect to your database. Please confirm that your service account has the Viewer and Job User roles on the project.",
             error_type=SupersetErrorType.
             CONNECTION_DATABASE_PERMISSIONS_ERROR,
             level=ErrorLevel.ERROR,
             extra={
                 "engine_name": "Google BigQuery",
                 "issue_codes": [{
                     "code": 1017,
                     "message": "",
                 }],
             },
         )
     ]
Beispiel #12
0
def test_validate_parameters_missing():
    parameters = {
        "host": "",
        "port": None,
        "username": "",
        "password": "",
        "database": "",
        "query": {},
    }
    errors = BasicParametersMixin.validate_parameters(parameters)
    assert errors == [
        SupersetError(
            message=("One or more parameters are missing: "
                     "database, host, port, username"),
            error_type=SupersetErrorType.CONNECTION_MISSING_PARAMETERS_ERROR,
            level=ErrorLevel.WARNING,
            extra={"missing": ["database", "host", "port", "username"]},
        ),
    ]
Beispiel #13
0
    def run(  # pylint: disable=too-many-statements,useless-suppression
        self, ) -> CommandResult:
        """Runs arbitrary sql and returns data as json"""
        try:
            query = self._try_get_existing_query()
            if self.is_query_handled(query):
                self._execution_context.set_query(query)  # type: ignore
                status = SqlJsonExecutionStatus.QUERY_ALREADY_CREATED
            else:
                status = self._run_sql_json_exec_from_scratch()

            self._execution_context_convertor.set_payload(
                self._execution_context, status)

            # save columns into metadata_json
            self._query_dao.save_metadata(
                self._execution_context.query,
                self._execution_context_convertor.payload)

            return {
                "status": status,
                "payload":
                self._execution_context_convertor.serialize_payload(),
            }
        except SupersetErrorsException as ex:
            if all(ex.error_type == SupersetErrorType.SYNTAX_ERROR
                   for ex in ex.errors):
                raise SupersetSyntaxErrorException(ex.errors) from ex
            raise ex
        except SupersetException as ex:
            if ex.error_type == SupersetErrorType.SYNTAX_ERROR:
                raise SupersetSyntaxErrorException([
                    SupersetError(
                        message=ex.message,
                        error_type=ex.error_type,
                        level=ErrorLevel.ERROR,
                    )
                ]) from ex
            raise ex
        except Exception as ex:
            raise SqlLabException(self._execution_context,
                                  exception=ex) from ex
Beispiel #14
0
def show_http_exception(ex: HTTPException) -> FlaskResponse:
    logger.warning(ex)
    if (
        "text/html" in request.accept_mimetypes
        and not config["DEBUG"]
        and ex.code in {404, 500}
    ):
        return redirect(f"/static/assets/{ex.code}.html")

    return json_errors_response(
        errors=[
            SupersetError(
                message=utils.error_msg_from_exception(ex),
                error_type=SupersetErrorType.GENERIC_BACKEND_ERROR,
                level=ErrorLevel.ERROR,
                extra={},
            ),
        ],
        status=ex.code or 500,
    )
Beispiel #15
0
 def test_extract_errors(self):
     """
     Test that custom error messages are extracted correctly.
     """
     msg = ": mismatched input 'fromm'. Expecting: "
     result = AthenaEngineSpec.extract_errors(Exception(msg))
     assert result == [
         SupersetError(
             message='Please check your query for syntax errors at or near "fromm". Then, try running your query again.',
             error_type=SupersetErrorType.SYNTAX_ERROR,
             level=ErrorLevel.ERROR,
             extra={
                 "engine_name": "Amazon Athena",
                 "issue_codes": [
                     {
                         "code": 1030,
                         "message": "Issue 1030 - The query has a syntax error.",
                     }
                 ],
             },
         )
     ]
def test_validate_parameters_simple(mocker: MockFixture, ) -> None:
    from superset.db_engine_specs.gsheets import (
        GSheetsEngineSpec,
        GSheetsParametersType,
    )

    parameters: GSheetsParametersType = {
        "service_account_info": "",
        "catalog": {},
    }
    errors = GSheetsEngineSpec.validate_parameters(parameters)
    assert errors == [
        SupersetError(
            message="Sheet name is required",
            error_type=SupersetErrorType.CONNECTION_MISSING_PARAMETERS_ERROR,
            level=ErrorLevel.WARNING,
            extra={"catalog": {
                "idx": 0,
                "name": True
            }},
        ),
    ]
Beispiel #17
0
    def run(self) -> List[Dict[str, Any]]:
        """
        Validates a SQL statement

        :return: A List of SQLValidationAnnotation
        :raises: DatabaseNotFoundError, NoValidatorConfigFoundError
          NoValidatorFoundError, ValidatorSQLUnexpectedError, ValidatorSQLError
          ValidatorSQL400Error
        """
        self.validate()
        if not self._validator or not self._model:
            raise ValidatorSQLUnexpectedError()
        sql = self._properties["sql"]
        schema = self._properties.get("schema")
        try:
            timeout = current_app.config["SQLLAB_VALIDATION_TIMEOUT"]
            timeout_msg = f"The query exceeded the {timeout} seconds timeout."
            with utils.timeout(seconds=timeout, error_message=timeout_msg):
                errors = self._validator.validate(sql, schema, self._model)
            return [err.to_dict() for err in errors]
        except Exception as ex:
            logger.exception(ex)
            superset_error = SupersetError(
                message=__(
                    "%(validator)s was unable to check your query.\n"
                    "Please recheck your query.\n"
                    "Exception: %(ex)s",
                    validator=self._validator.name,
                    ex=ex,
                ),
                error_type=SupersetErrorType.GENERIC_DB_ENGINE_ERROR,
                level=ErrorLevel.ERROR,
            )

            # Return as a 400 if the database error message says we got a 4xx error
            if re.search(r"([\W]|^)4\d{2}([\W]|$)", str(ex)):
                raise ValidatorSQL400Error(superset_error) from ex
            raise ValidatorSQLError(superset_error) from ex
def validate_adhoc_subquery(raw_sql: str) -> None:
    """
    Check if adhoc SQL contains sub-queries or nested sub-queries with table
    :param raw_sql: adhoc sql expression
    :raise SupersetSecurityException if sql contains sub-queries or
    nested sub-queries with table
    """
    # pylint: disable=import-outside-toplevel
    from superset import is_feature_enabled

    if is_feature_enabled("ALLOW_ADHOC_SUBQUERY"):
        return

    for statement in sqlparse.parse(raw_sql):
        if has_table_query(statement):
            raise SupersetSecurityException(
                SupersetError(
                    error_type=SupersetErrorType.
                    ADHOC_SUBQUERY_NOT_ALLOWED_ERROR,
                    message=_("Custom SQL fields cannot contain sub-queries."),
                    level=ErrorLevel.ERROR,
                ))
    return
Beispiel #19
0
def test_validate_partial(is_port_open, is_hostname_valid, app_context):
    """
    Test parameter validation when only some parameters are present.
    """
    is_hostname_valid.return_value = True
    is_port_open.return_value = True

    payload = {
        "engine": "postgresql",
        "parameters": {
            "host": "localhost",
            "port": 5432,
            "username": "",
            "password": "******",
            "database": "test",
            "query": {},
        },
    }
    command = ValidateDatabaseParametersCommand(None, payload)
    with pytest.raises(SupersetErrorsException) as excinfo:
        command.run()
    assert excinfo.value.errors == [
        SupersetError(
            message="One or more parameters are missing: username",
            error_type=SupersetErrorType.CONNECTION_MISSING_PARAMETERS_ERROR,
            level=ErrorLevel.WARNING,
            extra={
                "missing": ["username"],
                "issue_codes": [{
                    "code":
                    1018,
                    "message":
                    "Issue 1018 - One or more parameters needed to configure a database are missing.",
                }],
            },
        )
    ]
Beispiel #20
0
 def test_extract_errors(self):
     """
     Test that custom error messages are extracted correctly.
     """
     msg = 'SQLError: near "fromm": syntax error'
     result = GSheetsEngineSpec.extract_errors(Exception(msg))
     assert result == [
         SupersetError(
             message=
             'Please check your query for syntax errors near "fromm". Then, try running your query again.',
             error_type=SupersetErrorType.SYNTAX_ERROR,
             level=ErrorLevel.ERROR,
             extra={
                 "engine_name":
                 "Google Sheets",
                 "issue_codes": [{
                     "code":
                     1030,
                     "message":
                     "Issue 1030 - The query has a syntax error.",
                 }],
             },
         )
     ]
def test_validate_parameters_catalog(
    mocker: MockFixture,
    app_context: AppContext,
) -> None:
    from superset.db_engine_specs.gsheets import (
        GSheetsEngineSpec,
        GSheetsParametersType,
    )

    g = mocker.patch("superset.db_engine_specs.gsheets.g")
    g.user.email = "*****@*****.**"

    create_engine = mocker.patch(
        "superset.db_engine_specs.gsheets.create_engine")
    conn = create_engine.return_value.connect.return_value
    results = conn.execute.return_value
    results.fetchall.side_effect = [
        ProgrammingError("The caller does not have permission"),
        [(1, )],
        ProgrammingError("Unsupported table: https://www.google.com/"),
    ]

    parameters: GSheetsParametersType = {
        "credentials_info": {},
        "catalog": {
            "private_sheet": "https://docs.google.com/spreadsheets/d/1/edit",
            "public_sheet":
            "https://docs.google.com/spreadsheets/d/1/edit#gid=1",
            "not_a_sheet": "https://www.google.com/",
        },
    }
    errors = GSheetsEngineSpec.validate_parameters(parameters)  # ignore: type

    assert errors == [
        SupersetError(
            message="URL could not be identified",
            error_type=SupersetErrorType.TABLE_DOES_NOT_EXIST_ERROR,
            level=ErrorLevel.WARNING,
            extra={
                "invalid": ["catalog"],
                "name":
                "private_sheet",
                "url":
                "https://docs.google.com/spreadsheets/d/1/edit",
                "issue_codes": [
                    {
                        "code":
                        1003,
                        "message":
                        "Issue 1003 - There is a syntax error in the SQL query. Perhaps there was a misspelling or a typo.",
                    },
                    {
                        "code":
                        1005,
                        "message":
                        "Issue 1005 - The table was deleted or renamed in the database.",
                    },
                ],
            },
        ),
        SupersetError(
            message="URL could not be identified",
            error_type=SupersetErrorType.TABLE_DOES_NOT_EXIST_ERROR,
            level=ErrorLevel.WARNING,
            extra={
                "invalid": ["catalog"],
                "name":
                "not_a_sheet",
                "url":
                "https://www.google.com/",
                "issue_codes": [
                    {
                        "code":
                        1003,
                        "message":
                        "Issue 1003 - There is a syntax error in the SQL query. Perhaps there was a misspelling or a typo.",
                    },
                    {
                        "code":
                        1005,
                        "message":
                        "Issue 1005 - The table was deleted or renamed in the database.",
                    },
                ],
            },
        ),
    ]

    create_engine.assert_called_with(
        "gsheets://",
        service_account_info={},
        subject="*****@*****.**",
    )
    def test_extract_errors(self):
        """
        Test that custom error messages are extracted correctly.
        """
        msg = 'psql: error: FATAL:  role "testuser" does not exist'
        result = PostgresEngineSpec.extract_errors(Exception(msg))
        assert result == [
            SupersetError(
                error_type=SupersetErrorType.CONNECTION_INVALID_USERNAME_ERROR,
                message='The username "testuser" does not exist.',
                level=ErrorLevel.ERROR,
                extra={
                    "engine_name":
                    "PostgreSQL",
                    "issue_codes": [
                        {
                            "code":
                            1012,
                            "message":
                            ("Issue 1012 - The username provided when "
                             "connecting to a database is not valid."),
                        },
                    ],
                },
            )
        ]

        msg = (
            'psql: error: could not translate host name "locahost" to address: '
            "nodename nor servname provided, or not known")
        result = PostgresEngineSpec.extract_errors(Exception(msg))
        assert result == [
            SupersetError(
                error_type=SupersetErrorType.CONNECTION_INVALID_HOSTNAME_ERROR,
                message='The hostname "locahost" cannot be resolved.',
                level=ErrorLevel.ERROR,
                extra={
                    "engine_name":
                    "PostgreSQL",
                    "issue_codes": [{
                        "code":
                        1007,
                        "message":
                        "Issue 1007 - The hostname provided "
                        "can't be resolved.",
                    }],
                },
            )
        ]

        msg = dedent("""
psql: error: could not connect to server: Connection refused
        Is the server running on host "localhost" (::1) and accepting
        TCP/IP connections on port 12345?
could not connect to server: Connection refused
        Is the server running on host "localhost" (127.0.0.1) and accepting
        TCP/IP connections on port 12345?
            """)
        result = PostgresEngineSpec.extract_errors(Exception(msg))
        assert result == [
            SupersetError(
                error_type=SupersetErrorType.CONNECTION_PORT_CLOSED_ERROR,
                message=
                'Port 12345 on hostname "localhost" refused the connection.',
                level=ErrorLevel.ERROR,
                extra={
                    "engine_name":
                    "PostgreSQL",
                    "issue_codes": [{
                        "code":
                        1008,
                        "message":
                        "Issue 1008 - The port is closed."
                    }],
                },
            )
        ]

        msg = dedent("""
psql: error: could not connect to server: Operation timed out
        Is the server running on host "example.com" (93.184.216.34) and accepting
        TCP/IP connections on port 12345?
            """)
        result = PostgresEngineSpec.extract_errors(Exception(msg))
        assert result == [
            SupersetError(
                error_type=SupersetErrorType.CONNECTION_HOST_DOWN_ERROR,
                message=('The host "example.com" might be down, '
                         "and can't be reached on port 12345."),
                level=ErrorLevel.ERROR,
                extra={
                    "engine_name":
                    "PostgreSQL",
                    "issue_codes": [{
                        "code":
                        1009,
                        "message":
                        "Issue 1009 - The host might be down, "
                        "and can't be reached on the provided port.",
                    }],
                },
            )
        ]

        # response with IP only
        msg = dedent("""
psql: error: could not connect to server: Operation timed out
        Is the server running on host "93.184.216.34" and accepting
        TCP/IP connections on port 12345?
            """)
        result = PostgresEngineSpec.extract_errors(Exception(msg))
        assert result == [
            SupersetError(
                error_type=SupersetErrorType.CONNECTION_HOST_DOWN_ERROR,
                message=('The host "93.184.216.34" might be down, '
                         "and can't be reached on port 12345."),
                level=ErrorLevel.ERROR,
                extra={
                    "engine_name":
                    "PostgreSQL",
                    "issue_codes": [{
                        "code":
                        1009,
                        "message":
                        "Issue 1009 - The host might be down, "
                        "and can't be reached on the provided port.",
                    }],
                },
            )
        ]

        msg = 'FATAL:  password authentication failed for user "postgres"'
        result = PostgresEngineSpec.extract_errors(Exception(msg))
        assert result == [
            SupersetError(
                error_type=SupersetErrorType.CONNECTION_INVALID_PASSWORD_ERROR,
                message=
                ('The password provided for username "postgres" is incorrect.'
                 ),
                level=ErrorLevel.ERROR,
                extra={
                    "engine_name":
                    "PostgreSQL",
                    "issue_codes": [
                        {
                            "code":
                            1013,
                            "message":
                            ("Issue 1013 - The password provided when "
                             "connecting to a database is not valid."),
                        },
                    ],
                },
            )
        ]

        msg = 'database "badDB" does not exist'
        result = PostgresEngineSpec.extract_errors(Exception(msg))
        assert result == [
            SupersetError(
                message='Unable to connect to database "badDB".',
                error_type=SupersetErrorType.CONNECTION_UNKNOWN_DATABASE_ERROR,
                level=ErrorLevel.ERROR,
                extra={
                    "engine_name":
                    "PostgreSQL",
                    "issue_codes": [{
                        "code":
                        1015,
                        "message":
                        ("Issue 1015 - Either the database is spelled "
                         "incorrectly or does not exist.", ),
                    }],
                },
            )
        ]
Beispiel #23
0
def execute_sql_statements(  # pylint: disable=too-many-arguments, too-many-locals, too-many-statements, too-many-branches
    query_id: int,
    rendered_query: str,
    return_results: bool,
    store_results: bool,
    user_name: Optional[str],
    session: Session,
    start_time: Optional[float],
    expand_data: bool,
    log_params: Optional[Dict[str, Any]],
) -> Optional[Dict[str, Any]]:
    """Executes the sql query returns the results."""
    if store_results and start_time:
        # only asynchronous queries
        stats_logger.timing("sqllab.query.time_pending", now_as_float() - start_time)

    query = get_query(query_id, session)
    payload: Dict[str, Any] = dict(query_id=query_id)
    database = query.database
    db_engine_spec = database.db_engine_spec
    db_engine_spec.patch()

    if database.allow_run_async and not results_backend:
        raise SupersetErrorException(
            SupersetError(
                message=__("Results backend is not configured."),
                error_type=SupersetErrorType.RESULTS_BACKEND_NOT_CONFIGURED_ERROR,
                level=ErrorLevel.ERROR,
            )
        )

    # Breaking down into multiple statements
    parsed_query = ParsedQuery(rendered_query, strip_comments=True)
    if not db_engine_spec.run_multiple_statements_as_one:
        statements = parsed_query.get_statements()
        logger.info(
            "Query %s: Executing %i statement(s)", str(query_id), len(statements)
        )
    else:
        statements = [rendered_query]
        logger.info("Query %s: Executing query as a single statement", str(query_id))

    logger.info("Query %s: Set query to 'running'", str(query_id))
    query.status = QueryStatus.RUNNING
    query.start_running_time = now_as_float()
    session.commit()

    # Should we create a table or view from the select?
    if (
        query.select_as_cta
        and query.ctas_method == CtasMethod.TABLE
        and not parsed_query.is_valid_ctas()
    ):
        raise SupersetErrorException(
            SupersetError(
                message=__(
                    "CTAS (create table as select) can only be run with a query where "
                    "the last statement is a SELECT. Please make sure your query has "
                    "a SELECT as its last statement. Then, try running your query "
                    "again."
                ),
                error_type=SupersetErrorType.INVALID_CTAS_QUERY_ERROR,
                level=ErrorLevel.ERROR,
            )
        )
    if (
        query.select_as_cta
        and query.ctas_method == CtasMethod.VIEW
        and not parsed_query.is_valid_cvas()
    ):
        raise SupersetErrorException(
            SupersetError(
                message=__(
                    "CVAS (create view as select) can only be run with a query with "
                    "a single SELECT statement. Please make sure your query has only "
                    "a SELECT statement. Then, try running your query again."
                ),
                error_type=SupersetErrorType.INVALID_CVAS_QUERY_ERROR,
                level=ErrorLevel.ERROR,
            )
        )

    engine = database.get_sqla_engine(
        schema=query.schema,
        nullpool=True,
        user_name=user_name,
        source=QuerySource.SQL_LAB,
    )
    # Sharing a single connection and cursor across the
    # execution of all statements (if many)
    with closing(engine.raw_connection()) as conn:
        # closing the connection closes the cursor as well
        cursor = conn.cursor()
        statement_count = len(statements)
        for i, statement in enumerate(statements):
            # Check if stopped
            query = get_query(query_id, session)
            if query.status == QueryStatus.STOPPED:
                return None

            # For CTAS we create the table only on the last statement
            apply_ctas = query.select_as_cta and (
                query.ctas_method == CtasMethod.VIEW
                or (query.ctas_method == CtasMethod.TABLE and i == len(statements) - 1)
            )

            # Run statement
            msg = f"Running statement {i+1} out of {statement_count}"
            logger.info("Query %s: %s", str(query_id), msg)
            query.set_extra_json_key("progress", msg)
            session.commit()
            try:
                result_set = execute_sql_statement(
                    statement,
                    query,
                    user_name,
                    session,
                    cursor,
                    log_params,
                    apply_ctas,
                )
            except Exception as ex:  # pylint: disable=broad-except
                msg = str(ex)
                prefix_message = (
                    f"[Statement {i+1} out of {statement_count}]"
                    if statement_count > 1
                    else ""
                )
                payload = handle_query_error(
                    ex, query, session, payload, prefix_message
                )
                return payload

        # Commit the connection so CTA queries will create the table.
        conn.commit()

    # Success, updating the query entry in database
    query.rows = result_set.size
    query.progress = 100
    query.set_extra_json_key("progress", None)
    if query.select_as_cta:
        query.select_sql = database.select_star(
            query.tmp_table_name,
            schema=query.tmp_schema_name,
            limit=query.limit,
            show_cols=False,
            latest_partition=False,
        )
    query.end_time = now_as_float()

    use_arrow_data = store_results and cast(bool, results_backend_use_msgpack)
    data, selected_columns, all_columns, expanded_columns = _serialize_and_expand_data(
        result_set, db_engine_spec, use_arrow_data, expand_data
    )

    # TODO: data should be saved separately from metadata (likely in Parquet)
    payload.update(
        {
            "status": QueryStatus.SUCCESS,
            "data": data,
            "columns": all_columns,
            "selected_columns": selected_columns,
            "expanded_columns": expanded_columns,
            "query": query.to_dict(),
        }
    )
    payload["query"]["state"] = QueryStatus.SUCCESS

    if store_results and results_backend:
        key = str(uuid.uuid4())
        logger.info(
            "Query %s: Storing results in results backend, key: %s", str(query_id), key
        )
        with stats_timing("sqllab.query.results_backend_write", stats_logger):
            with stats_timing(
                "sqllab.query.results_backend_write_serialization", stats_logger
            ):
                serialized_payload = _serialize_payload(
                    payload, cast(bool, results_backend_use_msgpack)
                )
            cache_timeout = database.cache_timeout
            if cache_timeout is None:
                cache_timeout = config["CACHE_DEFAULT_TIMEOUT"]

            compressed = zlib_compress(serialized_payload)
            logger.debug(
                "*** serialized payload size: %i", getsizeof(serialized_payload)
            )
            logger.debug("*** compressed payload size: %i", getsizeof(compressed))
            results_backend.set(key, compressed, cache_timeout)
        query.results_key = key

    query.status = QueryStatus.SUCCESS
    session.commit()

    if return_results:
        # since we're returning results we need to create non-arrow data
        if use_arrow_data:
            (
                data,
                selected_columns,
                all_columns,
                expanded_columns,
            ) = _serialize_and_expand_data(
                result_set, db_engine_spec, False, expand_data
            )
            payload.update(
                {
                    "data": data,
                    "columns": all_columns,
                    "selected_columns": selected_columns,
                    "expanded_columns": expanded_columns,
                }
            )
        return payload

    return None
Beispiel #24
0
def execute_sql_statement(
    sql_statement: str,
    query: Query,
    user_name: Optional[str],
    session: Session,
    cursor: Any,
    log_params: Optional[Dict[str, Any]],
    apply_ctas: bool = False,
) -> SupersetResultSet:
    """Executes a single SQL statement"""
    database = query.database
    db_engine_spec = database.db_engine_spec
    parsed_query = ParsedQuery(sql_statement)
    sql = parsed_query.stripped()
    # This is a test to see if the query is being
    # limited by either the dropdown or the sql.
    # We are testing to see if more rows exist than the limit.
    increased_limit = None if query.limit is None else query.limit + 1

    if not db_engine_spec.is_readonly_query(parsed_query) and not database.allow_dml:
        raise SupersetErrorException(
            SupersetError(
                message=__("Only SELECT statements are allowed against this database."),
                error_type=SupersetErrorType.DML_NOT_ALLOWED_ERROR,
                level=ErrorLevel.ERROR,
            )
        )
    if apply_ctas:
        if not query.tmp_table_name:
            start_dttm = datetime.fromtimestamp(query.start_time)
            query.tmp_table_name = "tmp_{}_table_{}".format(
                query.user_id, start_dttm.strftime("%Y_%m_%d_%H_%M_%S")
            )
        sql = parsed_query.as_create_table(
            query.tmp_table_name,
            schema_name=query.tmp_schema_name,
            method=query.ctas_method,
        )
        query.select_as_cta_used = True

    # Do not apply limit to the CTA queries when SQLLAB_CTAS_NO_LIMIT is set to true
    if db_engine_spec.is_select_query(parsed_query) and not (
        query.select_as_cta_used and SQLLAB_CTAS_NO_LIMIT
    ):
        if SQL_MAX_ROW and (not query.limit or query.limit > SQL_MAX_ROW):
            query.limit = SQL_MAX_ROW
        if query.limit:
            # We are fetching one more than the requested limit in order
            # to test whether there are more rows than the limit.
            # Later, the extra row will be dropped before sending
            # the results back to the user.
            sql = database.apply_limit_to_sql(sql, increased_limit, force=True)

    # Hook to allow environment-specific mutation (usually comments) to the SQL
    sql = SQL_QUERY_MUTATOR(sql, user_name, security_manager, database)
    try:
        query.executed_sql = sql
        if log_query:
            log_query(
                query.database.sqlalchemy_uri,
                query.executed_sql,
                query.schema,
                user_name,
                __name__,
                security_manager,
                log_params,
            )
        session.commit()
        with stats_timing("sqllab.query.time_executing_query", stats_logger):
            logger.debug("Query %d: Running query: %s", query.id, sql)
            db_engine_spec.execute(cursor, sql, async_=True)
            logger.debug("Query %d: Handling cursor", query.id)
            db_engine_spec.handle_cursor(cursor, query, session)

        with stats_timing("sqllab.query.time_fetching_results", stats_logger):
            logger.debug(
                "Query %d: Fetching data for query object: %s",
                query.id,
                str(query.to_dict()),
            )
            data = db_engine_spec.fetch_data(cursor, increased_limit)
            if query.limit is None or len(data) <= query.limit:
                query.limiting_factor = LimitingFactor.NOT_LIMITED
            else:
                # return 1 row less than increased_query
                data = data[:-1]
    except SoftTimeLimitExceeded as ex:
        logger.warning("Query %d: Time limit exceeded", query.id)
        logger.debug("Query %d: %s", query.id, ex)
        raise SupersetErrorException(
            SupersetError(
                message=__(
                    f"The query was killed after {SQLLAB_TIMEOUT} seconds. It might "
                    "be too complex, or the database might be under heavy load."
                ),
                error_type=SupersetErrorType.SQLLAB_TIMEOUT_ERROR,
                level=ErrorLevel.ERROR,
            )
        )
    except Exception as ex:
        logger.error("Query %d: %s", query.id, type(ex), exc_info=True)
        logger.debug("Query %d: %s", query.id, ex)
        raise SqlLabException(db_engine_spec.extract_error_message(ex))

    logger.debug("Query %d: Fetching cursor description", query.id)
    cursor_description = cursor.description
    return SupersetResultSet(data, cursor_description, db_engine_spec)
Beispiel #25
0
    def validate_parameters(
        cls,
        parameters: GSheetsParametersType,
    ) -> List[SupersetError]:
        errors: List[SupersetError] = []
        encrypted_credentials = parameters.get("service_account_info") or "{}"

        # On create the encrypted credentials are a string,
        # at all other times they are a dict
        if isinstance(encrypted_credentials, str):
            encrypted_credentials = json.loads(encrypted_credentials)

        table_catalog = parameters.get("catalog", {})

        if not table_catalog:
            # Allowing users to submit empty catalogs
            errors.append(
                SupersetError(
                    message="Sheet name is required",
                    error_type=SupersetErrorType.
                    CONNECTION_MISSING_PARAMETERS_ERROR,
                    level=ErrorLevel.WARNING,
                    extra={"catalog": {
                        "idx": 0,
                        "name": True
                    }},
                ), )
            return errors

        # We need a subject in case domain wide delegation is set, otherwise the
        # check will fail. This means that the admin will be able to add sheets
        # that only they have access, even if later users are not able to access
        # them.
        subject = g.user.email if g.user else None

        engine = create_engine(
            "gsheets://",
            service_account_info=encrypted_credentials,
            subject=subject,
        )
        conn = engine.connect()
        idx = 0
        for name, url in table_catalog.items():

            if not name:
                errors.append(
                    SupersetError(
                        message="Sheet name is required",
                        error_type=SupersetErrorType.
                        CONNECTION_MISSING_PARAMETERS_ERROR,
                        level=ErrorLevel.WARNING,
                        extra={"catalog": {
                            "idx": idx,
                            "name": True
                        }},
                    ), )
                return errors

            if not url:
                errors.append(
                    SupersetError(
                        message="URL is required",
                        error_type=SupersetErrorType.
                        CONNECTION_MISSING_PARAMETERS_ERROR,
                        level=ErrorLevel.WARNING,
                        extra={"catalog": {
                            "idx": idx,
                            "url": True
                        }},
                    ), )
                return errors

            try:
                results = conn.execute(f'SELECT * FROM "{url}" LIMIT 1')
                results.fetchall()
            except Exception:  # pylint: disable=broad-except
                errors.append(
                    SupersetError(
                        message=
                        ("The URL could not be identified. Please check for typos "
                         "and make sure that ‘Type of Google Sheets allowed’ "
                         "selection matches the input."),
                        error_type=SupersetErrorType.
                        TABLE_DOES_NOT_EXIST_ERROR,
                        level=ErrorLevel.WARNING,
                        extra={"catalog": {
                            "idx": idx,
                            "url": True
                        }},
                    ), )
            idx += 1
        return errors
Beispiel #26
0
    def test_extract_errors(self):
        """
        Test that custom error messages are extracted correctly.
        """
        msg = "mysql: Access denied for user 'test'@'testuser.com'"
        result = MySQLEngineSpec.extract_errors(Exception(msg))
        assert result == [
            SupersetError(
                error_type=SupersetErrorType.CONNECTION_ACCESS_DENIED_ERROR,
                message=
                'Either the username "test" or the password is incorrect.',
                level=ErrorLevel.ERROR,
                extra={
                    "invalid": ["username", "password"],
                    "engine_name":
                    "MySQL",
                    "issue_codes": [
                        {
                            "code":
                            1014,
                            "message":
                            "Issue 1014 - Either the"
                            " username or the password is wrong.",
                        },
                        {
                            "code":
                            1015,
                            "message":
                            "Issue 1015 - Either the database is "
                            "spelled incorrectly or does not exist.",
                        },
                    ],
                },
            )
        ]

        msg = "mysql: Unknown MySQL server host 'badhostname.com'"
        result = MySQLEngineSpec.extract_errors(Exception(msg))
        assert result == [
            SupersetError(
                error_type=SupersetErrorType.CONNECTION_INVALID_HOSTNAME_ERROR,
                message='Unknown MySQL server host "badhostname.com".',
                level=ErrorLevel.ERROR,
                extra={
                    "invalid": ["host"],
                    "engine_name":
                    "MySQL",
                    "issue_codes": [{
                        "code":
                        1007,
                        "message":
                        "Issue 1007 - The hostname"
                        " provided can't be resolved.",
                    }],
                },
            )
        ]

        msg = "mysql: Can't connect to MySQL server on 'badconnection.com'"
        result = MySQLEngineSpec.extract_errors(Exception(msg))
        assert result == [
            SupersetError(
                error_type=SupersetErrorType.CONNECTION_HOST_DOWN_ERROR,
                message='The host "badconnection.com" might be '
                "down and can't be reached.",
                level=ErrorLevel.ERROR,
                extra={
                    "invalid": ["host", "port"],
                    "engine_name":
                    "MySQL",
                    "issue_codes": [{
                        "code":
                        1007,
                        "message":
                        "Issue 1007 - The hostname provided"
                        " can't be resolved.",
                    }],
                },
            )
        ]

        msg = "mysql: Can't connect to MySQL server on '93.184.216.34'"
        result = MySQLEngineSpec.extract_errors(Exception(msg))
        assert result == [
            SupersetError(
                error_type=SupersetErrorType.CONNECTION_HOST_DOWN_ERROR,
                message=
                'The host "93.184.216.34" might be down and can\'t be reached.',
                level=ErrorLevel.ERROR,
                extra={
                    "invalid": ["host", "port"],
                    "engine_name":
                    "MySQL",
                    "issue_codes": [{
                        "code":
                        10007,
                        "message":
                        "Issue 1007 - The hostname provided "
                        "can't be resolved.",
                    }],
                },
            )
        ]

        msg = "mysql: Unknown database 'badDB'"
        result = MySQLEngineSpec.extract_errors(Exception(msg))
        print(result)
        assert result == [
            SupersetError(
                message='Unable to connect to database "badDB".',
                error_type=SupersetErrorType.CONNECTION_UNKNOWN_DATABASE_ERROR,
                level=ErrorLevel.ERROR,
                extra={
                    "invalid": ["database"],
                    "engine_name":
                    "MySQL",
                    "issue_codes": [{
                        "code":
                        1015,
                        "message":
                        "Issue 1015 - Either the database is spelled incorrectly or does not exist.",
                    }],
                },
            )
        ]
    def test_execute_sql_statements_ctas(self, mock_execute_sql_statement,
                                         mock_get_query):
        sql = """
            -- comment
            SET @value = 42;
            SELECT @value AS foo;
            -- comment
        """
        mock_session = mock.MagicMock()
        mock_query = mock.MagicMock()
        mock_query.database.allow_run_async = False
        mock_cursor = mock.MagicMock()
        mock_query.database.get_sqla_engine.return_value.raw_connection.return_value.cursor.return_value = (
            mock_cursor)
        mock_query.database.db_engine_spec.run_multiple_statements_as_one = False
        mock_get_query.return_value = mock_query

        # set the query to CTAS
        mock_query.select_as_cta = True
        mock_query.ctas_method = CtasMethod.TABLE

        execute_sql_statements(
            query_id=1,
            rendered_query=sql,
            return_results=True,
            store_results=False,
            session=mock_session,
            start_time=None,
            expand_data=False,
            log_params=None,
        )
        mock_execute_sql_statement.assert_has_calls([
            mock.call(
                "SET @value = 42",
                mock_query,
                mock_session,
                mock_cursor,
                None,
                False,
            ),
            mock.call(
                "SELECT @value AS foo",
                mock_query,
                mock_session,
                mock_cursor,
                None,
                True,  # apply_ctas
            ),
        ])

        # try invalid CTAS
        sql = "DROP TABLE my_table"
        with pytest.raises(SupersetErrorException) as excinfo:
            execute_sql_statements(
                query_id=1,
                rendered_query=sql,
                return_results=True,
                store_results=False,
                session=mock_session,
                start_time=None,
                expand_data=False,
                log_params=None,
            )
        assert excinfo.value.error == SupersetError(
            message=
            "CTAS (create table as select) can only be run with a query where the last statement is a SELECT. Please make sure your query has a SELECT as its last statement. Then, try running your query again.",
            error_type=SupersetErrorType.INVALID_CTAS_QUERY_ERROR,
            level=ErrorLevel.ERROR,
            extra={
                "issue_codes": [{
                    "code":
                    1023,
                    "message":
                    "Issue 1023 - The CTAS (create table as select) doesn't have a SELECT statement at the end. Please make sure your query has a SELECT as its last statement. Then, try running your query again.",
                }]
            },
        )

        # try invalid CVAS
        mock_query.ctas_method = CtasMethod.VIEW
        sql = """
            -- comment
            SET @value = 42;
            SELECT @value AS foo;
            -- comment
        """
        with pytest.raises(SupersetErrorException) as excinfo:
            execute_sql_statements(
                query_id=1,
                rendered_query=sql,
                return_results=True,
                store_results=False,
                session=mock_session,
                start_time=None,
                expand_data=False,
                log_params=None,
            )
        assert excinfo.value.error == SupersetError(
            message=
            "CVAS (create view as select) can only be run with a query with a single SELECT statement. Please make sure your query has only a SELECT statement. Then, try running your query again.",
            error_type=SupersetErrorType.INVALID_CVAS_QUERY_ERROR,
            level=ErrorLevel.ERROR,
            extra={
                "issue_codes": [
                    {
                        "code":
                        1024,
                        "message":
                        "Issue 1024 - CVAS (create view as select) query has more than one statement.",
                    },
                    {
                        "code":
                        1025,
                        "message":
                        "Issue 1025 - CVAS (create view as select) query is not a SELECT statement.",
                    },
                ]
            },
        )
Beispiel #28
0
    def test_extract_errors(self):
        msg = "Generic Error"
        result = PrestoEngineSpec.extract_errors(Exception(msg))
        assert result == [
            SupersetError(
                message="Generic Error",
                error_type=SupersetErrorType.GENERIC_DB_ENGINE_ERROR,
                level=ErrorLevel.ERROR,
                extra={
                    "engine_name":
                    "Presto",
                    "issue_codes": [{
                        "code":
                        1002,
                        "message":
                        "Issue 1002 - The database returned an unexpected error.",
                    }],
                },
            )
        ]

        msg = "line 1:8: Column 'bogus' cannot be resolved"
        result = PrestoEngineSpec.extract_errors(Exception(msg))
        assert result == [
            SupersetError(
                message=
                'We can\'t seem to resolve the column "bogus" at line 1:8.',
                error_type=SupersetErrorType.COLUMN_DOES_NOT_EXIST_ERROR,
                level=ErrorLevel.ERROR,
                extra={
                    "engine_name":
                    "Presto",
                    "issue_codes": [
                        {
                            "code":
                            1003,
                            "message":
                            "Issue 1003 - There is a syntax error in the SQL query. Perhaps there was a misspelling or a typo.",
                        },
                        {
                            "code":
                            1004,
                            "message":
                            "Issue 1004 - The column was deleted or renamed in the database.",
                        },
                    ],
                },
            )
        ]

        msg = "line 1:15: Table 'tpch.tiny.region2' does not exist"
        result = PrestoEngineSpec.extract_errors(Exception(msg))
        assert result == [
            SupersetError(
                message=
                "The table \"'tpch.tiny.region2'\" does not exist. A valid table must be used to run this query.",
                error_type=SupersetErrorType.TABLE_DOES_NOT_EXIST_ERROR,
                level=ErrorLevel.ERROR,
                extra={
                    "engine_name":
                    "Presto",
                    "issue_codes": [
                        {
                            "code":
                            1003,
                            "message":
                            "Issue 1003 - There is a syntax error in the SQL query. Perhaps there was a misspelling or a typo.",
                        },
                        {
                            "code":
                            1005,
                            "message":
                            "Issue 1005 - The table was deleted or renamed in the database.",
                        },
                    ],
                },
            )
        ]

        msg = "line 1:15: Schema 'tin' does not exist"
        result = PrestoEngineSpec.extract_errors(Exception(msg))
        assert result == [
            SupersetError(
                message=
                'The schema "tin" does not exist. A valid schema must be used to run this query.',
                error_type=SupersetErrorType.SCHEMA_DOES_NOT_EXIST_ERROR,
                level=ErrorLevel.ERROR,
                extra={
                    "engine_name":
                    "Presto",
                    "issue_codes": [
                        {
                            "code":
                            1003,
                            "message":
                            "Issue 1003 - There is a syntax error in the SQL query. Perhaps there was a misspelling or a typo.",
                        },
                        {
                            "code":
                            1016,
                            "message":
                            "Issue 1005 - The schema was deleted or renamed in the database.",
                        },
                    ],
                },
            )
        ]

        msg = b"Access Denied: Invalid credentials"
        result = PrestoEngineSpec.extract_errors(Exception(msg),
                                                 {"username": "******"})
        assert result == [
            SupersetError(
                message=
                'Either the username "alice" or the password is incorrect.',
                error_type=SupersetErrorType.CONNECTION_ACCESS_DENIED_ERROR,
                level=ErrorLevel.ERROR,
                extra={
                    "engine_name":
                    "Presto",
                    "issue_codes": [{
                        "code":
                        1014,
                        "message":
                        "Issue 1014 - Either the username or the password is wrong.",
                    }],
                },
            )
        ]

        msg = "Failed to establish a new connection: [Errno 8] nodename nor servname provided, or not known"
        result = PrestoEngineSpec.extract_errors(Exception(msg),
                                                 {"hostname": "badhost"})
        assert result == [
            SupersetError(
                message='The hostname "badhost" cannot be resolved.',
                error_type=SupersetErrorType.CONNECTION_INVALID_HOSTNAME_ERROR,
                level=ErrorLevel.ERROR,
                extra={
                    "engine_name":
                    "Presto",
                    "issue_codes": [{
                        "code":
                        1007,
                        "message":
                        "Issue 1007 - The hostname provided can't be resolved.",
                    }],
                },
            )
        ]

        msg = "Failed to establish a new connection: [Errno 60] Operation timed out"
        result = PrestoEngineSpec.extract_errors(Exception(msg), {
            "hostname": "badhost",
            "port": 12345
        })
        assert result == [
            SupersetError(
                message=
                'The host "badhost" might be down, and can\'t be reached on port 12345.',
                error_type=SupersetErrorType.CONNECTION_HOST_DOWN_ERROR,
                level=ErrorLevel.ERROR,
                extra={
                    "engine_name":
                    "Presto",
                    "issue_codes": [{
                        "code":
                        1009,
                        "message":
                        "Issue 1009 - The host might be down, and can't be reached on the provided port.",
                    }],
                },
            )
        ]

        msg = "Failed to establish a new connection: [Errno 61] Connection refused"
        result = PrestoEngineSpec.extract_errors(Exception(msg), {
            "hostname": "badhost",
            "port": 12345
        })
        assert result == [
            SupersetError(
                message=
                'Port 12345 on hostname "badhost" refused the connection.',
                error_type=SupersetErrorType.CONNECTION_PORT_CLOSED_ERROR,
                level=ErrorLevel.ERROR,
                extra={
                    "engine_name":
                    "Presto",
                    "issue_codes": [{
                        "code":
                        1008,
                        "message":
                        "Issue 1008 - The port is closed."
                    }],
                },
            )
        ]

        msg = "line 1:15: Catalog 'wrong' does not exist"
        result = PrestoEngineSpec.extract_errors(Exception(msg))
        assert result == [
            SupersetError(
                message='Unable to connect to catalog named "wrong".',
                error_type=SupersetErrorType.CONNECTION_UNKNOWN_DATABASE_ERROR,
                level=ErrorLevel.ERROR,
                extra={
                    "engine_name":
                    "Presto",
                    "issue_codes": [{
                        "code":
                        1015,
                        "message":
                        "Issue 1015 - Either the database is spelled incorrectly or does not exist.",
                    }],
                },
            )
        ]
Beispiel #29
0
    def test_extract_errors(self):
        """
        Test that custom error messages are extracted correctly.
        """
        msg = dedent("""
DB-Lib error message 20009, severity 9:
Unable to connect: Adaptive Server is unavailable or does not exist (locahost)
            """)
        result = MssqlEngineSpec.extract_errors(Exception(msg))
        assert result == [
            SupersetError(
                error_type=SupersetErrorType.CONNECTION_INVALID_HOSTNAME_ERROR,
                message='The hostname "locahost" cannot be resolved.',
                level=ErrorLevel.ERROR,
                extra={
                    "engine_name":
                    "Microsoft SQL",
                    "issue_codes": [{
                        "code":
                        1007,
                        "message":
                        "Issue 1007 - The hostname provided can't be resolved.",
                    }],
                },
            )
        ]

        msg = dedent("""
DB-Lib error message 20009, severity 9:
Unable to connect: Adaptive Server is unavailable or does not exist (localhost)
Net-Lib error during Connection refused (61)
DB-Lib error message 20009, severity 9:
Unable to connect: Adaptive Server is unavailable or does not exist (localhost)
Net-Lib error during Connection refused (61)
            """)
        result = MssqlEngineSpec.extract_errors(Exception(msg),
                                                context={
                                                    "port": 12345,
                                                    "hostname": "localhost"
                                                })
        assert result == [
            SupersetError(
                error_type=SupersetErrorType.CONNECTION_PORT_CLOSED_ERROR,
                message=
                'Port 12345 on hostname "localhost" refused the connection.',
                level=ErrorLevel.ERROR,
                extra={
                    "engine_name":
                    "Microsoft SQL",
                    "issue_codes": [{
                        "code":
                        1008,
                        "message":
                        "Issue 1008 - The port is closed."
                    }],
                },
            )
        ]

        msg = dedent("""
DB-Lib error message 20009, severity 9:
Unable to connect: Adaptive Server is unavailable or does not exist (example.com)
Net-Lib error during Operation timed out (60)
DB-Lib error message 20009, severity 9:
Unable to connect: Adaptive Server is unavailable or does not exist (example.com)
Net-Lib error during Operation timed out (60)
            """)
        result = MssqlEngineSpec.extract_errors(Exception(msg),
                                                context={
                                                    "port": 12345,
                                                    "hostname": "example.com"
                                                })
        assert result == [
            SupersetError(
                error_type=SupersetErrorType.CONNECTION_HOST_DOWN_ERROR,
                message=('The host "example.com" might be down, '
                         "and can't be reached on port 12345."),
                level=ErrorLevel.ERROR,
                extra={
                    "engine_name":
                    "Microsoft SQL",
                    "issue_codes": [{
                        "code":
                        1009,
                        "message":
                        "Issue 1009 - The host might be down, and can't be reached on the provided port.",
                    }],
                },
            )
        ]

        msg = dedent("""
DB-Lib error message 20009, severity 9:
Unable to connect: Adaptive Server is unavailable or does not exist (93.184.216.34)
Net-Lib error during Operation timed out (60)
DB-Lib error message 20009, severity 9:
Unable to connect: Adaptive Server is unavailable or does not exist (93.184.216.34)
Net-Lib error during Operation timed out (60)
            """)
        result = MssqlEngineSpec.extract_errors(Exception(msg),
                                                context={
                                                    "port": 12345,
                                                    "hostname": "93.184.216.34"
                                                })
        assert result == [
            SupersetError(
                error_type=SupersetErrorType.CONNECTION_HOST_DOWN_ERROR,
                message=('The host "93.184.216.34" might be down, '
                         "and can't be reached on port 12345."),
                level=ErrorLevel.ERROR,
                extra={
                    "engine_name":
                    "Microsoft SQL",
                    "issue_codes": [{
                        "code":
                        1009,
                        "message":
                        "Issue 1009 - The host might be down, and can't be reached on the provided port.",
                    }],
                },
            )
        ]

        msg = dedent("""
DB-Lib error message 20018, severity 14:
General SQL Server error: Check messages from the SQL Server
DB-Lib error message 20002, severity 9:
Adaptive Server connection failed (mssqldb.cxiotftzsypc.us-west-2.rds.amazonaws.com)
DB-Lib error message 20002, severity 9:
Adaptive Server connection failed (mssqldb.cxiotftzsypc.us-west-2.rds.amazonaws.com)
            """)
        result = MssqlEngineSpec.extract_errors(
            Exception(msg), context={"username": "******"})
        assert result == [
            SupersetError(
                message=
                'Either the username "testuser" or the password is incorrect.',
                error_type=SupersetErrorType.CONNECTION_ACCESS_DENIED_ERROR,
                level=ErrorLevel.ERROR,
                extra={
                    "engine_name":
                    "Microsoft SQL",
                    "issue_codes": [{
                        "code":
                        1014,
                        "message":
                        "Issue 1014 - Either the username or the password is wrong.",
                    }],
                },
            )
        ]
Beispiel #30
0
    def run(self) -> None:
        engine = self._properties["engine"]
        engine_specs = get_engine_specs()

        if engine in BYPASS_VALIDATION_ENGINES:
            # Skip engines that are only validated onCreate
            return

        if engine not in engine_specs:
            raise InvalidEngineError(
                SupersetError(
                    message=__(
                        'Engine "%(engine)s" is not a valid engine.',
                        engine=engine,
                    ),
                    error_type=SupersetErrorType.GENERIC_DB_ENGINE_ERROR,
                    level=ErrorLevel.ERROR,
                    extra={
                        "allowed": list(engine_specs),
                        "provided": engine
                    },
                ), )
        engine_spec = engine_specs[engine]
        if not issubclass(engine_spec, BasicParametersMixin):
            raise InvalidEngineError(
                SupersetError(
                    message=__(
                        'Engine "%(engine)s" cannot be configured through parameters.',
                        engine=engine,
                    ),
                    error_type=SupersetErrorType.GENERIC_DB_ENGINE_ERROR,
                    level=ErrorLevel.ERROR,
                    extra={
                        "allowed": [
                            name for name, engine_spec in engine_specs.items()
                            if issubclass(engine_spec, BasicParametersMixin)
                        ],
                        "provided":
                        engine,
                    },
                ), )

        # perform initial validation
        errors = engine_spec.validate_parameters(
            self._properties.get("parameters", {}))
        if errors:
            raise InvalidParametersError(errors)

        serialized_encrypted_extra = self._properties.get(
            "encrypted_extra", "{}")
        try:
            encrypted_extra = json.loads(serialized_encrypted_extra)
        except json.decoder.JSONDecodeError:
            encrypted_extra = {}

        # try to connect
        sqlalchemy_uri = engine_spec.build_sqlalchemy_uri(
            self._properties.get("parameters", None),  # type: ignore
            encrypted_extra,
        )
        if self._model and sqlalchemy_uri == self._model.safe_sqlalchemy_uri():
            sqlalchemy_uri = self._model.sqlalchemy_uri_decrypted
        database = DatabaseDAO.build_db_for_connection_test(
            server_cert=self._properties.get("server_cert", ""),
            extra=self._properties.get("extra", "{}"),
            impersonate_user=self._properties.get("impersonate_user", False),
            encrypted_extra=serialized_encrypted_extra,
        )
        database.set_sqlalchemy_uri(sqlalchemy_uri)
        database.db_engine_spec.mutate_db_for_connection_test(database)
        username = self._actor.username if self._actor is not None else None
        engine = database.get_sqla_engine(user_name=username)
        try:
            with closing(engine.raw_connection()) as conn:
                alive = engine.dialect.do_ping(conn)
        except Exception as ex:  # pylint: disable=broad-except
            url = make_url(sqlalchemy_uri)
            context = {
                "hostname": url.host,
                "password": url.password,
                "port": url.port,
                "username": url.username,
                "database": url.database,
            }
            errors = database.db_engine_spec.extract_errors(ex, context)
            raise DatabaseTestConnectionFailedError(errors)

        if not alive:
            raise DatabaseOfflineError(
                SupersetError(
                    message=__("Database is offline."),
                    error_type=SupersetErrorType.GENERIC_DB_ENGINE_ERROR,
                    level=ErrorLevel.ERROR,
                ), )