Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(bigquery): Custom message when Service Account doesnt have the correct Roles and Permissions #21838

Merged
merged 5 commits into from
Oct 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 5 additions & 1 deletion superset/databases/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,12 @@
from superset.databases.utils import get_table_metadata
from superset.db_engine_specs import get_available_engine_specs
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
from superset.exceptions import SupersetErrorsException
from superset.extensions import security_manager
from superset.models.core import Database
from superset.superset_typing import FlaskResponse
from superset.utils.core import error_msg_from_exception, parse_js_uri_path_item
from superset.views.base import json_errors_response
from superset.views.base_api import (
BaseSupersetModelRestApi,
requires_form_data,
Expand Down Expand Up @@ -221,7 +223,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
log_to_statsd=False,
)
@requires_json
def post(self) -> Response:
def post(self) -> FlaskResponse:
"""Creates a new Database
---
post:
Expand Down Expand Up @@ -278,6 +280,8 @@ def post(self) -> Response:
return self.response_422(message=ex.normalized_messages())
except DatabaseConnectionFailedError as ex:
return self.response_422(message=str(ex))
except SupersetErrorsException as ex:
return json_errors_response(errors=ex.errors, status=ex.status)
except DatabaseCreateFailedError as ex:
logger.error(
"Error creating model %s: %s",
Expand Down
8 changes: 8 additions & 0 deletions superset/databases/commands/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
)
from superset.databases.commands.test_connection import TestConnectionDatabaseCommand
from superset.databases.dao import DatabaseDAO
from superset.exceptions import SupersetErrorsException
from superset.extensions import db, event_logger, security_manager

logger = logging.getLogger(__name__)
Expand All @@ -46,6 +47,13 @@ def run(self) -> Model:
try:
# Test connection before starting create transaction
TestConnectionDatabaseCommand(self._properties).run()
except SupersetErrorsException as ex:
event_logger.log_with_context(
action=f"db_creation_failed.{ex.__class__.__name__}",
engine=self._properties.get("sqlalchemy_uri", "").split(":")[0],
)
# So we can show the original message
raise ex
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Original message -> Custom message after extracting the error.
Otherwise the DatabaseConnectionFailedError would have the default Connection failed ... message caught from below.

except Exception as ex:
event_logger.log_with_context(
action=f"db_creation_failed.{ex.__class__.__name__}",
Expand Down
17 changes: 12 additions & 5 deletions superset/databases/commands/test_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,16 @@
from superset.databases.commands.exceptions import (
DatabaseSecurityUnsafeError,
DatabaseTestConnectionDriverError,
DatabaseTestConnectionFailedError,
DatabaseTestConnectionUnexpectedError,
)
from superset.databases.dao import DatabaseDAO
from superset.databases.utils import make_url_safe
from superset.errors import ErrorLevel, SupersetErrorType
from superset.exceptions import SupersetSecurityException, SupersetTimeoutException
from superset.exceptions import (
SupersetErrorsException,
SupersetSecurityException,
SupersetTimeoutException,
)
from superset.extensions import event_logger
from superset.models.core import Database

Expand All @@ -49,6 +52,7 @@ def __init__(self, data: Dict[str, Any]):

def run(self) -> None: # pylint: disable=too-many-statements
self.validate()
ex_str = ""
uri = self._properties.get("sqlalchemy_uri", "")
if self._model and uri == self._model.safe_sqlalchemy_uri():
uri = self._model.sqlalchemy_uri_decrypted
Expand Down Expand Up @@ -117,10 +121,13 @@ def ping(engine: Engine) -> bool:
level=ErrorLevel.ERROR,
extra={"sqlalchemy_uri": database.sqlalchemy_uri},
) from ex
except Exception: # pylint: disable=broad-except
except Exception as ex: # pylint: disable=broad-except
alive = False
# So we stop losing the original message if any
ex_str = str(ex)

if not alive:
raise DBAPIError(None, None, None)
raise DBAPIError(ex_str or None, None, None)

# Log succesful connection test with engine
event_logger.log_with_context(
Expand All @@ -145,7 +152,7 @@ def ping(engine: Engine) -> bool:
)
# check for custom errors (wrong username, wrong password, etc)
errors = database.db_engine_spec.extract_errors(ex, context)
raise DatabaseTestConnectionFailedError(errors) from ex
raise SupersetErrorsException(errors) from ex
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Antonio-RiveroMartnez I'm looking into some 500 errors for test connections, and I saw this change. What would happen if I were to change this back to raise the DatabaseTestConnectionFailedError which is a 422? Would it break anything from this PR? I checked through the implementation and it looks like it should still catch the SupersetErrorsExceptions because it is the base class, but I wanted to see if I might be missing anything here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @eschutho, the problem of doing that is that we would lost the original message and instead the message from DatabaseTestConnectionFailedError would be used (Connection failed, please check your connection settings) and not the custom message with roles after parsing the original message.

except SupersetSecurityException as ex:
event_logger.log_with_context(
action=f"test_connection_error.{ex.__class__.__name__}",
Expand Down
13 changes: 8 additions & 5 deletions superset/db_engine_specs/bigquery.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@


CONNECTION_DATABASE_PERMISSIONS_REGEX = re.compile(
"Access Denied: Project User does not have bigquery.jobs.create "
+ "permission in project (?P<project>.+?)"
"Access Denied: Project (?P<project_name>.+?): User does not have "
+ "bigquery.jobs.create permission in project (?P<project>.+?)"
)

TABLE_DOES_NOT_EXIST_REGEX = re.compile(
Expand Down Expand Up @@ -154,9 +154,12 @@ class BigQueryEngineSpec(BaseEngineSpec):
custom_errors: Dict[Pattern[str], Tuple[str, SupersetErrorType, Dict[str, Any]]] = {
CONNECTION_DATABASE_PERMISSIONS_REGEX: (
__(
"We were unable to connect to your database. Please "
"confirm that your service account has the Viewer "
"and Job User roles on the project."
"Unable to connect. Verify that the following roles are set "
'on the service account: "BigQuery Data Viewer", '
'"BigQuery Metadata Viewer", "BigQuery Job User" '
"and the following permissions are set "
'"bigquery.readsessions.create", '
'"bigquery.readsessions.getData"'
),
SupersetErrorType.CONNECTION_DATABASE_PERMISSIONS_ERROR,
{},
Expand Down
54 changes: 49 additions & 5 deletions tests/integration_tests/databases/api_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -525,11 +525,55 @@ def test_create_database_conn_fail(self):
self.login(username="admin")
response = self.client.post(uri, json=database_data)
response_data = json.loads(response.data.decode("utf-8"))
expected_response = {
"message": "Connection failed, please check your connection settings"
superset_error_mysql = SupersetError(
message='Either the username "superset" or the password is incorrect.',
error_type="CONNECTION_ACCESS_DENIED_ERROR",
level="error",
extra={
"engine_name": "MySQL",
"invalid": ["username", "password"],
"issue_codes": [
{
"code": 1014,
"message": (
"Issue 1014 - Either the username or the password is wrong."
),
},
{
"code": 1015,
"message": (
"Issue 1015 - Issue 1015 - Either the database is spelled incorrectly or does not exist."
),
},
],
},
)
superset_error_postgres = SupersetError(
message='The password provided for username "superset" is incorrect.',
error_type="CONNECTION_INVALID_PASSWORD_ERROR",
level="error",
extra={
"engine_name": "PostgreSQL",
"invalid": ["username", "password"],
"issue_codes": [
{
"code": 1013,
"message": (
"Issue 1013 - The password provided when connecting to a database is not valid."
),
}
],
},
)
expected_response_mysql = {"errors": [dataclasses.asdict(superset_error_mysql)]}
expected_response_postgres = {
"errors": [dataclasses.asdict(superset_error_postgres)]
}
self.assertEqual(response.status_code, 422)
self.assertEqual(response_data, expected_response)
self.assertEqual(response.status_code, 500)
if example_db.backend == "mysql":
self.assertEqual(response_data, expected_response_mysql)
else:
self.assertEqual(response_data, expected_response_postgres)

def test_update_database(self):
"""
Expand Down Expand Up @@ -1516,7 +1560,7 @@ def test_test_connection_failed_invalid_hostname(
url = "api/v1/database/test_connection/"
rv = self.post_assert_metric(url, data, "test_connection")

assert rv.status_code == 422
assert rv.status_code == 500
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)]}
Expand Down
5 changes: 2 additions & 3 deletions tests/integration_tests/databases/commands_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
DatabaseNotFoundError,
DatabaseSecurityUnsafeError,
DatabaseTestConnectionDriverError,
DatabaseTestConnectionFailedError,
DatabaseTestConnectionUnexpectedError,
)
from superset.databases.commands.export import ExportDatabasesCommand
Expand Down Expand Up @@ -683,7 +682,7 @@ def test_connection_do_ping_exception(
json_payload = {"sqlalchemy_uri": db_uri}
command_without_db_name = TestConnectionDatabaseCommand(json_payload)

with pytest.raises(DatabaseTestConnectionFailedError) as excinfo:
with pytest.raises(SupersetErrorsException) as excinfo:
command_without_db_name.run()
assert (
excinfo.value.errors[0].error_type
Expand Down Expand Up @@ -757,7 +756,7 @@ def test_connection_db_api_exc(
json_payload = {"sqlalchemy_uri": db_uri}
command_without_db_name = TestConnectionDatabaseCommand(json_payload)

with pytest.raises(DatabaseTestConnectionFailedError) as excinfo:
with pytest.raises(SupersetErrorsException) as excinfo:
command_without_db_name.run()
assert str(excinfo.value) == (
"Connection failed, please check your connection settings"
Expand Down
4 changes: 2 additions & 2 deletions tests/integration_tests/db_engine_specs/bigquery_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,11 +246,11 @@ def test_df_to_sql(self, mock_get_engine):
)

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"
msg = "403 POST https://bigquery.googleapis.com/bigquery/v2/projects/test-keel-310804/jobs?prettyPrint=false: Access Denied: Project profound-keel-310804: 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.",
message='Unable to connect. Verify that the following roles are set on the service account: "BigQuery Data Viewer", "BigQuery Metadata Viewer", "BigQuery Job User" and the following permissions are set "bigquery.readsessions.create", "bigquery.readsessions.getData"',
error_type=SupersetErrorType.CONNECTION_DATABASE_PERMISSIONS_ERROR,
level=ErrorLevel.ERROR,
extra={
Expand Down