Skip to content

Commit 897f1e8

Browse files
authored
fix: Better invalid account error (#331)
1 parent 4a77a9d commit 897f1e8

File tree

14 files changed

+302
-23
lines changed

14 files changed

+302
-23
lines changed

src/firebolt/async_db/util.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
from firebolt.client.auth import Auth
66
from firebolt.client.client import AsyncClientV2
77
from firebolt.common.settings import DEFAULT_TIMEOUT_SECONDS
8-
from firebolt.utils.exception import AccountNotFoundError, InterfaceError
8+
from firebolt.utils.exception import (
9+
AccountNotFoundOrNoAccessError,
10+
InterfaceError,
11+
)
912
from firebolt.utils.urls import GATEWAY_HOST_BY_ACCOUNT_NAME
1013

1114
ENGINE_STATUS_RUNNING = "Running"
@@ -26,7 +29,7 @@ async def _get_system_engine_url(
2629
url = GATEWAY_HOST_BY_ACCOUNT_NAME.format(account_name=account_name)
2730
response = await client.get(url=url)
2831
if response.status_code == codes.NOT_FOUND:
29-
raise AccountNotFoundError(account_name)
32+
raise AccountNotFoundOrNoAccessError(account_name)
3033
if response.status_code != codes.OK:
3134
raise InterfaceError(
3235
f"Unable to retrieve system engine endpoint {url}: "

src/firebolt/client/client.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
)
1919
from firebolt.utils.exception import (
2020
AccountNotFoundError,
21+
AccountNotFoundOrNoAccessError,
2122
FireboltEngineError,
2223
InterfaceError,
2324
)
@@ -142,7 +143,7 @@ def account_id(self) -> str:
142143
)
143144
if response.status_code == HttpxCodes.NOT_FOUND:
144145
assert self.account_name is not None
145-
raise AccountNotFoundError(self.account_name)
146+
raise AccountNotFoundOrNoAccessError(self.account_name)
146147
# process all other status codes
147148
response.raise_for_status()
148149
return response.json()["id"]
@@ -324,7 +325,7 @@ async def account_id(self) -> str:
324325
)
325326
if response.status_code == HttpxCodes.NOT_FOUND:
326327
assert self.account_name is not None
327-
raise AccountNotFoundError(self.account_name)
328+
raise AccountNotFoundOrNoAccessError(self.account_name)
328329
# process all other status codes
329330
response.raise_for_status()
330331
account_id = response.json()["id"]

src/firebolt/db/util.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
from firebolt.client import ClientV2
66
from firebolt.client.auth import Auth
77
from firebolt.common.settings import DEFAULT_TIMEOUT_SECONDS
8-
from firebolt.utils.exception import AccountNotFoundError, InterfaceError
8+
from firebolt.utils.exception import (
9+
AccountNotFoundOrNoAccessError,
10+
InterfaceError,
11+
)
912
from firebolt.utils.urls import GATEWAY_HOST_BY_ACCOUNT_NAME
1013

