Skip to content

Commit f6784f4

Browse files
committed
Added token validation
1 parent 54098b5 commit f6784f4

File tree

6 files changed

+112
-50
lines changed

6 files changed

+112
-50
lines changed

src/superannotate/lib/app/interface/base_interface.py

Lines changed: 40 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from lib.core import CONFIG
1616
from lib.core import setup_logging
1717
from lib.core.entities.base import ConfigEntity
18+
from lib.core.entities.base import TokenStr
1819
from lib.core.exceptions import AppException
1920
from lib.infrastructure.controller import Controller
2021
from lib.infrastructure.utils import extract_project_folder
@@ -26,39 +27,41 @@
2627
class BaseInterfaceFacade:
2728
REGISTRY = []
2829

29-
def __init__(self, token: str = None, config_path: str = None):
30-
if token:
31-
config = ConfigEntity(SA_TOKEN=token)
32-
elif config_path:
33-
config_path = Path(config_path)
34-
if not Path(config_path).is_file() or not os.access(config_path, os.R_OK):
35-
raise AppException(
36-
f"SuperAnnotate config file {str(config_path)} not found."
37-
)
38-
try:
30+
@validate_arguments
31+
def __init__(self, token: TokenStr = None, config_path: str = None):
32+
try:
33+
if token:
34+
config = ConfigEntity(SA_TOKEN=token)
35+
elif config_path:
36+
config_path = Path(config_path)
37+
if not Path(config_path).is_file() or not os.access(config_path, os.R_OK):
38+
raise AppException(
39+
f"SuperAnnotate config file {str(config_path)} not found."
40+
)
3941
if config_path.suffix == ".json":
4042
config = self._retrieve_configs_from_json(config_path)
4143
else:
4244
config = self._retrieve_configs_from_ini(config_path)
43-
except pydantic.ValidationError as e:
44-
raise AppException(wrap_error(e))
45-
except KeyError:
46-
raise
47-
else:
48-
config = self._retrieve_configs_from_env()
49-
if not config:
50-
if Path(constants.CONFIG_INI_FILE_LOCATION).exists():
51-
config = self._retrieve_configs_from_ini(
52-
constants.CONFIG_INI_FILE_LOCATION
53-
)
54-
elif Path(constants.CONFIG_JSON_FILE_LOCATION).exists():
55-
config = self._retrieve_configs_from_json(
56-
constants.CONFIG_JSON_FILE_LOCATION
57-
)
58-
else:
59-
raise AppException(
60-
f"SuperAnnotate config file {constants.CONFIG_INI_FILE_LOCATION} not found."
61-
)
45+
46+
else:
47+
config = self._retrieve_configs_from_env()
48+
if not config:
49+
if Path(constants.CONFIG_INI_FILE_LOCATION).exists():
50+
config = self._retrieve_configs_from_ini(
51+
constants.CONFIG_INI_FILE_LOCATION
52+
)
53+
elif Path(constants.CONFIG_JSON_FILE_LOCATION).exists():
54+
config = self._retrieve_configs_from_json(
55+
constants.CONFIG_JSON_FILE_LOCATION
56+
)
57+
else:
58+
raise AppException(
59+
f"SuperAnnotate config file {constants.CONFIG_INI_FILE_LOCATION} not found."
60+
)
61+
except pydantic.ValidationError as e:
62+
raise AppException(wrap_error(e))
63+
except KeyError:
64+
raise
6265
if not config:
6366
raise AppException("Credentials not provided.")
6467
setup_logging(config.LOGGING_LEVEL, config.LOGGING_PATH)
@@ -70,7 +73,13 @@ def _retrieve_configs_from_json(path: Path) -> typing.Union[ConfigEntity]:
7073
with open(path) as json_file:
7174
json_data = json.load(json_file)
7275
token = json_data["token"]
73-
config = ConfigEntity(SA_TOKEN=token)
76+
try:
77+
config = ConfigEntity(SA_TOKEN=token)
78+
except pydantic.ValidationError:
79+
raise pydantic.ValidationError(
80+
[pydantic.error_wrappers.ErrorWrapper(ValueError("Invalid token."), loc='token')],
81+
model=ConfigEntity
82+
)
7483
host = json_data.get("main_endpoint")
7584
verify_ssl = json_data.get("ssl_verify")
7685
if host:
@@ -89,10 +98,7 @@ def _retrieve_configs_from_ini(path: Path) -> typing.Union[ConfigEntity]:
8998
config_data = {}
9099
for key in config_parser["DEFAULT"]:
91100
config_data[key.upper()] = config_parser["DEFAULT"][key]
92-
try:
93-
return ConfigEntity(**config_data)
94-
except pydantic.ValidationError as e:
95-
raise AppException(wrap_error(e))
101+
return ConfigEntity(**config_data)
96102

