Skip to content

Commit d929327

Browse files
authored
fix: query args validation of /user/secrets GET (#343)
1 parent 60e6342 commit d929327

File tree

7 files changed

+60
-49
lines changed

7 files changed

+60
-49
lines changed

components/renku_data_services/base_api/error_handler.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
"""The error handler for the application."""
22

3+
import os
4+
import sys
5+
import traceback
36
from collections.abc import Mapping, Set
47
from sqlite3 import Error as SqliteError
58
from typing import Any, Optional, Protocol, TypeVar, Union
@@ -127,6 +130,12 @@ def default(self, request: Request, exception: Exception) -> HTTPResponse:
127130
message="The provided input is too large to be stored in the database"
128131
)
129132
self.log(request, formatted_exception)
133+
if formatted_exception.status_code == 500 and "PYTEST_CURRENT_TEST" in os.environ:
134+
# TODO: Figure out how to do logging properly in here, I could not get the sanic logs to show up from here
135+
# at all when running schemathesis. So 500 errors are hard to debug but print statements do show up.
136+
# The above log statement does not show up in the logs that pytest shows after a test is done.
137+
sys.stderr.write(f"A 500 error was raised because of {type(exception)} on request {request}\n")
138+
traceback.print_exception(exception)
130139
return json(
131140
self.api_spec.ErrorResponse(
132141
error=self.api_spec.Error(

components/renku_data_services/base_api/misc.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
from collections.abc import Awaitable, Callable, Coroutine
44
from dataclasses import dataclass
55
from functools import wraps
6-
from typing import Any, NoReturn, ParamSpec, TypeVar, cast
6+
from typing import Any, Concatenate, NoReturn, ParamSpec, TypeVar, cast
77

8+
from pydantic import BaseModel
89
from sanic import Request, json
910
from sanic.response import JSONResponse
11+
from sanic_ext import validate
1012

1113
from renku_data_services import errors
1214
from renku_data_services.base_api.blueprint import BlueprintFactoryResponse, CustomBlueprint
@@ -69,3 +71,29 @@ async def decorated_function(*args: _P.args, **kwargs: _P.kwargs) -> _T:
6971
return response
7072

7173
return decorated_function
74+
75+
76+
def validate_query(
77+
query: type[BaseModel],
78+
) -> Callable[
79+
[Callable[Concatenate[Request, _P], Awaitable[_T]]],
80+
Callable[Concatenate[Request, _P], Coroutine[Any, Any, _T]],
81+
]:
82+
"""Decorator for sanic query parameter validation.
83+
84+
Should be removed once sanic fixes this error in their validation code.
85+
"""
86+
87+
def decorator(
88+
f: Callable[Concatenate[Request, _P], Awaitable[_T]],
89+
) -> Callable[Concatenate[Request, _P], Coroutine[Any, Any, _T]]:
90+
@wraps(f)
91+
async def decorated_function(request: Request, *args: _P.args, **kwargs: _P.kwargs) -> _T:
92+
try:
93+
return await validate(query=query)(f)(request, *args, **kwargs)
94+
except KeyError:
95+
raise errors.ValidationError(message="Failed to validate the query parameters")
96+
97+
return decorated_function
98+
99+
return decorator

components/renku_data_services/migrations/alembic.ini

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -75,38 +75,3 @@ version_path_separator = os
7575
# black.type = console_scripts
7676
# black.entrypoint = black
7777
# black.options = -l 79 REVISION_SCRIPT_FILENAME
78-
79-
# Logging configuration
80-
[loggers]
81-
keys = root,sqlalchemy,alembic
82-
83-
[handlers]
84-
keys = console
85-
86-
[formatters]
87-
keys = generic
88-
89-
[logger_root]
90-
level = INFO
91-
handlers = console
92-
qualname =
93-
94-
[logger_sqlalchemy]
95-
level = WARN
96-
handlers =
97-
qualname = sqlalchemy.engine
98-
99-
[logger_alembic]
100-
level = INFO
101-
handlers =
102-
qualname = alembic
103-
104-
[handler_console]
105-
class = StreamHandler
106-
args = (sys.stderr,)
107-
level = NOTSET
108-
formatter = generic
109-
110-
[formatter_generic]
111-
format = %(levelname)-5.5s [%(name)s] %(message)s
112-
datefmt = %H:%M:%S

components/renku_data_services/migrations/env.py

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
"""Database migrations for Alembic."""
22

3-
from logging.config import fileConfig
4-
5-
from alembic import context
6-
from alembic.config import Config
3+
from logging.config import dictConfig
74

85
from renku_data_services.authz.orm import BaseORM as authz
96
from renku_data_services.connected_services.orm import BaseORM as connected_services
107
from renku_data_services.crc.orm import BaseORM as crc
118
from renku_data_services.message_queue.orm import BaseORM as events
12-
from renku_data_services.migrations.utils import run_migrations
9+
from renku_data_services.migrations.utils import logging_config, run_migrations
1310
from renku_data_services.namespace.orm import BaseORM as namespaces
1411
from renku_data_services.platform.orm import BaseORM as platform
1512
from renku_data_services.project.orm import BaseORM as project
@@ -19,14 +16,9 @@
1916
from renku_data_services.user_preferences.orm import BaseORM as user_preferences
2017
from renku_data_services.users.orm import BaseORM as users
2118

22-
# this is the Alembic Config object, which provides
23-
# access to the values within the .ini file in use.
24-
config: Config = context.config
25-
2619
# Interpret the config file for Python logging.
2720
# This line sets up loggers basically.
28-
if config.config_file_name is not None:
29-
fileConfig(config.config_file_name)
21+
dictConfig(logging_config)
3022

3123
all_metadata = [
3224
authz.metadata,

components/renku_data_services/migrations/utils.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,3 +191,19 @@ def __del__(self) -> None:
191191
self._thread.join()
192192
del self._loop
193193
del self._thread
194+
195+
196+
logging_config: dict = {
197+
"version": 1,
198+
"disable_existing_loggers": False,
199+
"loggers": {
200+
"alembic": {
201+
"level": "INFO",
202+
"qualname": "alembic",
203+
},
204+
"sqlalchemy": {
205+
"level": "WARN",
206+
"qualname": "sqlalchemy.engine",
207+
},
208+
},
209+
}

components/renku_data_services/users/blueprints.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import renku_data_services.base_models as base_models
1111
from renku_data_services.base_api.auth import authenticate, only_authenticated
1212
from renku_data_services.base_api.blueprint import BlueprintFactoryResponse, CustomBlueprint
13+
from renku_data_services.base_api.misc import validate_query
1314
from renku_data_services.base_models.validation import validated_json
1415
from renku_data_services.errors import errors
1516
from renku_data_services.secrets.db import UserSecretsRepo
@@ -160,7 +161,7 @@ def get_all(self) -> BlueprintFactoryResponse:
160161

161162
@authenticate(self.authenticator)
162163
@only_authenticated
163-
@validate(query=GetSecretsParams)
164+
@validate_query(query=GetSecretsParams)
164165
async def _get_all(request: Request, user: base_models.APIUser, query: GetSecretsParams) -> JSONResponse:
165166
secret_kind = SecretKind[query.kind.value]
166167
secrets = await self.secret_repo.get_user_secrets(requested_by=user, kind=secret_kind)

test/bases/renku_data_services/data_api/test_schemathesis.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def requests_statistics():
2121
stats = sorted(stats)
2222
p95 = stats[math.floor(0.95 * len(stats))]
2323

24-
assert p95 < timedelta(milliseconds=100)
24+
assert p95 < timedelta(milliseconds=100), f"The p95 response time {p95} was >= 100 ms"
2525

2626

2727
@pytest_asyncio.fixture

0 commit comments

Comments
 (0)