This commit is contained in:
Maxime Alves LIRMM@home 2021-10-04 20:12:43 +02:00
parent f3c12f516e
commit f27b68e350
9 changed files with 97 additions and 22 deletions

View File

@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
__version__ = '0.5.10' __version__ = '0.5.11'
def version(): def version():
return f'HalfAPI version:{__version__}' return f'HalfAPI version:{__version__}'

View File

@ -17,6 +17,7 @@ from starlette.applications import Starlette
from starlette.authentication import UnauthenticatedUser from starlette.authentication import UnauthenticatedUser
from starlette.middleware import Middleware from starlette.middleware import Middleware
from starlette.routing import Route from starlette.routing import Route
from starlette.responses import Response, PlainTextResponse
from starlette.middleware.authentication import AuthenticationMiddleware from starlette.middleware.authentication import AuthenticationMiddleware
from timing_asgi import TimingMiddleware 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.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.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: class HalfAPI:
def __init__(self, config=None): def __init__(self, config=None):
config_logging(logging.DEBUG)
if config: if config:
SECRET = config.get('SECRET') SECRET = config.get('SECRET')
PRODUCTION = config.get('PRODUCTION') PRODUCTION = config.get('PRODUCTION')
@ -54,16 +58,10 @@ class HalfAPI:
routes = [ Route('/', get_api_routes(DOMAINS)) ] 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: if not PRODUCTION:
for route in debug_routes(): for route in debug_routes():
@ -100,6 +98,7 @@ class HalfAPI:
) )
if SECRET: if SECRET:
self.SECRET = SECRET
self.application.add_middleware( self.application.add_middleware(
AuthenticationMiddleware, AuthenticationMiddleware,
backend=JWTAuthenticationBackend(secret_key=SECRET) backend=JWTAuthenticationBackend(secret_key=SECRET)
@ -115,4 +114,23 @@ class HalfAPI:
logger.info('CONFIG:\n%s', CONFIG) 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 application = HalfAPI().application

View File

@ -13,6 +13,7 @@ from .cli import cli
from ..conf import config, write_config, DOMAINSDICT from ..conf import config, write_config, DOMAINSDICT
from ..lib.schemas import schema_dict_dom from ..lib.schemas import schema_dict_dom
from ..lib.routes import api_routes
logger = logging.getLogger('halfapi') logger = logging.getLogger('halfapi')
@ -43,11 +44,15 @@ def list_routes(domain, m_dom):
Echoes the list of the **m_dom** active routes 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(): def list_api_routes():

View File

@ -42,8 +42,8 @@ from configparser import ConfigParser
import importlib import importlib
from .lib.domain import d_domains 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()) PROJECT_NAME = environ.get('HALFAPI_PROJECT_NAME') or os.path.basename(os.getcwd())
DOMAINSDICT = lambda: {} DOMAINSDICT = lambda: {}

View File

@ -22,15 +22,27 @@ from starlette.authentication import (
from starlette.requests import HTTPConnection from starlette.requests import HTTPConnection
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
logger = logging.getLogger('halfapi') logger = logging.getLogger('uvicorn.error')
SECRET=None
try: try:
from ..conf import SECRET from ..conf import SECRET
except ImportError as exc: except ImportError as exc:
logger.error('Could not import SECRET variable from conf module,'\ logger.error('Could not import SECRET variable from conf module,'\
' using HALFAPI_SECRET environment variable') ' 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): class JWTUser(BaseUser):
@ -119,7 +131,7 @@ class JWTAuthenticationBackend(AuthenticationBackend):
PRODUCTION = conn.scope['app'].debug == False PRODUCTION = conn.scope['app'].debug == False
if not token and not is_check_call: if not token and not is_check_call:
return AuthCredentials(), UnauthenticatedUser() return AuthCredentials(), Nobody()
try: try:
if token and not is_fake_user_id: if token and not is_fake_user_id:
@ -142,7 +154,7 @@ class JWTAuthenticationBackend(AuthenticationBackend):
if token: if token:
return AuthCredentials(), CheckUser(payload['user_id']) return AuthCredentials(), CheckUser(payload['user_id'])
else: else:
return AuthCredentials(), UnauthenticatedUser() return AuthCredentials(), Nobody()
if PRODUCTION and 'debug' in payload.keys() and payload['debug']: if PRODUCTION and 'debug' in payload.keys() and payload['debug']:
@ -177,7 +189,7 @@ class JWTWebSocketAuthenticationBackend(AuthenticationBackend):
) -> typing.Optional[typing.Tuple["AuthCredentials", "BaseUser"]]: ) -> typing.Optional[typing.Tuple["AuthCredentials", "BaseUser"]]:
if self.query_param_name not in conn.query_params: if self.query_param_name not in conn.query_params:
return AuthCredentials(), UnauthenticatedUser() return AuthCredentials(), Nobody()
token = conn.query_params[self.query_param_name] token = conn.query_params[self.query_param_name]

View File

@ -21,6 +21,8 @@ import orjson
# asgi framework # asgi framework
from starlette.responses import PlainTextResponse, Response, JSONResponse from starlette.responses import PlainTextResponse, Response, JSONResponse
from .jwt_middleware import JWTUser, Nobody
__all__ = [ __all__ = [
'HJSONResponse', 'HJSONResponse',
@ -82,11 +84,16 @@ class ORJSONResponse(JSONResponse):
list_types = { list_types = {
set set
} }
jsonable_types = {
JWTUser, Nobody
}
if type(typ) in str_types: if type(typ) in str_types:
return str(typ) return str(typ)
if type(typ) in list_types: if type(typ) in list_types:
return list(typ) return list(typ)
if type(typ) in jsonable_types:
return typ.json
raise TypeError(f'Type {type(typ)} is not handled by ORJSONResponse') raise TypeError(f'Type {type(typ)} is not handled by ORJSONResponse')

31
halfapi/logging.py Normal file
View File

@ -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')

1
scripts/whoami.sh Normal file
View File

@ -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')

View File

@ -5,9 +5,10 @@ from starlette.testclient import TestClient
import json 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) c = TestClient(application_debug)
r = c.get('/halfapi/current_user') r = c.get('/halfapi/whoami')
assert r.status_code == 200 assert r.status_code == 200
def test_log(application_debug): def test_log(application_debug):