97103
@staticmethod
98104
def _retrieve_configs_from_env() -> typing.Union[ConfigEntity, None]:

src/superannotate/lib/app/interface/types.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,4 @@ def wrapped(self, *args, **kwargs):
5151
return pydantic_validate_arguments(func)(self, *args, **kwargs)
5252
except ValidationError as e:
5353
raise AppException(wrap_error(e)) from e
54-
5554
return wrapped

src/superannotate/lib/core/entities/base.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import re
12
import warnings
23
from datetime import datetime
34
from enum import Enum
@@ -14,6 +15,7 @@
1415
from lib.core.enums import BaseTitledEnum
1516
from pydantic import BaseModel as PydanticBaseModel
1617
from pydantic import Extra
18+
from pydantic import StrictStr
1719
from pydantic import Field
1820
from pydantic.datetime_parse import parse_datetime
1921
from pydantic.typing import is_namedtuple
@@ -292,8 +294,21 @@ def map_fields(entity: dict) -> dict:
292294
return entity
293295

294296

297+
class TokenStr(StrictStr):
298+
regex = r'^[-.@_A-Za-z0-9]+=\d+$'
299+
300+
@classmethod
301+
def validate(cls, value: Union[str]) -> Union[str]:
302+
if cls.curtail_length and len(value) > cls.curtail_length:
303+
value = value[: cls.curtail_length]
304+
if cls.regex:
305+
if not re.match(cls.regex, value):
306+
raise ValueError("Invalid token.")
307+
return value
308+
309+
295310
class ConfigEntity(BaseModel):
296-
API_TOKEN: str = Field(alias="SA_TOKEN")
311+
API_TOKEN: TokenStr = Field(alias="SA_TOKEN")
297312
API_URL: str = Field(alias="SA_URL", default=BACKEND_URL)
298313
LOGGING_LEVEL: Literal[
299314
"NOTSET", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"

src/superannotate/lib/infrastructure/services/http_client.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -183,27 +183,30 @@ def serialize_response(
183183
"status": response.status_code,
184184
}
185185
try:
186-
data_json = response.json()
187186
if not response.ok:
188187
if response.status_code in (502, 504):
189188
data[
190189
"_error"
191190
] = "Our service is currently unavailable, please try again later."
191+
return content_type(**data)
192192
else:
193+
data_json = response.json()
193194
data["_error"] = data_json.get(
194195
"error", data_json.get("errors", "Unknown Error")
195196
)
196-
else:
197-
if dispatcher:
198-
if dispatcher in data_json:
199-
data["data"] = data_json.pop(dispatcher)
200-
else:
201-
data["data"] = data_json
202-
data_json = {}
203-
data.update(data_json)
197+
return content_type(**data)
198+
data_json = response.json()
199+
if dispatcher:
200+
if dispatcher in data_json:
201+
data["data"] = data_json.pop(dispatcher)
204202
else:
205203
data["data"] = data_json
204+
data_json = {}
205+
data.update(data_json)
206+
else:
207+
data["data"] = data_json
206208
return content_type(**data)
207209
except json.decoder.JSONDecodeError:
210+
data['_error'] = response.content
208211
data["reason"] = response.reason
209212
return content_type(**data)

tests/integration/annotations/test_upload_annotations_from_folder_to_project.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
sa = SAClient()
99

1010

11-
class TestAnnotationUploadVector(BaseTestCase):
11+
class \
12+
TestAnnotationUploadVector(BaseTestCase):
1213
PROJECT_NAME = "Test-Upload_annotations_from_folder_to_project"
1314
PROJECT_DESCRIPTION = "Desc"
1415
PROJECT_TYPE = "Vector"

tests/unit/test_init.py

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from unittest.mock import patch
77

88
import superannotate.lib.core as constants
9+
from superannotate.lib.app.interface.types import validate_arguments
910
from superannotate import AppException
1011
from superannotate import SAClient
1112

@@ -15,10 +16,7 @@ class ClientInitTestCase(TestCase):
1516

1617
def test_init_via_invalid_token(self):
1718
_token = "123"
18-
with self.assertRaisesRegexp(
19-
AppException,
20-
"Unable to retrieve team data. Please verify your credentials.",
21-
):
19+
with self.assertRaisesRegexp(AppException, r"(\s+)token(\s+)Invalid token."):
2220
SAClient(token=_token)
2321

2422
@patch("lib.core.usecases.GetTeamUseCase")
@@ -49,6 +47,20 @@ def test_init_via_config_json(self, get_team_use_case):
4947
self._token.split("=")[-1]
5048
)
5149

