Skip to content

Commit cf1c90c

Browse files
authored
♻️ Maintenance: Enhanced logs on sms service errors and tests image labels (#4456)
1 parent 86567d7 commit cf1c90c

File tree

6 files changed

+122
-25
lines changed

6 files changed

+122
-25
lines changed

.github/ISSUE_TEMPLATE/4_pre_release.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ body:
106106
- Fill up
107107
value: |
108108
- [ ] `` make release-staging name=<sprint_name> version=<version> git_sha=<commit_sha>``
109+
- `https://github.com/ITISFoundation/osparc-simcore/releases/new?prerelease=1&target=<commit_sha>&tag=staging_<sprint_name><version>&title=Staging%20<sprint_name><version>`
109110
- [ ] Draft [pre-release](https://github.com/ITISFoundation/osparc-simcore/releases)
110111
- [ ] Announce
111112
```json

packages/models-library/tests/test_utils_service_io.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@
66

77
import itertools
88
import json
9+
import re
910
import sys
11+
from collections.abc import Iterable
12+
from contextlib import suppress
1013
from pathlib import Path
11-
from typing import Union
1214

1315
import pytest
16+
from models_library.basic_regex import VERSION_RE
1417
from models_library.services import ServiceInput, ServiceOutput, ServicePortKey
1518
from models_library.utils.json_schema import jsonschema_validate_schema
1619
from models_library.utils.services_io import get_service_io_json_schema
@@ -25,7 +28,7 @@
2528

2629

2730
@pytest.fixture(params=example_inputs_labels + example_outputs_labels)
28-
def service_port(request: pytest.FixtureRequest) -> Union[ServiceInput, ServiceOutput]:
31+
def service_port(request: pytest.FixtureRequest) -> ServiceInput | ServiceOutput:
2932
try:
3033
index = example_inputs_labels.index(request.param)
3134
example = ServiceInput.Config.schema_extra["examples"][index]
@@ -36,7 +39,7 @@ def service_port(request: pytest.FixtureRequest) -> Union[ServiceInput, ServiceO
3639
return ServiceOutput.parse_obj(example)
3740

3841

39-
def test_get_schema_from_port(service_port: Union[ServiceInput, ServiceOutput]):
42+
def test_get_schema_from_port(service_port: ServiceInput | ServiceOutput):
4043
print(service_port.json(indent=2))
4144

4245
# get
@@ -55,7 +58,7 @@ def test_get_schema_from_port(service_port: Union[ServiceInput, ServiceOutput]):
5558
TEST_DATA_FOLDER = CURRENT_DIR / "data"
5659

5760

58-
@pytest.mark.diagnostics
61+
@pytest.mark.diagnostics()
5962
@pytest.mark.parametrize(
6063
"metadata_path",
6164
TEST_DATA_FOLDER.rglob("metadata*.json"),
@@ -82,3 +85,37 @@ def test_against_service_metadata_configs(metadata_path: Path):
8285
assert schema
8386
# check valid jsons-schema
8487
jsonschema_validate_schema(schema)
88+
89+
90+
assert VERSION_RE[0] == "^"
91+
assert VERSION_RE[-1] == "$"
92+
_VERSION_SEARCH_RE = re.compile(VERSION_RE[1:-1]) # without $ and ^
93+
94+
95+
def _iter_main_services() -> Iterable[Path]:
96+
"""NOTE: Filters the main service when there is a group
97+
of services behind a node.
98+
"""
99+
for p in TEST_DATA_FOLDER.rglob("metadata-*.json"):
100+
with suppress(Exception):
101+
meta = json.loads(p.read_text())
102+
if (meta.get("type") == "computational") or meta.get(
103+
"service.container-http-entrypoint"
104+
):
105+
yield p
106+
107+
108+
@pytest.mark.diagnostics()
109+
@pytest.mark.parametrize(
110+
"metadata_path",
111+
(p for p in _iter_main_services() if "latest" not in p.name),
112+
ids=lambda p: f"{p.parent.name}/{p.name}",
113+
)
114+
def test_service_metadata_has_same_version_as_tag(metadata_path: Path):
115+
meta = json.loads(metadata_path.read_text())
116+
117+
# metadata-M.m.b.json
118+
match = _VERSION_SEARCH_RE.search(metadata_path.name)
119+
assert match, f"tag {metadata_path.name} is not a version"
120+
version_in_tag = match.group()
121+
assert meta["version"] == version_in_tag

services/web/server/src/simcore_service_webserver/login/_2fa.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,10 +169,11 @@ async def send_email_code(
169169
#
170170

171171
_FROM, _TO = 3, -1
172+
_MIN_NUM_DIGITS = 5
172173

173174

174-
def mask_phone_number(phn: str) -> str:
175-
assert len(phn) > 5 # nosec
175+
def mask_phone_number(phone: str) -> str:
176+
assert len(phone) > _MIN_NUM_DIGITS # nosec
176177
# SEE https://github.com/pydantic/pydantic/issues/1551
177178
# SEE https://en.wikipedia.org/wiki/E.164
178-
return phn[:_FROM] + len(phn[_FROM:_TO]) * "X" + phn[_TO:]
179+
return phone[:_FROM] + len(phone[_FROM:_TO]) * "X" + phone[_TO:]

services/web/server/src/simcore_service_webserver/login/_constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
from typing import Final
22

33
MSG_2FA_CODE_SENT: Final[str] = "Code sent by SMS to {phone_number}"
4+
MSG_2FA_UNAVAILABLE_OEC: Final[
5+
str
6+
] = "Currently we cannot use 2FA, please try again later ({error_code})"
47
MSG_ACTIVATED: Final[str] = "Your account is activated"
58
MSG_ACTIVATION_REQUIRED: Final[
69
str

services/web/server/src/simcore_service_webserver/login/handlers_auth.py

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from pydantic import BaseModel, Field, PositiveInt, SecretStr
88
from servicelib.aiohttp.requests_validation import parse_request_body_as
99
from servicelib.error_codes import create_error_code
10-
from servicelib.logging_utils import get_log_record_extra, log_context
10+
from servicelib.logging_utils import LogExtra, get_log_record_extra, log_context
1111
from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON
1212
from servicelib.request_keys import RQT_USERID_KEY
1313
from simcore_postgres_database.models.users import UserRole
@@ -33,6 +33,7 @@
3333
MAX_2FA_CODE_RESEND,
3434
MAX_2FA_CODE_TRIALS,
3535
MSG_2FA_CODE_SENT,
36+
MSG_2FA_UNAVAILABLE_OEC,
3637
MSG_LOGGED_OUT,
3738
MSG_PHONE_MISSING,
3839
MSG_UNAUTHORIZED_LOGIN_2FA,
@@ -120,13 +121,11 @@ async def login(request: web.Request):
120121
# Some roles have login privileges
121122
has_privileges: Final[bool] = UserRole.USER < UserRole(user["role"])
122123
if has_privileges or not settings.LOGIN_2FA_REQUIRED:
123-
response = await login_granted_response(request, user=user)
124-
return response
124+
return await login_granted_response(request, user=user)
125125

126126
# no phone
127127
if not user["phone"]:
128-
129-
response = envelope_response(
128+
return envelope_response(
130129
# LoginNextPage
131130
{
132131
"name": CODE_PHONE_NUMBER_REQUIRED,
@@ -140,7 +139,6 @@ async def login(request: web.Request):
140139
},
141140
status=web.HTTPAccepted.status_code,
142141
)
143-
return response
144142

145143
# create 2FA
146144
assert user["phone"] # nosec
@@ -163,7 +161,7 @@ async def login(request: web.Request):
163161
user_name=user["name"],
164162
)
165163

166-
response = envelope_response(
164+
return envelope_response(
167165
# LoginNextPage
168166
{
169167
"name": CODE_2FA_CODE_REQUIRED,
@@ -181,19 +179,20 @@ async def login(request: web.Request):
181179
},
182180
status=web.HTTPAccepted.status_code,
183181
)
184-
return response
185182

186-
except Exception as e:
187-
error_code = create_error_code(e)
183+
except Exception as exc:
184+
error_code = create_error_code(exc)
185+
more_extra: LogExtra = get_log_record_extra(user_id=user.get("id")) or {}
188186
log.exception(
189-
"Unexpectedly failed while setting up 2FA code and sending SMS[%s]",
187+
"Failed while setting up 2FA code and sending SMS to %s [%s]",
188+
mask_phone_number(user.get("phone", "Unknown")),
190189
f"{error_code}",
191-
extra={"error_code": error_code},
190+
extra={"error_code": error_code, **more_extra},
192191
)
193192
raise web.HTTPServiceUnavailable(
194-
reason=f"Currently we cannot use 2FA, please try again later ({error_code})",
193+
reason=MSG_2FA_UNAVAILABLE_OEC.format(error_code=error_code),
195194
content_type=MIMETYPE_APPLICATION_JSON,
196-
) from e
195+
) from exc
197196

198197

199198
class LoginTwoFactorAuthBody(InputSchema):
@@ -239,8 +238,7 @@ async def login_2fa(request: web.Request):
239238
# dispose since code was used
240239
await delete_2fa_code(request.app, login_2fa_.email)
241240

242-
response = await login_granted_response(request, user=user)
243-
return response
241+
return await login_granted_response(request, user=user)
244242

245243

246244
class LogoutBody(InputSchema):

services/web/server/tests/unit/with_dbs/03/login/test_login_2fa.py

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# pylint: disable=unused-variable
44

55
import asyncio
6+
import logging
67
from contextlib import AsyncExitStack
78
from unittest.mock import Mock
89

@@ -12,6 +13,7 @@
1213
from aiohttp.test_utils import TestClient, make_mocked_request
1314
from faker import Faker
1415
from pytest import CaptureFixture, MonkeyPatch
16+
from pytest_mock import MockerFixture
1517
from pytest_simcore.helpers.utils_assert import assert_status
1618
from pytest_simcore.helpers.utils_envs import EnvVarsDict, setenvs_from_dict
1719
from pytest_simcore.helpers.utils_login import NewUser, parse_link, parse_test_marks
@@ -27,8 +29,10 @@
2729
get_redis_validation_code_client,
2830
send_email_code,
2931
)
32+
from simcore_service_webserver.login._constants import MSG_2FA_UNAVAILABLE_OEC
3033
from simcore_service_webserver.login.storage import AsyncpgStorage
3134
from simcore_service_webserver.products.plugin import get_current_product
35+
from twilio.base.exceptions import TwilioRestException
3236

3337

3438
@pytest.fixture
@@ -65,7 +69,7 @@ def postgres_db(postgres_db: sa.engine.Engine):
6569

6670

6771
@pytest.fixture
68-
def mocked_twilio_service(mocker) -> dict[str, Mock]:
72+
def mocked_twilio_service(mocker: MockerFixture) -> dict[str, Mock]:
6973
return {
7074
"send_sms_code_for_registration": mocker.patch(
7175
"simcore_service_webserver.login.handlers_registration.send_sms_code",
@@ -101,7 +105,7 @@ async def test_2fa_code_operations(client: TestClient):
101105
assert await get_2fa_code(client.app, email) is None
102106

103107

104-
@pytest.mark.acceptance_test
108+
@pytest.mark.acceptance_test()
105109
async def test_workflow_register_and_login_with_2fa(
106110
client: TestClient,
107111
db: AsyncpgStorage,
@@ -322,3 +326,56 @@ async def test_send_email_code(
322326
assert parsed_context["code"] == f"{code}"
323327
assert parsed_context["name"] == user_name.capitalize()
324328
assert parsed_context["support_email"] == support_email
329+
330+
331+
async def test_2fa_sms_failure_during_login(
332+
client: TestClient,
333+
fake_user_email: str,
334+
fake_user_password: str,
335+
fake_user_phone_number: str,
336+
caplog: pytest.LogCaptureFixture,
337+
mocker: MockerFixture,
338+
):
339+
assert client.app
340+
341+
# Mocks error in graylog https://monitoring.osparc.io/graylog/search/649e7619ce6e0838a96e9bf1?q=%222FA%22&rangetype=relative&from=172800
342+
mocker.patch(
343+
"simcore_service_webserver.login.handlers_auth.send_sms_code",
344+
autospec=True,
345+
side_effect=TwilioRestException(
346+
status=400,
347+
uri="https://www.twilio.com/doc",
348+
msg="Unable to create record: A 'From' phone number is required",
349+
),
350+
)
351+
352+
# A registered user ...
353+
async with NewUser(
354+
params={
355+
"email": fake_user_email,
356+
"password": fake_user_password,
357+
"phone": fake_user_phone_number,
358+
},
359+
app=client.app,
360+
):
361+
# ... logs in, but fails to send SMS !
362+
with caplog.at_level(logging.ERROR):
363+
url = client.app.router["auth_login"].url_for()
364+
response = await client.post(
365+
f"{url}",
366+
json={
367+
"email": fake_user_email,
368+
"password": fake_user_password,
369+
},
370+
)
371+
372+
# Expects failure:
373+
# HTTPServiceUnavailable: Currently we cannot use 2FA, please try again later (OEC:140558738809344)
374+
data, error = await assert_status(response, web.HTTPServiceUnavailable)
375+
assert not data
376+
assert error["errors"][0]["message"].startswith(
377+
MSG_2FA_UNAVAILABLE_OEC[:10]
378+
)
379+
380+
# Expects logs like 'Failed while setting up 2FA code and sending SMS to 157XXXXXXXX3 [OEC:140392495277888]'
381+
assert f"{fake_user_phone_number[:3]}" in caplog.text

0 commit comments

Comments
 (0)