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
__version__ = '0.5.10'
__version__ = '0.5.11'
def version():
return f'HalfAPI version:{__version__}'

View File

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

View File

@ -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():

View File

@ -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: {}

View File

@ -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]

View File

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

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
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):