50+
def test_init_via_config_json_invalid_json(self):
51+
with tempfile.TemporaryDirectory() as config_dir:
52+
constants.HOME_PATH = config_dir
53+
config_ini_path = f"{config_dir}/config.ini"
54+
config_json_path = f"{config_dir}/config.json"
55+
with patch("lib.core.CONFIG_INI_FILE_LOCATION", config_ini_path), patch(
56+
"lib.core.CONFIG_JSON_FILE_LOCATION", config_json_path
57+
):
58+
with open(f"{config_dir}/config.json", "w") as config_json:
59+
json.dump({"token": "INVALID_TOKEN"}, config_json)
60+
for kwargs in ({}, {"config_path": f"{config_dir}/config.json"}):
61+
with self.assertRaisesRegexp(AppException, r"(\s+)token(\s+)Invalid token."):
62+
SAClient(**kwargs)
63+
5264
@patch("lib.core.usecases.GetTeamUseCase")
5365
def test_init_via_config_ini(self, get_team_use_case):
5466
with tempfile.TemporaryDirectory() as config_dir:
@@ -83,6 +95,32 @@ def test_init_env(self, get_team_use_case):
8395
assert sa.controller._config.API_URL == "SOME_URL"
8496
assert get_team_use_case.call_args_list[0].kwargs["team_id"] == 123
8597

98+
@patch.dict(os.environ, {"SA_URL": "SOME_URL", "SA_TOKEN": "SOME_TOKEN"})
99+
def test_init_env_invalid_token(self):
100+
with self.assertRaisesRegexp(AppException, r"(\s+)SA_TOKEN(\s+)Invalid token."):
101+
SAClient()
102+
103+
def test_init_via_config_ini_invalid_token(self):
104+
with tempfile.TemporaryDirectory() as config_dir:
105+
constants.HOME_PATH = config_dir
106+
config_ini_path = f"{config_dir}/config.ini"
107+
config_json_path = f"{config_dir}/config.json"
108+
with patch("lib.core.CONFIG_INI_FILE_LOCATION", config_ini_path), patch(
109+
"lib.core.CONFIG_JSON_FILE_LOCATION", config_json_path
110+
):
111+
with open(f"{config_dir}/config.ini", "w") as config_ini:
112+
config_parser = ConfigParser()
113+
config_parser.optionxform = str
114+
config_parser["DEFAULT"] = {
115+
"SA_TOKEN": "INVALID_TOKEN",
116+
"LOGGING_LEVEL": "DEBUG",
117+
}
118+
config_parser.write(config_ini)
119+
120+
for kwargs in ({}, {"config_path": f"{config_dir}/config.ini"}):
121+
with self.assertRaisesRegexp(AppException, r"(\s+)SA_TOKEN(\s+)Invalid token."):
122+
SAClient(**kwargs)
123+
86124
def test_invalid_config_path(self):
87125
_path = "something"
88126
with self.assertRaisesRegexp(

0 commit comments

Comments
 (0)