From f27b68e350b20a350b8deffbee6d9520ba596fe4 Mon Sep 17 00:00:00 2001 From: "Maxime Alves LIRMM@home" Date: Mon, 4 Oct 2021 20:12:43 +0200 Subject: [PATCH] [0.5.11] --- halfapi/__init__.py | 2 +- halfapi/app.py | 36 ++++++++++++++++++++++++++--------- halfapi/cli/domain.py | 13 +++++++++---- halfapi/conf.py | 2 +- halfapi/lib/jwt_middleware.py | 22 ++++++++++++++++----- halfapi/lib/responses.py | 7 +++++++ halfapi/logging.py | 31 ++++++++++++++++++++++++++++++ scripts/whoami.sh | 1 + tests/test_debug_routes.py | 5 +++-- 9 files changed, 97 insertions(+), 22 deletions(-) create mode 100644 halfapi/logging.py create mode 100644 scripts/whoami.sh diff --git a/halfapi/__init__.py b/halfapi/__init__.py index aa84553..5b52fb5 100644 --- a/halfapi/__init__.py +++ b/halfapi/__init__.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -__version__ = '0.5.10' +__version__ = '0.5.11' def version(): return f'HalfAPI version:{__version__}' diff --git a/halfapi/app.py b/halfapi/app.py index be6deef..89a96fa 100644 --- a/halfapi/app.py +++ b/halfapi/app.py @@ -17,6 +17,7 @@ from starlette.applications import Starlette from starlette.authentication import UnauthenticatedUser from starlette.middleware import Middleware from starlette.routing import Route +from starlette.responses import Response, PlainTextResponse from starlette.middleware.authentication import AuthenticationMiddleware from timing_asgi import TimingMiddleware @@ -34,12 +35,15 @@ from halfapi.lib.responses import (ORJSONResponse, UnauthorizedResponse, from halfapi.lib.routes import gen_starlette_routes, debug_routes from halfapi.lib.schemas import get_api_routes, get_api_domain_routes, schema_json, get_acls +from halfapi.logging import logger, config_logging +from halfapi import __version__ -logger = logging.getLogger('uvicorn.asgi') class HalfAPI: def __init__(self, config=None): + config_logging(logging.DEBUG) + if config: SECRET = config.get('SECRET') PRODUCTION = config.get('PRODUCTION') @@ -54,16 +58,10 @@ class HalfAPI: routes = [ Route('/', get_api_routes(DOMAINS)) ] - routes += [ - Route('/halfapi/schema', schema_json), - Route('/halfapi/acls', get_acls), - ] - routes += Route('/halfapi/current_user', lambda request, *args, **kwargs: - ORJSONResponse({'user':request.user.json}) - if SECRET and not isinstance(request.user, UnauthenticatedUser) - else ORJSONResponse({'user': None})), + for route in self.routes(): + routes.append(route) if not PRODUCTION: for route in debug_routes(): @@ -100,6 +98,7 @@ class HalfAPI: ) if SECRET: + self.SECRET = SECRET self.application.add_middleware( AuthenticationMiddleware, backend=JWTAuthenticationBackend(secret_key=SECRET) @@ -115,4 +114,23 @@ class HalfAPI: logger.info('CONFIG:\n%s', CONFIG) + @property + def version(self): + return __version__ + + async def version_async(self, request, *args, **kwargs): + return Response(self.version) + + def routes(self): + """ Halfapi default routes + """ + + async def get_user(request, *args, **kwargs): + return ORJSONResponse({'user':request.user}) + + yield Route('/halfapi/whoami', get_user) + yield Route('/halfapi/schema', schema_json) + yield Route('/halfapi/acls', get_acls) + yield Route('/halfapi/version', self.version_async) + application = HalfAPI().application diff --git a/halfapi/cli/domain.py b/halfapi/cli/domain.py index 7a7e9d6..1b2754a 100644 --- a/halfapi/cli/domain.py +++ b/halfapi/cli/domain.py @@ -13,6 +13,7 @@ from .cli import cli from ..conf import config, write_config, DOMAINSDICT from ..lib.schemas import schema_dict_dom +from ..lib.routes import api_routes logger = logging.getLogger('halfapi') @@ -43,11 +44,15 @@ def list_routes(domain, m_dom): Echoes the list of the **m_dom** active routes """ - click.echo(f'\nDomain : {domain}') + click.echo(f'\nDomain : {domain}\n') + routes = api_routes(m_dom)[0] + if len(routes): + for key, item in routes.items(): + methods = '|'.join(list(item.keys())) + click.echo(f'\t{key} : {methods}') + else: + click.echo(f'\t**No ROUTES**') - for key, item in schema_dict_dom({domain: m_dom}).get('paths', {}).items(): - methods = '|'.join(list(item.keys())) - click.echo(f'{key} : {methods}') def list_api_routes(): diff --git a/halfapi/conf.py b/halfapi/conf.py index 30471e6..a2a496f 100644 --- a/halfapi/conf.py +++ b/halfapi/conf.py @@ -42,8 +42,8 @@ from configparser import ConfigParser import importlib from .lib.domain import d_domains +from .logging import logger -logger = logging.getLogger('uvicorn.asgi') PROJECT_NAME = environ.get('HALFAPI_PROJECT_NAME') or os.path.basename(os.getcwd()) DOMAINSDICT = lambda: {} diff --git a/halfapi/lib/jwt_middleware.py b/halfapi/lib/jwt_middleware.py index ae3d107..b0b9284 100644 --- a/halfapi/lib/jwt_middleware.py +++ b/halfapi/lib/jwt_middleware.py @@ -22,15 +22,27 @@ from starlette.authentication import ( from starlette.requests import HTTPConnection from starlette.exceptions import HTTPException -logger = logging.getLogger('halfapi') +logger = logging.getLogger('uvicorn.error') +SECRET=None try: from ..conf import SECRET except ImportError as exc: logger.error('Could not import SECRET variable from conf module,'\ ' using HALFAPI_SECRET environment variable') - raise Exception('Missing secret') from exc +class Nobody(UnauthenticatedUser): + """ Nobody class + + The default class when no token is passed + """ + @property + def json(self): + return { + 'id' : '', + 'token': '', + 'payload': '' + } class JWTUser(BaseUser): @@ -119,7 +131,7 @@ class JWTAuthenticationBackend(AuthenticationBackend): PRODUCTION = conn.scope['app'].debug == False if not token and not is_check_call: - return AuthCredentials(), UnauthenticatedUser() + return AuthCredentials(), Nobody() try: if token and not is_fake_user_id: @@ -142,7 +154,7 @@ class JWTAuthenticationBackend(AuthenticationBackend): if token: return AuthCredentials(), CheckUser(payload['user_id']) else: - return AuthCredentials(), UnauthenticatedUser() + return AuthCredentials(), Nobody() if PRODUCTION and 'debug' in payload.keys() and payload['debug']: @@ -177,7 +189,7 @@ class JWTWebSocketAuthenticationBackend(AuthenticationBackend): ) -> typing.Optional[typing.Tuple["AuthCredentials", "BaseUser"]]: if self.query_param_name not in conn.query_params: - return AuthCredentials(), UnauthenticatedUser() + return AuthCredentials(), Nobody() token = conn.query_params[self.query_param_name] diff --git a/halfapi/lib/responses.py b/halfapi/lib/responses.py index 7dbde9b..2f79bea 100644 --- a/halfapi/lib/responses.py +++ b/halfapi/lib/responses.py @@ -21,6 +21,8 @@ import orjson # asgi framework from starlette.responses import PlainTextResponse, Response, JSONResponse +from .jwt_middleware import JWTUser, Nobody + __all__ = [ 'HJSONResponse', @@ -82,11 +84,16 @@ class ORJSONResponse(JSONResponse): list_types = { set } + jsonable_types = { + JWTUser, Nobody + } if type(typ) in str_types: return str(typ) if type(typ) in list_types: return list(typ) + if type(typ) in jsonable_types: + return typ.json raise TypeError(f'Type {type(typ)} is not handled by ORJSONResponse') diff --git a/halfapi/logging.py b/halfapi/logging.py new file mode 100644 index 0000000..057099d --- /dev/null +++ b/halfapi/logging.py @@ -0,0 +1,31 @@ +import logging + + +def config_logging(level=logging.INFO): + + # When run by 'uvicorn ...', a root handler is already + # configured and the basicConfig below does nothing. + # To get the desired formatting: + logging.getLogger().handlers.clear() + + # 'uvicorn --log-config' is broken so we configure in the app. + # https://github.com/encode/uvicorn/issues/511 + logging.basicConfig( + # match gunicorn format + format='%(asctime)s [%(process)d] [%(levelname)s] %(message)s', + datefmt='[%Y-%m-%d %H:%M:%S %z]', + level=level) + + # When run by 'gunicorn -k uvicorn.workers.UvicornWorker ...', + # These loggers are already configured and propogating. + # So we have double logging with a root logger. + # (And setting propagate = False hurts the other usage.) + logging.getLogger('uvicorn.asgi').handlers.clear() + logging.getLogger('uvicorn.access').handlers.clear() + logging.getLogger('uvicorn.error').handlers.clear() + logging.getLogger('uvicorn.asgi').propagate = True + logging.getLogger('uvicorn.access').propagate = True + logging.getLogger('uvicorn.error').propagate = True + +config_logging() +logger = logging.getLogger('uvicorn.asgi') diff --git a/scripts/whoami.sh b/scripts/whoami.sh new file mode 100644 index 0000000..740138b --- /dev/null +++ b/scripts/whoami.sh @@ -0,0 +1 @@ +http 127.0.0.1:3000/halfapi/whoami Authorization:$(http 127.0.0.1:3000/authentication/check email=malves password=papa|jq -r '.token') diff --git a/tests/test_debug_routes.py b/tests/test_debug_routes.py index 0870717..f96f4a7 100644 --- a/tests/test_debug_routes.py +++ b/tests/test_debug_routes.py @@ -5,9 +5,10 @@ from starlette.testclient import TestClient import json -def test_current_user(project_runner, application_debug): +def test_whoami(project_runner, application_debug): + # @TODO : test with fake login c = TestClient(application_debug) - r = c.get('/halfapi/current_user') + r = c.get('/halfapi/whoami') assert r.status_code == 200 def test_log(application_debug):