1114
ENGINE_STATUS_RUNNING = "Running"
@@ -26,7 +29,7 @@ def _get_system_engine_url(
2629
url = GATEWAY_HOST_BY_ACCOUNT_NAME.format(account_name=account_name)
2730
response = client.get(url=url)
2831
if response.status_code == codes.NOT_FOUND:
29-
raise AccountNotFoundError(account_name)
32+
raise AccountNotFoundOrNoAccessError(account_name)
3033
if response.status_code != codes.OK:
3134
raise InterfaceError(
3235
f"Unable to retrieve system engine endpoint {url}: "

src/firebolt/utils/exception.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,27 @@ def __init__(self, account_name: str):
7575
self.account_name = account_name
7676

7777

78+
class AccountNotFoundOrNoAccessError(FireboltError):
79+
"""Account with provided name doesn't exist.
80+
81+
Args:
82+
account_name (str): Name of account that wasn't found
83+
84+
Attributes:
85+
account_name (str): Name of account that wasn't found
86+
"""
87+
88+
def __init__(self, account_name: str):
89+
super().__init__(
90+
f"Account '{account_name}' does not exist "
91+
"in this organization or is not authorized. "
92+
"Please verify the account name and make sure your "
93+
"service account has the correct RBAC permissions and "
94+
"is linked to a user."
95+
)
96+
self.account_name = account_name
97+
98+
7899
class AttachedEngineInUseError(FireboltDatabaseError):
79100
"""Engine unavailable because it's starting/stopping.
80101

tests/integration/conftest.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ def account_name() -> str:
9494
return must_env(ACCOUNT_NAME_ENV)
9595

9696

97+
@fixture(scope="session")
98+
def invalid_account_name(account_name: str) -> str:
99+
return f"{account_name}--"
100+
101+
97102
@fixture(scope="session")
98103
def api_endpoint() -> str:
99104
return must_env(API_ENDPOINT_ENV)

tests/integration/dbapi/async/V2/conftest.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1+
from random import randint
2+
from typing import Tuple
3+
14
from pytest import fixture
25

36
from firebolt.async_db import Connection, connect
47
from firebolt.client.auth.base import Auth
8+
from firebolt.client.auth.client_credentials import ClientCredentials
9+
from tests.integration.conftest import Secret
510

611

712
@fixture
@@ -66,3 +71,38 @@ async def connection_system_engine_no_db(
6671
api_endpoint=api_endpoint,
6772
) as connection:
6873
yield connection
74+
75+
76+
@fixture
77+
async def service_account_no_user(
78+
connection_system_engine_no_db: Connection,
79+
database_name: str,
80+
) -> Tuple[str, Secret]:
81+
# function-level fixture so we need to make sa name is unique
82+
sa_account_name = f"{database_name}_sa_no_user_{randint(0, 1000)}"
83+
with connection_system_engine_no_db.cursor() as cursor:
84+
await cursor.execute(
85+
f'CREATE SERVICE ACCOUNT "{sa_account_name}" '
86+
'WITH DESCRIPTION = "Ecosytem test with no user"'
87+
)
88+
await cursor.execute(f"CALL fb_GENERATESERVICEACCOUNTKEY('{sa_account_name}')")
89+
# service_account_name, service_account_id, secret
90+
_, s_id, key = await cursor.fetchone()
91+
# Currently this is bugged so retrieve id via a query. FIR-28719
92+
if not s_id:
93+
await cursor.execute(
94+
"SELECT service_account_id FROM information_schema.service_accounts "
95+
f"WHERE service_account_name='{sa_account_name}'"
96+
)
97+
s_id = (await cursor.fetchone())[0]
98+
# Wrap in secret to avoid leaking the key in the logs
99+
yield s_id, Secret(key)
100+
await cursor.execute(f"DROP SERVICE ACCOUNT {sa_account_name}")
101+
102+
103+
@fixture
104+
async def auth_no_user(
105+
service_account_no_user: Tuple[str, Secret],
106+
) -> ClientCredentials:
107+
s_id, s_secret = service_account_no_user
108+
return ClientCredentials(s_id, s_secret.value)

tests/integration/dbapi/async/V2/test_errors_async.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from firebolt.async_db import Connection, connect
44
from firebolt.client.auth import ClientCredentials
55
from firebolt.utils.exception import (
6-
AccountNotFoundError,
6+
AccountNotFoundOrNoAccessError,
77
EngineNotRunningError,
88
FireboltEngineError,
99
InterfaceError,
@@ -13,25 +13,45 @@
1313

1414
async def test_invalid_account(
1515
database_name: str,
16-
engine_name: str,
16+
invalid_account_name: str,
1717
auth: ClientCredentials,
1818
api_endpoint: str,
1919
) -> None:
2020
"""Connection properly reacts to invalid account error."""
21-
account_name = "--"
22-
with raises(AccountNotFoundError) as exc_info:
21+
with raises(AccountNotFoundOrNoAccessError) as exc_info:
2322
async with await connect(
2423
database=database_name,
25-
engine_name=engine_name,
2624
auth=auth,
25+
account_name=invalid_account_name,
26+
api_endpoint=api_endpoint,
27+
) as connection:
28+
await connection.cursor().execute("show tables")
29+
30+
assert str(exc_info.value).startswith(
31+
f"Account '{invalid_account_name}' does not exist"
32+
), "Invalid account error message."
33+
34+
35+
async def test_account_no_user(
36+
database_name: str,
37+
account_name: str,
38+
auth_no_user: ClientCredentials,
39+
api_endpoint: str,
40+
) -> None:
41+
"""Connection properly reacts to account that doesn't have
42+
a user attached to it."""
43+
with raises(AccountNotFoundOrNoAccessError) as exc_info:
44+
async with await connect(
45+
database=database_name,
46+
auth=auth_no_user,
2747
account_name=account_name,
2848
api_endpoint=api_endpoint,
2949
) as connection:
3050
await connection.cursor().execute("show tables")
3151

32-
assert str(exc_info.value).startswith(
33-
f'Account "{account_name}" does not exist'
34-
), "Invalid account error message."
52+
assert str(exc_info.value).startswith(
53+
f"Account '{account_name}' does not exist"
54+
), "Invalid account error message."
3555

3656

3757
async def test_engine_name_not_exists(

tests/integration/dbapi/async/V2/test_queries_async.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,7 @@ async def test_bytea_roundtrip(
415415
), "Invalid bytea data returned after roundtrip"
416416

417417

418-
@fixture
418+
@fixture(scope="session")
419419
async def setup_db(connection_system_engine_no_db: Connection, use_db_name: str):
420420
use_db_name = use_db_name + "_async"
421421
with connection_system_engine_no_db.cursor() as cursor:

tests/integration/dbapi/sync/V2/conftest.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1+
from random import randint
2+
from typing import Tuple
3+
14
from pytest import fixture
25

36
from firebolt.client.auth.base import Auth
7+
from firebolt.client.auth.client_credentials import ClientCredentials
48
from firebolt.db import Connection, connect
9+
from tests.integration.conftest import Secret
510

611

712
@fixture
@@ -66,3 +71,38 @@ def connection_system_engine_no_db(
6671
api_endpoint=api_endpoint,
6772
) as connection:
6873
yield connection
74+
75+
76+
@fixture
77+
def service_account_no_user(
78+
connection_system_engine_no_db: Connection,
79+
database_name: str,
80+
) -> Tuple[str, Secret]:
81+
# function-level fixture so we need to make sa name is unique
82+
sa_account_name = f"{database_name}_sa_no_user_{randint(0, 1000)}"
83+
with connection_system_engine_no_db.cursor() as cursor:
84+
cursor.execute(
85+
f'CREATE SERVICE ACCOUNT "{sa_account_name}" '
86+
'WITH DESCRIPTION = "Ecosytem test with no user"'
87+
)
88+
cursor.execute(f"CALL fb_GENERATESERVICEACCOUNTKEY('{sa_account_name}')")
89+
# service_account_name, service_account_id, secret
90+
_, s_id, key = cursor.fetchone()
91+
# Currently this is bugged so retrieve id via a query. FIR-28719
92+
if not s_id:
93+
cursor.execute(
94+
"SELECT service_account_id FROM information_schema.service_accounts "
95+
f"WHERE service_account_name='{sa_account_name}'"
96+
)
97+
s_id = cursor.fetchone()[0]
98+
# Wrap in secret to avoid leaking the key in the logs
99+
yield s_id, Secret(key)
100+
cursor.execute(f"DROP SERVICE ACCOUNT {sa_account_name}")
101+
102+
103+
@fixture
104+
def auth_no_user(
105+
service_account_no_user: Tuple[str, Secret],
106+
) -> ClientCredentials:
107+
s_id, s_secret = service_account_no_user
108+
return ClientCredentials(s_id, s_secret.value)

tests/integration/dbapi/sync/V2/test_errors.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from firebolt.client.auth import ClientCredentials
44
from firebolt.db import Connection, connect
55
from firebolt.utils.exception import (
6-
AccountNotFoundError,
6+
AccountNotFoundOrNoAccessError,
77
EngineNotRunningError,
88
FireboltDatabaseError,
99
FireboltEngineError,
@@ -13,25 +13,44 @@
1313

1414
def test_invalid_account(
1515
database_name: str,
16-
engine_name: str,
16+
invalid_account_name: str,
1717
auth: ClientCredentials,
1818
api_endpoint: str,
1919
) -> None:
2020
"""Connection properly reacts to invalid account error."""
21-
account_name = "--"
22-
with raises(AccountNotFoundError) as exc_info:
21+
with raises(AccountNotFoundOrNoAccessError) as exc_info:
2322
with connect(
2423
database=database_name,
25-
engine_name=engine_name, # Omit engine_url to force account_id lookup.
2624
auth=auth,
25+
account_name=invalid_account_name,
26+
api_endpoint=api_endpoint,
27+
) as connection:
28+
connection.cursor().execute("show tables")
29+
30+
assert str(exc_info.value).startswith(
31+
f"Account '{invalid_account_name}' does not exist"
32+
), "Invalid account error message."
33+
34+
35+
def test_account_no_user(
36+
database_name: str,
37+
account_name: str,
38+
auth_no_user: ClientCredentials,
39+
api_endpoint: str,
40+
) -> None:
41+
"""Connection properly reacts to invalid account error."""
42+
with raises(AccountNotFoundOrNoAccessError) as exc_info:
43+
with connect(
44+
database=database_name,
45+
auth=auth_no_user,
2746
account_name=account_name,
2847
api_endpoint=api_endpoint,
2948
) as connection:
3049
connection.cursor().execute("show tables")
3150

32-
assert str(exc_info.value).startswith(
33-
f'Account "{account_name}" does not exist'
34-
), "Invalid account error message."
51+
assert str(exc_info.value).startswith(
52+
f"Account '{account_name}' does not exist"
53+
), "Invalid account error message."
3554

3655

3756
def test_engine_name_not_exists(

0 commit comments

Comments
 (0)